mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-60874 - show usertz in scheduled message options (#29116)
* MM-60874 - show usertz in schedule message time options * get the teammate time working with moment * replace moment with luxon * add unit tests to core_menu_options * fix linter * address pr feedback * MM-60123 - store most recently custom time (#29120) * MM-60123 - store most recently used custom time * add the most recent used custom date option * improve the code and add some validations * add unit tests to cover the recently used time scenario * clean up code in unit tests * fix linter report * fix translation files * address PR feedback * do not show recent custom time if matches any of the existing options * prevent show date if is in the past and update unit tests * format custom date based on week and add unit tests for new scenarios * remove unused mock, clean up code, extract code to function * extract recent used custom date logic to be an independent component * address PR comments for improvements * turn function into selector
This commit is contained in:
parent
c90e562528
commit
37d97e8024
@ -1,30 +1,15 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {DateTime} from 'luxon';
|
import React from 'react';
|
||||||
import React, {useEffect, useState} from 'react';
|
|
||||||
import {useSelector} from 'react-redux';
|
|
||||||
|
|
||||||
import {getDirectChannel} from 'mattermost-redux/selectors/entities/channels';
|
|
||||||
import {isScheduledPostsEnabled} from 'mattermost-redux/selectors/entities/scheduled_posts';
|
|
||||||
import {getTimezoneForUserProfile} from 'mattermost-redux/selectors/entities/timezone';
|
|
||||||
import {getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
|
||||||
|
|
||||||
import RemoteUserHour from 'components/advanced_text_editor/remote_user_hour';
|
import RemoteUserHour from 'components/advanced_text_editor/remote_user_hour';
|
||||||
import ScheduledPostIndicator from 'components/advanced_text_editor/scheduled_post_indicator/scheduled_post_indicator';
|
import ScheduledPostIndicator from 'components/advanced_text_editor/scheduled_post_indicator/scheduled_post_indicator';
|
||||||
|
|
||||||
import Constants, {UserStatuses} from 'utils/constants';
|
import useTimePostBoxIndicator from '../use_post_box_indicator';
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
|
||||||
|
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
const DEFAULT_TIMEZONE = {
|
|
||||||
useAutomaticTimezone: true,
|
|
||||||
automaticTimezone: '',
|
|
||||||
manualTimezone: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
teammateDisplayName: string;
|
teammateDisplayName: string;
|
||||||
@ -33,37 +18,12 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PostBoxIndicator({channelId, teammateDisplayName, location, postId}: Props) {
|
export default function PostBoxIndicator({channelId, teammateDisplayName, location, postId}: Props) {
|
||||||
const teammateId = useSelector((state: GlobalState) => getDirectChannel(state, channelId)?.teammate_id || '');
|
const {
|
||||||
const isTeammateDND = useSelector((state: GlobalState) => (teammateId ? getStatusForUserId(state, teammateId) === UserStatuses.DND : false));
|
showRemoteUserHour,
|
||||||
const isDM = useSelector((state: GlobalState) => Boolean(getDirectChannel(state, channelId)?.teammate_id));
|
isScheduledPostEnabled,
|
||||||
const showDndWarning = isTeammateDND && isDM;
|
currentUserTimesStamp,
|
||||||
|
teammateTimezone,
|
||||||
const [timestamp, setTimestamp] = useState(0);
|
} = useTimePostBoxIndicator(channelId);
|
||||||
const [showIt, setShowIt] = useState(false);
|
|
||||||
|
|
||||||
const teammateTimezone = useSelector((state: GlobalState) => {
|
|
||||||
const teammate = teammateId ? getUser(state, teammateId) : undefined;
|
|
||||||
return teammate ? getTimezoneForUserProfile(teammate) : DEFAULT_TIMEZONE;
|
|
||||||
}, (a, b) => a.automaticTimezone === b.automaticTimezone &&
|
|
||||||
a.manualTimezone === b.manualTimezone &&
|
|
||||||
a.useAutomaticTimezone === b.useAutomaticTimezone);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const teammateUserDate = DateTime.local().setZone(teammateTimezone.useAutomaticTimezone ? teammateTimezone.automaticTimezone : teammateTimezone.manualTimezone);
|
|
||||||
setTimestamp(teammateUserDate.toMillis());
|
|
||||||
setShowIt(teammateUserDate.get('hour') >= Constants.REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY || teammateUserDate.get('hour') < Constants.REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY);
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
const teammateUserDate = DateTime.local().setZone(teammateTimezone.useAutomaticTimezone ? teammateTimezone.automaticTimezone : teammateTimezone.manualTimezone);
|
|
||||||
setTimestamp(teammateUserDate.toMillis());
|
|
||||||
setShowIt(teammateUserDate.get('hour') >= Constants.REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY || teammateUserDate.get('hour') < Constants.REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY);
|
|
||||||
}, 1000 * 60);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [teammateTimezone.useAutomaticTimezone, teammateTimezone.automaticTimezone, teammateTimezone.manualTimezone]);
|
|
||||||
|
|
||||||
const isScheduledPostEnabled = useSelector(isScheduledPostsEnabled);
|
|
||||||
|
|
||||||
const showRemoteUserHour = showDndWarning && showIt && timestamp !== 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='postBoxIndicator'>
|
<div className='postBoxIndicator'>
|
||||||
@ -71,7 +31,7 @@ export default function PostBoxIndicator({channelId, teammateDisplayName, locati
|
|||||||
showRemoteUserHour &&
|
showRemoteUserHour &&
|
||||||
<RemoteUserHour
|
<RemoteUserHour
|
||||||
displayName={teammateDisplayName}
|
displayName={teammateDisplayName}
|
||||||
timestamp={timestamp}
|
timestamp={currentUserTimesStamp}
|
||||||
teammateTimezone={teammateTimezone}
|
teammateTimezone={teammateTimezone}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -5,15 +5,19 @@ import moment from 'moment';
|
|||||||
import type {Moment} from 'moment-timezone';
|
import type {Moment} from 'moment-timezone';
|
||||||
import React, {useCallback, useMemo, useState} from 'react';
|
import React, {useCallback, useMemo, useState} from 'react';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import {useSelector} from 'react-redux';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||||
import {generateCurrentTimezoneLabel, getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
import {generateCurrentTimezoneLabel, getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||||
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DMUserTimezone,
|
DMUserTimezone,
|
||||||
} from 'components/advanced_text_editor/send_button/scheduled_post_custom_time_modal/dm_user_timezone';
|
} from 'components/advanced_text_editor/send_button/scheduled_post_custom_time_modal/dm_user_timezone';
|
||||||
import DateTimePickerModal from 'components/date_time_picker_modal/date_time_picker_modal';
|
import DateTimePickerModal from 'components/date_time_picker_modal/date_time_picker_modal';
|
||||||
|
|
||||||
|
import {scheduledPosts} from 'utils/constants';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
onExited: () => void;
|
onExited: () => void;
|
||||||
@ -25,19 +29,35 @@ export default function ScheduledPostCustomTimeModal({channelId, onExited, onCon
|
|||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
const [errorMessage, setErrorMessage] = useState<string>();
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
const userTimezone = useSelector(getCurrentTimezone);
|
const userTimezone = useSelector(getCurrentTimezone);
|
||||||
|
const now = moment().tz(userTimezone);
|
||||||
|
const currentUserId = useSelector(getCurrentUserId);
|
||||||
|
const dispatch = useDispatch();
|
||||||
const [selectedDateTime, setSelectedDateTime] = useState<Moment>(() => {
|
const [selectedDateTime, setSelectedDateTime] = useState<Moment>(() => {
|
||||||
if (initialTime) {
|
if (initialTime) {
|
||||||
return initialTime;
|
return initialTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = moment().tz(userTimezone);
|
|
||||||
return now.add(1, 'days').set({hour: 9, minute: 0, second: 0, millisecond: 0});
|
return now.add(1, 'days').set({hour: 9, minute: 0, second: 0, millisecond: 0});
|
||||||
});
|
});
|
||||||
|
|
||||||
const userTimezoneLabel = useMemo(() => generateCurrentTimezoneLabel(userTimezone), [userTimezone]);
|
const userTimezoneLabel = useMemo(() => generateCurrentTimezoneLabel(userTimezone), [userTimezone]);
|
||||||
|
|
||||||
const handleOnConfirm = useCallback(async (dateTime: Moment) => {
|
const handleOnConfirm = useCallback(async (dateTime: Moment) => {
|
||||||
const response = await onConfirm(dateTime.valueOf());
|
const selectedTime = dateTime.valueOf();
|
||||||
|
const response = await onConfirm(selectedTime);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
savePreferences(
|
||||||
|
currentUserId,
|
||||||
|
[{
|
||||||
|
user_id: currentUserId,
|
||||||
|
category: scheduledPosts.SCHEDULED_POSTS,
|
||||||
|
name: scheduledPosts.RECENTLY_USED_CUSTOM_TIME,
|
||||||
|
value: JSON.stringify({update_at: moment().tz(userTimezone).valueOf(), timestamp: selectedTime}),
|
||||||
|
}],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
setErrorMessage(response.error);
|
setErrorMessage(response.error);
|
||||||
} else {
|
} else {
|
||||||
|
@ -0,0 +1,155 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {DateTime} from 'luxon';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import useTimePostBoxIndicator from 'components/advanced_text_editor/use_post_box_indicator';
|
||||||
|
|
||||||
|
import {renderWithContext, fireEvent, screen} from 'tests/react_testing_utils';
|
||||||
|
|
||||||
|
import CoreMenuOptions from './core_menu_options';
|
||||||
|
|
||||||
|
jest.mock('components/menu', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
Item: jest.fn(({labels, trailingElements, children, ...props}) => (
|
||||||
|
<div {...props}>
|
||||||
|
{labels}
|
||||||
|
{children}
|
||||||
|
{trailingElements}
|
||||||
|
</div>
|
||||||
|
)),
|
||||||
|
Separator: jest.fn(() => <div className='menu-separator'/>),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('components/advanced_text_editor/use_post_box_indicator');
|
||||||
|
const mockedUseTimePostBoxIndicator = jest.mocked(useTimePostBoxIndicator);
|
||||||
|
|
||||||
|
const teammateDisplayName = 'John Doe';
|
||||||
|
const userCurrentTimezone = 'America/New_York';
|
||||||
|
const teammateTimezone = {
|
||||||
|
useAutomaticTimezone: true,
|
||||||
|
automaticTimezone: 'Europe/London',
|
||||||
|
manualTimezone: '',
|
||||||
|
};
|
||||||
|
const defaultUseTimePostBoxIndicatorReturnValue = {
|
||||||
|
userCurrentTimezone: 'America/New_York',
|
||||||
|
teammateTimezone,
|
||||||
|
teammateDisplayName,
|
||||||
|
isDM: false,
|
||||||
|
showRemoteUserHour: false,
|
||||||
|
currentUserTimesStamp: 0,
|
||||||
|
isScheduledPostEnabled: false,
|
||||||
|
showDndWarning: false,
|
||||||
|
teammateId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
entities: {
|
||||||
|
preferences: {
|
||||||
|
myPreferences: {},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
currentUserId: 'currentUserId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('CoreMenuOptions Component', () => {
|
||||||
|
const handleOnSelect = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
handleOnSelect.mockReset();
|
||||||
|
mockedUseTimePostBoxIndicator.mockReturnValue({
|
||||||
|
...defaultUseTimePostBoxIndicatorReturnValue,
|
||||||
|
isDM: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderComponent(state = initialState, handleOnSelectOverride = handleOnSelect) {
|
||||||
|
renderWithContext(
|
||||||
|
<CoreMenuOptions
|
||||||
|
handleOnSelect={handleOnSelectOverride}
|
||||||
|
channelId='channelId'
|
||||||
|
/>,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMockDate(weekday: number) {
|
||||||
|
const mockDate = DateTime.fromObject({weekday}, {zone: userCurrentTimezone}).toJSDate();
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(mockDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should render tomorrow option on Sunday', () => {
|
||||||
|
setMockDate(7); // Sunday
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.getByText(/Tomorrow at/)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/Monday at/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render tomorrow and next Monday options on Monday', () => {
|
||||||
|
setMockDate(1); // Monday
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.getByText(/Tomorrow at/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Next Monday at/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render Monday option on Friday', () => {
|
||||||
|
setMockDate(5); // Friday
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.getByText(/Monday at/)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/Tomorrow at/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include trailing element when isDM true', () => {
|
||||||
|
setMockDate(2); // Tuesday
|
||||||
|
|
||||||
|
mockedUseTimePostBoxIndicator.mockReturnValue({
|
||||||
|
...defaultUseTimePostBoxIndicatorReturnValue,
|
||||||
|
isDM: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
// Check the trailing element is rendered in the component
|
||||||
|
expect(screen.getAllByText(/John Doe/)[0]).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT include trailing element when isDM false', () => {
|
||||||
|
setMockDate(2); // Tuesday
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(screen.queryByText(/John Doe/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call handleOnSelect with the right timestamp if tomorrow option is clicked', () => {
|
||||||
|
setMockDate(3); // Wednesday
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const tomorrowOption = screen.getByText(/Tomorrow at/);
|
||||||
|
fireEvent.click(tomorrowOption);
|
||||||
|
|
||||||
|
const expectedTimestamp = DateTime.now().
|
||||||
|
setZone(userCurrentTimezone).
|
||||||
|
plus({days: 1}).
|
||||||
|
set({hour: 9, minute: 0, second: 0, millisecond: 0}).
|
||||||
|
toMillis();
|
||||||
|
|
||||||
|
expect(handleOnSelect).toHaveBeenCalledWith(expect.anything(), expectedTimestamp);
|
||||||
|
});
|
||||||
|
});
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import moment from 'moment';
|
import {DateTime} from 'luxon';
|
||||||
import React, {memo, useCallback, useEffect} from 'react';
|
import React, {memo, useCallback, useEffect} from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
@ -10,20 +10,44 @@ import {
|
|||||||
TrackPropertyUser, TrackPropertyUserAgent,
|
TrackPropertyUser, TrackPropertyUserAgent,
|
||||||
TrackScheduledPostsFeature,
|
TrackScheduledPostsFeature,
|
||||||
} from 'mattermost-redux/constants/telemetry';
|
} from 'mattermost-redux/constants/telemetry';
|
||||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
import {trackFeatureEvent} from 'actions/telemetry_actions';
|
import {trackFeatureEvent} from 'actions/telemetry_actions';
|
||||||
|
|
||||||
|
import useTimePostBoxIndicator from 'components/advanced_text_editor/use_post_box_indicator';
|
||||||
import * as Menu from 'components/menu';
|
import * as Menu from 'components/menu';
|
||||||
|
import type {Props as MenuItemProps} from 'components/menu/menu_item';
|
||||||
import Timestamp from 'components/timestamp';
|
import Timestamp from 'components/timestamp';
|
||||||
|
|
||||||
|
import RecentUsedCustomDate from './recent_used_custom_date';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleOnSelect: (e: React.FormEvent, scheduledAt: number) => void;
|
handleOnSelect: (e: React.FormEvent, scheduledAt: number) => void;
|
||||||
|
channelId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CoreMenuOptions({handleOnSelect}: Props) {
|
function getScheduledTimeInTeammateTimezone(userCurrentTimestamp: number, teammateTimezoneString: string): string {
|
||||||
const userTimezone = useSelector(getCurrentTimezone);
|
const scheduledTimeUTC = DateTime.fromMillis(userCurrentTimestamp, {zone: 'utc'});
|
||||||
|
const teammateScheduledTime = scheduledTimeUTC.setZone(teammateTimezoneString);
|
||||||
|
const formattedTime = teammateScheduledTime.toFormat('h:mm a');
|
||||||
|
return formattedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextWeekday(dateTime: DateTime, targetWeekday: number) {
|
||||||
|
const daysDifference = targetWeekday - dateTime.weekday;
|
||||||
|
const adjustedDays = (daysDifference + 7) % 7;
|
||||||
|
const deltaDays = adjustedDays === 0 ? 7 : adjustedDays;
|
||||||
|
return dateTime.plus({days: deltaDays});
|
||||||
|
}
|
||||||
|
|
||||||
|
function CoreMenuOptions({handleOnSelect, channelId}: Props) {
|
||||||
|
const {
|
||||||
|
userCurrentTimezone,
|
||||||
|
teammateTimezone,
|
||||||
|
teammateDisplayName,
|
||||||
|
isDM,
|
||||||
|
} = useTimePostBoxIndicator(channelId);
|
||||||
|
|
||||||
const currentUserId = useSelector(getCurrentUserId);
|
const currentUserId = useSelector(getCurrentUserId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -40,12 +64,19 @@ function CoreMenuOptions({handleOnSelect}: Props) {
|
|||||||
);
|
);
|
||||||
}, [currentUserId]);
|
}, [currentUserId]);
|
||||||
|
|
||||||
const today = moment().tz(userTimezone);
|
const now = DateTime.now().setZone(userCurrentTimezone);
|
||||||
const tomorrow9amTime = moment().
|
const tomorrow9amTime = DateTime.now().
|
||||||
tz(userTimezone).
|
setZone(userCurrentTimezone).
|
||||||
add(1, 'days').
|
plus({days: 1}).
|
||||||
set({hour: 9, minute: 0, second: 0, millisecond: 0}).
|
set({hour: 9, minute: 0, second: 0, millisecond: 0}).
|
||||||
valueOf();
|
toMillis();
|
||||||
|
|
||||||
|
const nextMonday = getNextWeekday(now, 1).set({
|
||||||
|
hour: 9,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
}).toMillis();
|
||||||
|
|
||||||
const timeComponent = (
|
const timeComponent = (
|
||||||
<Timestamp
|
<Timestamp
|
||||||
@ -54,6 +85,29 @@ function CoreMenuOptions({handleOnSelect}: Props) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const extraProps: Partial<MenuItemProps> = {};
|
||||||
|
|
||||||
|
if (isDM) {
|
||||||
|
const teammateTimezoneString = teammateTimezone.useAutomaticTimezone ? teammateTimezone.automaticTimezone : teammateTimezone.manualTimezone || 'UTC';
|
||||||
|
const scheduledTimeInTeammateTimezone = getScheduledTimeInTeammateTimezone(tomorrow9amTime, teammateTimezoneString);
|
||||||
|
const teammateTimeDisplay = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='create_post_button.option.schedule_message.options.teammate_user_hour'
|
||||||
|
defaultMessage="{time} {user}'s time"
|
||||||
|
values={{
|
||||||
|
user: (
|
||||||
|
<span className='userDisplayName'>
|
||||||
|
{teammateDisplayName}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
time: scheduledTimeInTeammateTimezone,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
extraProps.trailingElements = teammateTimeDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
const tomorrowClickHandler = useCallback((e) => handleOnSelect(e, tomorrow9amTime), [handleOnSelect, tomorrow9amTime]);
|
const tomorrowClickHandler = useCallback((e) => handleOnSelect(e, tomorrow9amTime), [handleOnSelect, tomorrow9amTime]);
|
||||||
|
|
||||||
const optionTomorrow = (
|
const optionTomorrow = (
|
||||||
@ -67,15 +121,11 @@ function CoreMenuOptions({handleOnSelect}: Props) {
|
|||||||
values={{'9amTime': timeComponent}}
|
values={{'9amTime': timeComponent}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
className='core-menu-options'
|
||||||
|
{...extraProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextMonday = moment().
|
|
||||||
tz(userTimezone).
|
|
||||||
day(8). // next monday; 1 = Monday, 8 = next Monday
|
|
||||||
set({hour: 9, minute: 0, second: 0, millisecond: 0}). // 9 AM
|
|
||||||
valueOf();
|
|
||||||
|
|
||||||
const nextMondayClickHandler = useCallback((e) => handleOnSelect(e, nextMonday), [handleOnSelect, nextMonday]);
|
const nextMondayClickHandler = useCallback((e) => handleOnSelect(e, nextMonday), [handleOnSelect, nextMonday]);
|
||||||
|
|
||||||
const optionNextMonday = (
|
const optionNextMonday = (
|
||||||
@ -89,6 +139,8 @@ function CoreMenuOptions({handleOnSelect}: Props) {
|
|||||||
values={{'9amTime': timeComponent}}
|
values={{'9amTime': timeComponent}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
className='core-menu-options'
|
||||||
|
{...extraProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -105,14 +157,16 @@ function CoreMenuOptions({handleOnSelect}: Props) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
className='core-menu-options'
|
||||||
|
{...extraProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
let options: React.ReactElement[] = [];
|
let options: React.ReactElement[] = [];
|
||||||
|
|
||||||
switch (today.day()) {
|
switch (now.weekday) {
|
||||||
// Sunday
|
// Sunday
|
||||||
case 0:
|
case 7:
|
||||||
options = [optionTomorrow];
|
options = [optionTomorrow];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -133,9 +187,15 @@ function CoreMenuOptions({handleOnSelect}: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<>
|
||||||
{options}
|
{options}
|
||||||
</React.Fragment>
|
<RecentUsedCustomDate
|
||||||
|
handleOnSelect={handleOnSelect}
|
||||||
|
userCurrentTimezone={userCurrentTimezone}
|
||||||
|
tomorrow9amTime={tomorrow9amTime}
|
||||||
|
nextMonday={nextMonday}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,10 @@ export function SendPostOptions({disabled, onSelect, channelId}: Props) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CoreMenuOptions handleOnSelect={handleOnSelect}/>
|
<CoreMenuOptions
|
||||||
|
handleOnSelect={handleOnSelect}
|
||||||
|
channelId={channelId}
|
||||||
|
/>
|
||||||
|
|
||||||
<Menu.Separator/>
|
<Menu.Separator/>
|
||||||
|
|
||||||
|
@ -0,0 +1,276 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {DateTime} from 'luxon';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
|
||||||
|
|
||||||
|
import {renderWithContext, fireEvent, screen} from 'tests/react_testing_utils';
|
||||||
|
import {scheduledPosts} from 'utils/constants';
|
||||||
|
|
||||||
|
import RecentUsedCustomDate from './recent_used_custom_date';
|
||||||
|
|
||||||
|
jest.mock('components/advanced_text_editor/use_post_box_indicator', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('components/menu', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
Item: jest.fn(({labels, trailingElements, children, ...props}) => (
|
||||||
|
<div {...props}>
|
||||||
|
{labels}
|
||||||
|
{children}
|
||||||
|
{trailingElements}
|
||||||
|
</div>
|
||||||
|
)),
|
||||||
|
Separator: jest.fn(() => <div className='menu-separator'/>),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
entities: {
|
||||||
|
preferences: {
|
||||||
|
myPreferences: {},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
currentUserId: 'currentUserId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentUsedCustomDateString = 'Recently used custom time';
|
||||||
|
|
||||||
|
describe('CoreMenuOptions Component', () => {
|
||||||
|
const userCurrentTimezone = 'America/New_York';
|
||||||
|
|
||||||
|
const handleOnSelect = jest.fn();
|
||||||
|
|
||||||
|
let now: DateTime;
|
||||||
|
let tomorrow9amTime: number;
|
||||||
|
let nextMonday: number;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
handleOnSelect.mockReset();
|
||||||
|
now = DateTime.fromISO('2024-11-01T10:00:00', {zone: userCurrentTimezone});
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(now.toJSDate());
|
||||||
|
|
||||||
|
// Hardcode `tomorrow9amTime` and `nextMonday`
|
||||||
|
tomorrow9amTime = DateTime.fromISO('2024-11-02T09:00:00', {zone: userCurrentTimezone}).toMillis();
|
||||||
|
nextMonday = DateTime.fromISO('2024-11-04T09:00:00', {zone: userCurrentTimezone}).toMillis();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createStateWithRecentlyUsedCustomDate(value: string) {
|
||||||
|
return {
|
||||||
|
...initialState,
|
||||||
|
entities: {
|
||||||
|
...initialState.entities,
|
||||||
|
preferences: {
|
||||||
|
...initialState.entities.preferences,
|
||||||
|
myPreferences: {
|
||||||
|
...initialState.entities.preferences.myPreferences,
|
||||||
|
[getPreferenceKey(scheduledPosts.SCHEDULED_POSTS, scheduledPosts.RECENTLY_USED_CUSTOM_TIME)]: {value},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComponent(state = initialState, handleOnSelectOverride = handleOnSelect) {
|
||||||
|
renderWithContext(
|
||||||
|
<RecentUsedCustomDate
|
||||||
|
handleOnSelect={handleOnSelectOverride}
|
||||||
|
userCurrentTimezone={userCurrentTimezone}
|
||||||
|
tomorrow9amTime={tomorrow9amTime}
|
||||||
|
nextMonday={nextMonday}
|
||||||
|
/>,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should render recently used custom time option when valid', () => {
|
||||||
|
const recentTimestamp = DateTime.now().plus({days: 7}).toMillis();
|
||||||
|
|
||||||
|
const recentlyUsedCustomDateVal = {
|
||||||
|
update_at: DateTime.now().toMillis(),
|
||||||
|
timestamp: recentTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = createStateWithRecentlyUsedCustomDate(JSON.stringify(recentlyUsedCustomDateVal));
|
||||||
|
|
||||||
|
renderComponent(state);
|
||||||
|
|
||||||
|
expect(screen.getByText(recentUsedCustomDateString)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render recently used custom time when preference value is invalid JSON', () => {
|
||||||
|
const invalidJson = '{ invalid JSON }';
|
||||||
|
|
||||||
|
const state = createStateWithRecentlyUsedCustomDate(invalidJson);
|
||||||
|
|
||||||
|
renderComponent(state);
|
||||||
|
|
||||||
|
expect(screen.queryByText(recentUsedCustomDateString)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call handleOnSelect with the correct timestamp when "Recently used custom time" is clicked', () => {
|
||||||
|
const recentTimestamp = DateTime.now().plus({days: 5}).toMillis();
|
||||||
|
|
||||||
|
const recentlyUsedCustomDateVal = {
|
||||||
|
update_at: DateTime.now().toMillis(),
|
||||||
|
timestamp: recentTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = createStateWithRecentlyUsedCustomDate(JSON.stringify(recentlyUsedCustomDateVal));
|
||||||
|
|
||||||
|
const handleOnSelectMock = jest.fn();
|
||||||
|
|
||||||
|
renderComponent(state, handleOnSelectMock);
|
||||||
|
|
||||||
|
const recentCustomOption = screen.getByText(recentUsedCustomDateString);
|
||||||
|
fireEvent.click(recentCustomOption);
|
||||||
|
|
||||||
|
expect(handleOnSelectMock).toHaveBeenCalledWith(expect.anything(), recentTimestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render recently used custom time when update_at is older than 30 days', () => {
|
||||||
|
const outdatedUpdateAt = DateTime.now().minus({days: 35}).toMillis();
|
||||||
|
const recentTimestamp = DateTime.now().plus({days: 5}).toMillis();
|
||||||
|
|
||||||
|
const recentlyUsedCustomDateVal = {
|
||||||
|
update_at: outdatedUpdateAt,
|
||||||
|
timestamp: recentTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = createStateWithRecentlyUsedCustomDate(JSON.stringify(recentlyUsedCustomDateVal));
|
||||||
|
|
||||||
|
renderComponent(state);
|
||||||
|
|
||||||
|
expect(screen.queryByText(recentUsedCustomDateString)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render recently used custom time when timestamp is in the past', () => {
|
||||||
|
const now = DateTime.now().setZone(userCurrentTimezone);
|
||||||
|
const nowMillis = now.toMillis();
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(now.toJSDate());
|
||||||
|
|
||||||
|
const pastTimestamp = now.minus({days: 1}).toMillis();
|
||||||
|
|
||||||
|
const recentlyUsedCustomDateVal = {
|
||||||
|
update_at: nowMillis,
|
||||||
|
timestamp: pastTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = createStateWithRecentlyUsedCustomDate(JSON.stringify(recentlyUsedCustomDateVal));
|
||||||
|
|
||||||
|
renderComponent(state);
|
||||||
|
|
||||||
|
expect(screen.queryByText(recentUsedCustomDateString)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render recently used custom time when timestamp equals tomorrow9amTime', () => {
|
||||||
|
const nowMillis = now.toMillis();
|
||||||
|
|
||||||
|
const recentlyUsedCustomDateVal = {
|
||||||
|
update_at: nowMillis,
|
||||||
|
timestamp: tomorrow9amTime, // Use the value from beforeEach
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = createStateWithRecentlyUsedCustomDate(JSON.stringify(recentlyUsedCustomDateVal));
|
||||||
|
|
||||||
|
renderComponent(state);
|
||||||
|
|
||||||
|
expect(screen.queryByText(recentUsedCustomDateString)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render recently used custom time when timestamp equals nextMonday', () => {
|
||||||
|
const nowMillis = now.toMillis();
|
||||||
|
|
||||||
|
const recentlyUsedCustomDateVal = {
|
||||||
|
update_at: nowMillis,
|
||||||
|
timestamp: nextMonday,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = createStateWithRecentlyUsedCustomDate(JSON.stringify(recentlyUsedCustomDateVal));
|
||||||
|
|
||||||
|
renderComponent(state);
|
||||||
|
|
||||||
|
expect(screen.queryByText(recentUsedCustomDateString)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render "Today at HH:MM AM/PM" when recently used custom date is TODAY', () => {
|
||||||
|
const now = DateTime.fromISO('2024-11-01T10:00:00', {zone: userCurrentTimezone});
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(now.toJSDate());
|
||||||
|
|
||||||
|
const recentTimestamp = now.plus({minutes: 5}).toMillis();
|
||||||
|
|
||||||
|
const recentlyUsedCustomDateVal = {
|
||||||
|
update_at: now.toMillis(),
|
||||||
|
timestamp: recentTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = createStateWithRecentlyUsedCustomDate(JSON.stringify(recentlyUsedCustomDateVal));
|
||||||
|
|
||||||
|
renderComponent(state);
|
||||||
|
|
||||||
|
expect(screen.getByText(recentUsedCustomDateString)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Today at/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render "Weekday at HH:MM AM/PM" if recent used custom date is in the SAME week', () => {
|
||||||
|
const now = DateTime.fromISO('2024-11-01T10:00:00', {zone: userCurrentTimezone});
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(now.toJSDate());
|
||||||
|
|
||||||
|
const recentTimestamp = now.plus({days: 2}).toMillis();
|
||||||
|
|
||||||
|
const recentlyUsedCustomDateVal = {
|
||||||
|
update_at: now.toMillis(),
|
||||||
|
timestamp: recentTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = createStateWithRecentlyUsedCustomDate(JSON.stringify(recentlyUsedCustomDateVal));
|
||||||
|
|
||||||
|
renderComponent(state);
|
||||||
|
|
||||||
|
expect(screen.getByText(recentUsedCustomDateString)).toBeInTheDocument();
|
||||||
|
|
||||||
|
const scheduledDate = DateTime.fromMillis(recentTimestamp).setZone(userCurrentTimezone);
|
||||||
|
const weekdayName = scheduledDate.toFormat('EEEE');
|
||||||
|
|
||||||
|
expect(screen.getByText(new RegExp(`${weekdayName} at`))).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render "Month Day at HH:MM AM/PM" if recent used custom date is NOT in the same week', () => {
|
||||||
|
const now = DateTime.fromISO('2024-11-01T10:00:00', {zone: userCurrentTimezone});
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(now.toJSDate());
|
||||||
|
|
||||||
|
const recentTimestamp = now.plus({days: 14}).toMillis();
|
||||||
|
|
||||||
|
const recentlyUsedCustomDateVal = {
|
||||||
|
update_at: now.toMillis(),
|
||||||
|
timestamp: recentTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = createStateWithRecentlyUsedCustomDate(JSON.stringify(recentlyUsedCustomDateVal));
|
||||||
|
|
||||||
|
renderComponent(state);
|
||||||
|
|
||||||
|
expect(screen.getByText(recentUsedCustomDateString)).toBeInTheDocument();
|
||||||
|
|
||||||
|
const scheduledDate = DateTime.fromMillis(recentTimestamp).setZone(userCurrentTimezone);
|
||||||
|
const monthDay = scheduledDate.toFormat('MMMM d');
|
||||||
|
|
||||||
|
expect(screen.getByText(new RegExp(`${monthDay} at`))).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,130 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import type {Zone} from 'luxon';
|
||||||
|
import {DateTime} from 'luxon';
|
||||||
|
import React, {memo, useCallback, useMemo} from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import type {GlobalState} from '@mattermost/types/store';
|
||||||
|
|
||||||
|
import {get as getPreference} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
import Timestamp, {RelativeRanges} from 'components/timestamp';
|
||||||
|
|
||||||
|
import {scheduledPosts} from 'utils/constants';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleOnSelect: (e: React.FormEvent, scheduledAt: number) => void;
|
||||||
|
userCurrentTimezone: string;
|
||||||
|
tomorrow9amTime: number;
|
||||||
|
nextMonday: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATE_RANGES = [
|
||||||
|
RelativeRanges.TODAY_TITLE_CASE,
|
||||||
|
RelativeRanges.TOMORROW_TITLE_CASE,
|
||||||
|
];
|
||||||
|
|
||||||
|
interface RecentlyUsedCustomDate {
|
||||||
|
update_at?: number;
|
||||||
|
timestamp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTimestampWithinLast30Days(timestamp: number, timeZone = 'UTC') {
|
||||||
|
if (!timestamp || isNaN(timestamp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const usedDate = DateTime.fromMillis(timestamp).setZone(timeZone);
|
||||||
|
const now = DateTime.now().setZone(timeZone);
|
||||||
|
const thirtyDaysAgo = now.minus({days: 30});
|
||||||
|
|
||||||
|
return usedDate >= thirtyDaysAgo && usedDate <= now;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowRecentlyUsedCustomTime(
|
||||||
|
nowMillis: number,
|
||||||
|
recentlyUsedCustomDateVal: RecentlyUsedCustomDate,
|
||||||
|
userCurrentTimezone: string,
|
||||||
|
tomorrow9amTime: number,
|
||||||
|
nextMonday: number,
|
||||||
|
) {
|
||||||
|
return recentlyUsedCustomDateVal &&
|
||||||
|
typeof recentlyUsedCustomDateVal.update_at === 'number' &&
|
||||||
|
typeof recentlyUsedCustomDateVal.timestamp === 'number' &&
|
||||||
|
recentlyUsedCustomDateVal.timestamp > nowMillis && // is in the future
|
||||||
|
recentlyUsedCustomDateVal.timestamp !== tomorrow9amTime && // is not the existing option tomorrow 9a.m
|
||||||
|
recentlyUsedCustomDateVal.timestamp !== nextMonday && // is not the existing option tomorrow 9a.m
|
||||||
|
isTimestampWithinLast30Days(recentlyUsedCustomDateVal.update_at, userCurrentTimezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
const USE_DATE_WEEKDAY_LONG = {weekday: 'long'} as const;
|
||||||
|
const USE_TIME_HOUR_MINUTE_NUMERIC = {hour: 'numeric', minute: 'numeric'} as const;
|
||||||
|
const USE_DATE_MONTH_DAY = {month: 'long', day: 'numeric'} as const;
|
||||||
|
|
||||||
|
function getDateOption(now: DateTime, timestamp: number | undefined, userCurrentTimezone: string | Zone | undefined) {
|
||||||
|
if (!now || !timestamp || !userCurrentTimezone) {
|
||||||
|
return USE_DATE_WEEKDAY_LONG;
|
||||||
|
}
|
||||||
|
const scheduledDate = DateTime.fromMillis(timestamp).setZone(userCurrentTimezone);
|
||||||
|
const isInCurrentWeek = scheduledDate.weekNumber === now.weekNumber && scheduledDate.weekYear === now.weekYear;
|
||||||
|
return isInCurrentWeek ? USE_DATE_WEEKDAY_LONG : USE_DATE_MONTH_DAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RecentUsedCustomDate({handleOnSelect, userCurrentTimezone, nextMonday, tomorrow9amTime}: Props) {
|
||||||
|
const now = DateTime.now().setZone(userCurrentTimezone);
|
||||||
|
const recentlyUsedCustomDate = useSelector((state: GlobalState) => getPreference(state, scheduledPosts.SCHEDULED_POSTS, scheduledPosts.RECENTLY_USED_CUSTOM_TIME));
|
||||||
|
const recentlyUsedCustomDateVal: RecentlyUsedCustomDate = useMemo(() => {
|
||||||
|
if (recentlyUsedCustomDate) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(recentlyUsedCustomDate) as RecentlyUsedCustomDate;
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}, [recentlyUsedCustomDate]);
|
||||||
|
const handleRecentlyUsedCustomTime = useCallback((e) => handleOnSelect(e, recentlyUsedCustomDateVal.timestamp!), [handleOnSelect, recentlyUsedCustomDateVal.timestamp]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!shouldShowRecentlyUsedCustomTime(now.toMillis(), recentlyUsedCustomDateVal, userCurrentTimezone, tomorrow9amTime, nextMonday)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateOption = getDateOption(now, recentlyUsedCustomDateVal.timestamp, userCurrentTimezone);
|
||||||
|
|
||||||
|
const timestamp = (
|
||||||
|
<Timestamp
|
||||||
|
ranges={DATE_RANGES}
|
||||||
|
value={recentlyUsedCustomDateVal.timestamp}
|
||||||
|
timeZone={userCurrentTimezone}
|
||||||
|
useDate={dateOption}
|
||||||
|
useTime={USE_TIME_HOUR_MINUTE_NUMERIC}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const trailingElement = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='create_post_button.option.schedule_message.options.recently_used_custom_time'
|
||||||
|
defaultMessage='Recently used custom time'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu.Separator key='recent_custom_separator'/>
|
||||||
|
<Menu.Item
|
||||||
|
key='recently_used_custom_time'
|
||||||
|
onClick={handleRecentlyUsedCustomTime}
|
||||||
|
labels={timestamp}
|
||||||
|
className='core-menu-options'
|
||||||
|
trailingElements={trailingElement}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(RecentUsedCustomDate);
|
@ -6,4 +6,16 @@ ul#dropdown_send_post_options {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li.core-menu-options{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.trailing-elements {
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-left: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {DateTime} from 'luxon';
|
||||||
|
import {useState, useEffect, useMemo} from 'react';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {getDirectChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
import {isScheduledPostsEnabled} from 'mattermost-redux/selectors/entities/scheduled_posts';
|
||||||
|
import {getTimezoneForUserProfile, getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||||
|
import {getStatusForUserId, getUser, makeGetDisplayName} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
|
import Constants, {UserStatuses} from 'utils/constants';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
|
const DEFAULT_TIMEZONE = {
|
||||||
|
useAutomaticTimezone: true,
|
||||||
|
automaticTimezone: 'UTC',
|
||||||
|
manualTimezone: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MINUTE = 1000 * 60;
|
||||||
|
|
||||||
|
function useTimePostBoxIndicator(channelId: string) {
|
||||||
|
const getDisplayName = useMemo(makeGetDisplayName, []);
|
||||||
|
|
||||||
|
const teammateId = useSelector((state: GlobalState) => getDirectChannel(state, channelId)?.teammate_id || '');
|
||||||
|
const teammateDisplayName = useSelector((state: GlobalState) => (teammateId ? getDisplayName(state, teammateId) : ''));
|
||||||
|
|
||||||
|
const isDM = useSelector(
|
||||||
|
(state: GlobalState) => Boolean(getDirectChannel(state, channelId)?.teammate_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the teammate is in DND status
|
||||||
|
const isTeammateDND = useSelector((state: GlobalState) =>
|
||||||
|
(teammateId ? getStatusForUserId(state, teammateId) === UserStatuses.DND : false),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine if the DND warning should be shown
|
||||||
|
const showDndWarning = isTeammateDND && isDM;
|
||||||
|
|
||||||
|
const [timestamp, setTimestamp] = useState(0);
|
||||||
|
const [showIt, setShowIt] = useState(false);
|
||||||
|
|
||||||
|
// get teammate timezone information
|
||||||
|
const teammateTimezone = useSelector(
|
||||||
|
(state: GlobalState) => {
|
||||||
|
if (!teammateId) {
|
||||||
|
return DEFAULT_TIMEZONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const teammate = getUser(state, teammateId);
|
||||||
|
return getTimezoneForUserProfile(teammate);
|
||||||
|
},
|
||||||
|
(a, b) =>
|
||||||
|
a.automaticTimezone === b.automaticTimezone &&
|
||||||
|
a.manualTimezone === b.manualTimezone &&
|
||||||
|
a.useAutomaticTimezone === b.useAutomaticTimezone,
|
||||||
|
);
|
||||||
|
|
||||||
|
// current user timezone
|
||||||
|
const userCurrentTimezone = useSelector((state: GlobalState) => getCurrentTimezone(state));
|
||||||
|
|
||||||
|
// UseEffect to update the timestamp and the visibility for the time indicator
|
||||||
|
useEffect(() => {
|
||||||
|
function updateTime() {
|
||||||
|
const timezone =
|
||||||
|
teammateTimezone.useAutomaticTimezone ? teammateTimezone.automaticTimezone : teammateTimezone.manualTimezone || 'UTC';
|
||||||
|
|
||||||
|
const teammateUserDate = DateTime.local().setZone(timezone);
|
||||||
|
|
||||||
|
setTimestamp(teammateUserDate.toMillis());
|
||||||
|
|
||||||
|
const currentHour = teammateUserDate.hour;
|
||||||
|
const showIndicator =
|
||||||
|
currentHour >= Constants.REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY ||
|
||||||
|
currentHour < Constants.REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY;
|
||||||
|
|
||||||
|
setShowIt(showIndicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTime();
|
||||||
|
|
||||||
|
const interval = setInterval(updateTime, MINUTE);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [
|
||||||
|
teammateTimezone.useAutomaticTimezone,
|
||||||
|
teammateTimezone.automaticTimezone,
|
||||||
|
teammateTimezone.manualTimezone,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isScheduledPostEnabledValue = useSelector(isScheduledPostsEnabled);
|
||||||
|
|
||||||
|
const showRemoteUserHour = isDM && showIt && timestamp !== 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
showRemoteUserHour,
|
||||||
|
isDM,
|
||||||
|
currentUserTimesStamp: timestamp,
|
||||||
|
teammateTimezone,
|
||||||
|
userCurrentTimezone,
|
||||||
|
isScheduledPostEnabled: isScheduledPostEnabledValue,
|
||||||
|
showDndWarning,
|
||||||
|
teammateId,
|
||||||
|
teammateDisplayName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useTimePostBoxIndicator;
|
@ -3455,6 +3455,8 @@
|
|||||||
"create_post_button.option.schedule_message.options.header": "Schedule message",
|
"create_post_button.option.schedule_message.options.header": "Schedule message",
|
||||||
"create_post_button.option.schedule_message.options.monday": "Monday at {9amTime}",
|
"create_post_button.option.schedule_message.options.monday": "Monday at {9amTime}",
|
||||||
"create_post_button.option.schedule_message.options.next_monday": "Next Monday at {9amTime}",
|
"create_post_button.option.schedule_message.options.next_monday": "Next Monday at {9amTime}",
|
||||||
|
"create_post_button.option.schedule_message.options.recently_used_custom_time": "Recently used custom time",
|
||||||
|
"create_post_button.option.schedule_message.options.teammate_user_hour": "{time} {user}’s time",
|
||||||
"create_post_button.option.schedule_message.options.tomorrow": "Tomorrow at {9amTime}",
|
"create_post_button.option.schedule_message.options.tomorrow": "Tomorrow at {9amTime}",
|
||||||
"create_post_button.option.send_now": "Send Now",
|
"create_post_button.option.send_now": "Send Now",
|
||||||
"create_post.dm_or_gm_remote": "Direct Messages and Group Messages with remote users are not supported.",
|
"create_post.dm_or_gm_remote": "Direct Messages and Group Messages with remote users are not supported.",
|
||||||
|
@ -10,7 +10,10 @@ import {getTimezoneLabel, getUserCurrentTimezone} from 'mattermost-redux/utils/t
|
|||||||
|
|
||||||
import {getCurrentUser} from './common';
|
import {getCurrentUser} from './common';
|
||||||
|
|
||||||
export function getTimezoneForUserProfile(profile: UserProfile) {
|
export const getTimezoneForUserProfile = createSelector(
|
||||||
|
'getTimezoneForUserProfile',
|
||||||
|
(profile: UserProfile) => profile,
|
||||||
|
(profile) => {
|
||||||
if (profile && profile.timezone) {
|
if (profile && profile.timezone) {
|
||||||
return {
|
return {
|
||||||
...profile.timezone,
|
...profile.timezone,
|
||||||
@ -23,7 +26,8 @@ export function getTimezoneForUserProfile(profile: UserProfile) {
|
|||||||
automaticTimezone: '',
|
automaticTimezone: '',
|
||||||
manualTimezone: '',
|
manualTimezone: '',
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const getCurrentTimezoneFull = createSelector(
|
export const getCurrentTimezoneFull = createSelector(
|
||||||
'getCurrentTimezoneFull',
|
'getCurrentTimezoneFull',
|
||||||
|
@ -2223,5 +2223,10 @@ export const PageLoadContext = {
|
|||||||
|
|
||||||
export const SCHEDULED_POST_URL_SUFFIX = 'scheduled_posts';
|
export const SCHEDULED_POST_URL_SUFFIX = 'scheduled_posts';
|
||||||
|
|
||||||
|
export const scheduledPosts = {
|
||||||
|
RECENTLY_USED_CUSTOM_TIME: 'recently_used_custom_time',
|
||||||
|
SCHEDULED_POSTS: 'scheduled_posts',
|
||||||
|
};
|
||||||
|
|
||||||
export default Constants;
|
export default Constants;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user