Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed count, error state and created empty state component for scheduled post and draft #8626

Open
wants to merge 12 commits into
base: scheduled-post-options
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,4 @@ const ScheduledPostTooltip = ({onClose}: Props) => {
};

export default ScheduledPostTooltip;

29 changes: 27 additions & 2 deletions app/components/scheduled_post_indicator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,48 @@
// See LICENSE.txt for license information.

import {withDatabase, withObservables} from '@nozbe/watermelondb/react';
import {map} from 'rxjs/operators';
import {of} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';

import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {observeScheduledPostCountForChannel, observeScheduledPostCountForDMsAndGMs, observeScheduledPostCountForThread} from '@queries/servers/scheduled_post';
import {observeCurrentChannelId, observeCurrentTeamId} from '@queries/servers/system';
import {observeCurrentUser} from '@queries/servers/user';

import {ScheduledPostIndicator} from './scheduled_post_indicator';

const enhance = withObservables([], ({database}) => {
import type {WithDatabaseArgs} from '@typings/database/database';

type Props = WithDatabaseArgs & {
channelId?: string;
channelType?: ChannelType;
isCRTEnabled: boolean;
rootId?: string;
}

const enhance = withObservables(['channelId', 'channelType', 'isCRTEnabled', 'rootId'], ({database, channelId, channelType, isCRTEnabled, rootId}: Props) => {
const currentUser = observeCurrentUser(database);
const currentTeamId = observeCurrentTeamId(database);
const currentChannelId = observeCurrentChannelId(database);
const preferences = queryDisplayNamePreferences(database).
observeWithColumns(['value']);
const isMilitaryTime = preferences.pipe(map((prefs) => getDisplayNamePreferenceAsBool(prefs, 'use_military_time')));

let scheduledPostCount = of(0);
if (rootId) {
scheduledPostCount = observeScheduledPostCountForThread(database, rootId);
} else if ((channelType === 'D' || channelType === 'G') && channelId) {
scheduledPostCount = observeScheduledPostCountForDMsAndGMs(database, channelId, isCRTEnabled);
} else if (channelType === 'O' && channelId) {
scheduledPostCount = currentTeamId.pipe(switchMap((teamId) => observeScheduledPostCountForChannel(database, teamId, channelId, isCRTEnabled)));
}

return {
currentUser,
isMilitaryTime,
scheduledPostCount,
currentChannelId,
};
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,180 +1,109 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import {Database} from '@nozbe/watermelondb';
import {fireEvent, screen} from '@testing-library/react-native';
import React from 'react';
import {DeviceEventEmitter} from 'react-native';

import {Events} from '@constants';
import {DRAFT} from '@constants/screens';
import NetworkManager from '@managers/network_manager';
import {renderWithEverything} from '@test/intl-test-helper';
import TestHelper from '@test/test_helper';

import ScheduledPostIndicator from './';
import {ScheduledPostIndicator} from './scheduled_post_indicator';

import type ServerDataOperator from '@database/operator/server_data_operator';
import type {Database} from '@nozbe/watermelondb';

const SERVER_URL = 'https://appv1.mattermost.com';
jest.mock('@utils/theme', () => ({
changeOpacity: jest.fn().mockReturnValue('rgba(0,0,0,0.5)'),
makeStyleSheetFromTheme: jest.fn().mockReturnValue(() => ({
wrapper: {},
container: {},
text: {},
link: {},
})),
}));

// this is needed when using the useServerUrl hook
jest.mock('@context/server', () => ({
useServerUrl: jest.fn(() => SERVER_URL),
jest.mock('@actions/local/draft', () => ({
switchToGlobalDrafts: jest.fn(),
}));

describe('components/scheduled_post_indicator', () => {
jest.mock('@components/formatted_time', () => {
const MockFormattedTime = (props: any) => {
// Store props for test assertions
MockFormattedTime.mockProps = props;
return null;
};
MockFormattedTime.mockProps = {};
return MockFormattedTime;
});

describe('ScheduledPostIndicator', () => {
let database: Database;
let operator: ServerDataOperator;

beforeEach(async () => {
const server = await TestHelper.setupServerDatabase(SERVER_URL);
beforeAll(async () => {
const server = await TestHelper.setupServerDatabase();
database = server.database;
operator = server.operator;
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-02-24T12:00:00Z'));
});

afterEach(async () => {
await TestHelper.tearDown();
NetworkManager.invalidateClient(SERVER_URL);
afterAll(() => {
jest.useRealTimers();
});

it('should render single scheduled post indicator correctly', async () => {
const {getByText} = renderWithEverything(
<ScheduledPostIndicator
isThread={false}
scheduledPostCount={1}
/>,
{database},
);

await screen.findByTestId('scheduled_post_indicator_single_time');
expect(getByText(/Message scheduled for/)).toBeVisible();
expect(getByText(/See all./)).toBeVisible();
});
const baseProps = {
isMilitaryTime: false,
scheduledPostCount: 1,
};

it('should render multiple scheduled posts indicator for channel', async () => {
const {getByText} = renderWithEverything(
<ScheduledPostIndicator
isThread={false}
scheduledPostCount={10}
/>,
{database},
);

expect(getByText(/10 scheduled messages in channel./)).toBeVisible();
expect(getByText(/See all./)).toBeVisible();
});
test('should not render when scheduledPostCount is 0', () => {
const props = {
...baseProps,
scheduledPostCount: 0,
channelId: 'channel_id',
};

it('should render multiple scheduled posts indicator for thread', async () => {
const {getByText} = renderWithEverything(
<ScheduledPostIndicator
isThread={true}
scheduledPostCount={10}
/>,
{database},
);

expect(getByText(/10 scheduled messages in thread./)).toBeVisible();
expect(getByText(/See all./)).toBeVisible();
renderWithEverything(<ScheduledPostIndicator {...props}/>, {database});
expect(screen.queryByText(/scheduled/i)).toBeNull();
});

it('renders with military time when preference is set', async () => {
await operator.handlePreferences({
preferences: [
{
user_id: 'user_1',
category: 'display_settings',
name: 'use_military_time',
value: 'true',
},
],
prepareRecordsOnly: false,
});

const {getByText, findByTestId} = renderWithEverything(
<ScheduledPostIndicator
scheduledPostCount={1}
/>,
{database},
);

const timeElement = await findByTestId('scheduled_post_indicator_single_time');
expect(timeElement).toBeVisible();

expect(getByText(/19:41/)).toBeVisible();
});
test('should render multiple posts message when scheduledPostCount is greater than 1', () => {
const props = {
...baseProps,
scheduledPostCount: 2,
channelId: 'channel_id',
};

it('renders with 12-hour time when preference is set to 12 hours', async () => {
await operator.handlePreferences({
preferences: [
{
user_id: 'user_1',
category: 'display_settings',
name: 'use_military_time',
value: 'false',
},
],
prepareRecordsOnly: false,
});

const {getByText, findByTestId} = renderWithEverything(
<ScheduledPostIndicator
scheduledPostCount={1}
/>,
{database},
);

const timeElement = await findByTestId('scheduled_post_indicator_single_time');
expect(timeElement).toBeVisible();

expect(getByText(/7:41 PM/)).toBeVisible();
renderWithEverything(<ScheduledPostIndicator {...props}/>, {database});
expect(screen.getByText(/2 scheduled messages in channel/i)).toBeTruthy();
});

it('renders with 12-hour time when preference is not set', async () => {
const {getByText, findByTestId} = renderWithEverything(
<ScheduledPostIndicator
scheduledPostCount={1}
/>,
{database},
);

const timeElement = await findByTestId('scheduled_post_indicator_single_time');
expect(timeElement).toBeVisible();
test('should render thread message when isThread is true', () => {
const props = {
...baseProps,
scheduledPostCount: 2,
isThread: true,
channelId: 'channel_id',
};

expect(getByText(/7:41 PM/)).toBeVisible();
renderWithEverything(<ScheduledPostIndicator {...props}/>, {database});
expect(screen.getByText(/2 scheduled messages in thread/i)).toBeTruthy();
});

it('handles missing current user', async () => {
const {getByText, findByTestId} = renderWithEverything(
<ScheduledPostIndicator
scheduledPostCount={1}
/>,
{database},
);

const timeElement = await findByTestId('scheduled_post_indicator_single_time');
expect(timeElement).toBeVisible();
expect(getByText(/Message scheduled for/)).toBeVisible();
});

it('handles See all link press correctly', async () => {
test('should handle see all scheduled posts click', () => {
const props = {
...baseProps,
scheduledPostCount: 2,
channelId: 'channel_id',
};
const emitSpy = jest.spyOn(DeviceEventEmitter, 'emit');
const switchToGlobalDraftsSpy = jest.spyOn(require('@actions/local/draft'), 'switchToGlobalDrafts');

const {getByText} = renderWithEverything(
<ScheduledPostIndicator
scheduledPostCount={1}
/>,
{database},
);
renderWithEverything(<ScheduledPostIndicator {...props}/>, {database});

const seeAllLink = getByText('See all.');
fireEvent.press(seeAllLink);
fireEvent.press(screen.getByText('See all.'));

expect(emitSpy).toHaveBeenCalledWith(Events.ACTIVE_SCREEN, DRAFT);
expect(switchToGlobalDraftsSpy).toHaveBeenCalledWith(1);

emitSpy.mockRestore();
switchToGlobalDraftsSpy.mockRestore();
expect(emitSpy).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import {DeviceEventEmitter, Text, View} from 'react-native';

import {switchToGlobalDrafts} from '@actions/local/draft';
import CompassIcon from '@components/compass_icon';
import FormattedTime from '@components/formatted_time';
import ScheduledPostIndicatorWithDatetime from '@components/scheduled_post_indicator_with_datetime';
import {Events} from '@constants';
import {DRAFT} from '@constants/screens';
import {useTheme} from '@context/theme';
import {DRAFT_SCREEN_TAB_SCHEDULED_POSTS} from '@screens/global_drafts';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {getUserTimezone} from '@utils/user';

import type UserModel from '@typings/database/models/servers/user';

Expand Down Expand Up @@ -54,9 +53,16 @@ type Props = {
isMilitaryTime: boolean;
isThread?: boolean;
scheduledPostCount?: number;
channelId: string;
}

export function ScheduledPostIndicator({currentUser, isMilitaryTime, isThread, scheduledPostCount = 0}: Props) {
export function ScheduledPostIndicator({
currentUser,
isMilitaryTime,
isThread,
scheduledPostCount = 0,
channelId,
}: Props) {
const theme = useTheme();
const styles = getStyleSheet(theme);

Expand All @@ -70,26 +76,11 @@ export function ScheduledPostIndicator({currentUser, isMilitaryTime, isThread, s
if (scheduledPostCount === 0) {
return null;
} else if (scheduledPostCount === 1) {
// eslint-disable-next-line no-warning-comments
//TODO: remove this hardcoded value with actual value
const value = 1738611689000;

const dateTime = (
<FormattedTime
timezone={getUserTimezone(currentUser)}
isMilitaryTime={isMilitaryTime}
value={value}
testID='scheduled_post_indicator_single_time'
/>
);

scheduledPostText = (
<FormattedMessage
id='scheduled_post.channel_indicator.single'
defaultMessage='Message scheduled for {dateTime}.'
values={{
dateTime,
}}
<ScheduledPostIndicatorWithDatetime
channelId={channelId}
currentUser={currentUser}
isMilitaryTime={isMilitaryTime}
/>
);
} else {
Expand Down
Loading