Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/constants/screens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const THREAD_FOLLOW_BUTTON = 'ThreadFollowButton';
export const THREAD_OPTIONS = 'ThreadOptions';
export const USER_PROFILE = 'UserProfile';
export const SHOW_TRANSLATION = 'ShowTranslation';
export const WATERMARK = 'Watermark';

export default {
ABOUT,
Expand Down Expand Up @@ -188,6 +189,7 @@ export default {
THREAD_OPTIONS,
USER_PROFILE,
SHOW_TRANSLATION,
WATERMARK,
...PLAYBOOKS_SCREENS,
...AGENTS_SCREENS,
} as const;
Expand Down Expand Up @@ -216,6 +218,7 @@ export const SCREENS_WITH_TRANSPARENT_BACKGROUND = new Set<string>([
REVIEW_APP,
SNACK_BAR,
GENERIC_OVERLAY,
WATERMARK,
]);

export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
Expand Down
49 changes: 49 additions & 0 deletions app/screens/home/channel_list/channel_list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ jest.mock('@react-native-camera-roll/camera-roll', () => ({
},
}));

jest.mock('@screens/navigation', () => ({
resetToTeams: jest.fn(),
openToS: jest.fn(),
showWatermarkOverlay: jest.fn(),
dismissWatermarkOverlay: jest.fn(),
}));

function getBaseProps(): ComponentProps<typeof ChannelListScreen> {
return {
hasChannels: true,
Expand All @@ -32,6 +39,7 @@ function getBaseProps(): ComponentProps<typeof ChannelListScreen> {
hasTeams: true,
isCRTEnabled: true,
isLicensed: true,
isWatermarkEnabled: false,
launchType: 'normal',
showIncomingCalls: true,
showToS: false,
Expand All @@ -55,3 +63,44 @@ describe('performance metrics', () => {
});
});
});

describe('watermark overlay', () => {
let database: Database;
const serverUrl = 'http://www.someserverurl.com';

beforeAll(async () => {
const server = await TestHelper.setupServerDatabase(serverUrl);
database = server.database;
});

beforeEach(() => {
jest.clearAllMocks();
});

it('should show the watermark overlay when isWatermarkEnabled is true', async () => {
const {showWatermarkOverlay} = require('@screens/navigation');
const props = {...getBaseProps(), isWatermarkEnabled: true};
renderWithEverything(<ChannelListScreen {...props}/>, {database, serverUrl});
await waitFor(() => {
expect(showWatermarkOverlay).toHaveBeenCalledTimes(1);
});
});

it('should not show the watermark overlay when isWatermarkEnabled is false', async () => {
const {showWatermarkOverlay} = require('@screens/navigation');
const props = {...getBaseProps(), isWatermarkEnabled: false};
renderWithEverything(<ChannelListScreen {...props}/>, {database, serverUrl});
await waitFor(() => {
expect(showWatermarkOverlay).not.toHaveBeenCalled();
});
});

it('should dismiss the watermark overlay when isWatermarkEnabled changes to false', async () => {
const {dismissWatermarkOverlay} = require('@screens/navigation');
const props = {...getBaseProps(), isWatermarkEnabled: false};
renderWithEverything(<ChannelListScreen {...props}/>, {database, serverUrl});
await waitFor(() => {
expect(dismissWatermarkOverlay).toHaveBeenCalledTimes(1);
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
larkox marked this conversation as resolved.
});
11 changes: 10 additions & 1 deletion app/screens/home/channel_list/channel_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
import {resetToTeams, openToS} from '@screens/navigation';
import {resetToTeams, openToS, showWatermarkOverlay, dismissWatermarkOverlay} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {isMainActivity} from '@utils/helpers';
import {tryRunAppReview} from '@utils/reviews';
Expand All @@ -37,6 +37,7 @@ type ChannelProps = {
hasTeams: boolean;
hasMoreThanOneTeam: boolean;
isLicensed: boolean;
isWatermarkEnabled: boolean;
showToS: boolean;
launchType: LaunchType;
coldStart?: boolean;
Expand Down Expand Up @@ -174,6 +175,14 @@ const ChannelListScreen = (props: ChannelProps) => {
}
}, [props.launchType, props.coldStart]);

useEffect(() => {
if (props.isWatermarkEnabled) {
Comment thread
asaadmahmood marked this conversation as resolved.
showWatermarkOverlay();
} else {
dismissWatermarkOverlay();
}
}, [props.isWatermarkEnabled]);

useEffect(() => {
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
PerformanceMetricsManager.measureTimeToInteraction();
Expand Down
3 changes: 2 additions & 1 deletion app/screens/home/channel_list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {distinctUntilChanged, switchMap} from 'rxjs/operators';

import {observeIncomingCalls} from '@calls/state';
import {queryAllMyChannelsForTeam} from '@queries/servers/channel';
import {observeCurrentTeamId, observeCurrentUserId, observeLicense} from '@queries/servers/system';
import {observeConfigBooleanValue, observeCurrentTeamId, observeCurrentUserId, observeLicense} from '@queries/servers/system';
import {queryMyTeams} from '@queries/servers/team';
import {observeShowToS} from '@queries/servers/terms_of_service';
import {observeIsCRTEnabled} from '@queries/servers/thread';
Expand All @@ -30,6 +30,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
);

return {
isWatermarkEnabled: observeConfigBooleanValue(database, 'ExperimentalEnableWatermark'),
isCRTEnabled: observeIsCRTEnabled(database),
hasTeams: teamsCount.pipe(
switchMap((v) => of$(v > 0)),
Expand Down
5 changes: 5 additions & 0 deletions app/screens/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@ Navigation.setLazyComponentRegistrator((screenName) => {
case Screens.SHOW_TRANSLATION:
screen = withServerDatabase(require('@screens/show_translation').default);
break;
case Screens.WATERMARK: {
const watermarkScreen = withServerDatabase(require('@screens/watermark').default);
Navigation.registerComponent(Screens.WATERMARK, () => watermarkScreen);
return;
}
case Screens.CALL:
screen = withServerDatabase(require('@calls/screens/call_screen').default);
break;
Expand Down
50 changes: 45 additions & 5 deletions app/screens/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ const alpha = {
};
let subscriptions: Array<EmitterSubscription | EventSubscription> | undefined;

// Watermark overlay state.
let watermarkShouldBeShown = false;
let watermarkCurrentlyShown = false;

// IDs of non-watermark overlays that are currently shown.
// dismissAllOverlays() uses this set so it can dismiss each overlay individually
// without ever touching the watermark overlay, preventing any visual flash.
const shownNonWatermarkOverlayIds = new Set<string>();

export const allOrientations: LayoutOrientation[] = ['sensor', 'sensorLandscape', 'sensorPortrait', 'landscape', 'portrait'];
export const portraitOrientation: LayoutOrientation[] = ['portrait'];

Expand Down Expand Up @@ -121,6 +130,16 @@ function onPoppedListener({componentId}: ScreenPoppedEvent) {

function onScreenWillAppear(event: ComponentWillAppearEvent) {
showBottomTabsIfNeeded(event.componentId as AvailableScreens);

if (event.componentId === Screens.WATERMARK) {
return;
}

// Safety net: re-show watermark if it was somehow dismissed (e.g., by an error path).
if (watermarkShouldBeShown && !watermarkCurrentlyShown) {
watermarkCurrentlyShown = true;
showOverlay(Screens.WATERMARK, {}, {overlay: {interceptTouchOutside: false}}, Screens.WATERMARK);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
larkox marked this conversation as resolved.
}

export const loginAnimationOptions = () => {
Expand Down Expand Up @@ -390,6 +409,7 @@ export function resetToSelectServer(passProps: LaunchProps) {
name: Screens.SERVER,
passProps: {
...passProps,
animated: false,
theme,
},
options: {
Expand Down Expand Up @@ -821,6 +841,11 @@ export function showOverlay(name: AvailableScreens, passProps = {}, options: Opt
return;
}

const overlayId = id ?? name;
if (overlayId !== Screens.WATERMARK) {
shownNonWatermarkOverlayIds.add(overlayId);
}

const defaultOptions = {
layout: {
backgroundColor: 'transparent',
Expand All @@ -842,6 +867,7 @@ export function showOverlay(name: AvailableScreens, passProps = {}, options: Opt
}

export async function dismissOverlay(componentId: string) {
shownNonWatermarkOverlayIds.delete(componentId);
try {
await Navigation.dismissOverlay(componentId);
} catch (error) {
Expand All @@ -851,11 +877,11 @@ export async function dismissOverlay(componentId: string) {
}

export async function dismissAllOverlays() {
try {
await Navigation.dismissAllOverlays();
} catch {
// do nothing
}
// Dismiss each non-watermark overlay individually so the watermark overlay
// is never touched and never flashes during navigation.
const ids = [...shownNonWatermarkOverlayIds];
shownNonWatermarkOverlayIds.clear();
await Promise.all(ids.map((id) => Navigation.dismissOverlay(id).catch(() => { /* already gone */ })));
Comment thread
larkox marked this conversation as resolved.
Outdated
}

type BottomSheetArgs = {
Expand Down Expand Up @@ -961,6 +987,20 @@ export const showShareFeedbackOverlay = () => {
);
};

export const showWatermarkOverlay = () => {
watermarkShouldBeShown = true;
if (!watermarkCurrentlyShown) {
watermarkCurrentlyShown = true;
showOverlay(Screens.WATERMARK, {}, {overlay: {interceptTouchOutside: false}}, Screens.WATERMARK);
}
};

export const dismissWatermarkOverlay = () => {
watermarkShouldBeShown = false;
watermarkCurrentlyShown = false;
dismissOverlay(Screens.WATERMARK);
};

export async function findChannels(title: string, theme: Theme) {
const options: Options = {};
const closeButtonId = 'close-find-channels';
Expand Down
55 changes: 55 additions & 0 deletions app/screens/watermark/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';

import {renderWithEverything, act} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';

import WatermarkScreenExport from './index';

import type {Database} from '@nozbe/watermelondb';

// The exported component is the HOC-wrapped version; we need to render it
// with full database context so withDatabase and withObservables can resolve.
describe('WatermarkScreen', () => {
let database: Database;
const serverUrl = 'http://www.someserver.com';

beforeAll(async () => {
const server = await TestHelper.setupServerDatabase(serverUrl);
database = server.database;
});

it('should render watermark text containing username and domain', async () => {
const {getAllByText} = renderWithEverything(
<WatermarkScreenExport componentId='Watermark'/>,
{database, serverUrl},
);

// The watermark renders its text multiple times (grid pattern).
// Verify at least one instance of the domain is present.
await act(async () => {
// Allow observables to emit
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
larkox marked this conversation as resolved.

// Domain extracted from serverUrl should be in the watermark text
const items = getAllByText(/www\.someserver\.com/);
Comment thread
larkox marked this conversation as resolved.
Outdated
expect(items.length).toBeGreaterThan(0);
});

it('should render multiple copies of the watermark text for the grid pattern', async () => {
const {getAllByText} = renderWithEverything(
<WatermarkScreenExport componentId='Watermark'/>,
{database, serverUrl},
);

await act(async () => {
// Allow observables to emit
});

// The watermark renders a grid of repeated text — expect more than one copy
const items = getAllByText(/www\.someserver\.com/);
expect(items.length).toBeGreaterThan(1);
});
});
Loading
Loading