Skip to content
This repository was archived by the owner on Mar 13, 2024. It is now read-only.

Commit af9628f

Browse files
add post reminder
Co-Authored-By: Ashish Bhate <[email protected]>
1 parent 329a179 commit af9628f

19 files changed

+703
-71
lines changed

components/dot_menu/dot_menu.scss

+7
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@
1818
}
1919
}
2020
}
21+
22+
.postReminderMenuHeader {
23+
padding: 6px 20px;
24+
margin: 0;
25+
font-weight: bold;
26+
pointer-events: none;
27+
}

components/dot_menu/dot_menu.test.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
import React from 'react';
55

6-
import {PostType} from '@mattermost/types/posts';
7-
86
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
97

108
import {Locations} from 'utils/constants';
119
import {TestHelper} from 'utils/test_helper';
1210

11+
import {PostType} from '@mattermost/types/posts';
12+
1313
import * as dotUtils from './utils';
1414
jest.mock('./utils');
1515

@@ -36,6 +36,7 @@ describe('components/dot_menu/DotMenu', () => {
3636
markPostAsUnread: jest.fn(),
3737
postEphemeralCallResponseForPost: jest.fn(),
3838
setThreadFollow: jest.fn(),
39+
addPostReminder: jest.fn(),
3940
setGlobalItem: jest.fn(),
4041
},
4142
canEdit: false,
@@ -48,6 +49,7 @@ describe('components/dot_menu/DotMenu', () => {
4849
threadId: 'post_id_1',
4950
threadReplyCount: 0,
5051
userId: 'user_id_1',
52+
isMilitaryTime: false,
5153
showForwardPostNewLabel: false,
5254
};
5355

components/dot_menu/dot_menu.tsx

+24-13
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import Badge from '../widgets/badges/badge';
4343
import {ChangeEvent, trackDotMenuEvent} from './utils';
4444

4545
import './dot_menu.scss';
46+
import {PostReminderSubmenu} from './post_reminder_submenu';
4647

4748
type ShortcutKeyProps = {
4849
shortcutKey: string;
@@ -75,6 +76,8 @@ type Props = {
7576
teamUrl?: string; // TechDebt: Made non-mandatory while converting to typescript
7677
isMobileView: boolean;
7778
showForwardPostNewLabel: boolean;
79+
timezone?: string;
80+
isMilitaryTime: boolean;
7881

7982
/**
8083
* Components for overriding provided by plugins
@@ -203,7 +206,18 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
203206
this.disableCanEditPostByTime();
204207
}
205208

209+
componentDidUpdate(prevProps: Props): void {
210+
if (!prevProps.isMenuOpen && this.props.isMenuOpen) {
211+
window.addEventListener('keydown', this.onShortcutKeyDown);
212+
}
213+
214+
if (prevProps.isMenuOpen && !this.props.isMenuOpen) {
215+
window.removeEventListener('keydown', this.onShortcutKeyDown);
216+
}
217+
}
218+
206219
componentWillUnmount(): void {
220+
window.removeEventListener('keydown', this.onShortcutKeyDown);
207221
this.editDisableAction.cancel();
208222
}
209223

@@ -338,11 +352,11 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
338352
this.props.handleCommentClick?.(e);
339353
}
340354

341-
isKeyboardEvent = (e: React.KeyboardEvent): any => {
355+
isKeyboardEvent = (e: KeyboardEvent): any => {
342356
return (e).getModifierState !== undefined;
343357
}
344358

345-
onShortcutKeyDown = (e: React.KeyboardEvent): void => {
359+
onShortcutKeyDown = (e: KeyboardEvent): void => {
346360
e.preventDefault();
347361
if (!this.isKeyboardEvent(e)) {
348362
return;
@@ -556,7 +570,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
556570
leadingElement={<ReplyOutlineIcon size={18}/>}
557571
trailingElements={<ShortcutKey shortcutKey='R'/>}
558572
onClick={this.handleCommentClick}
559-
onKeyDown={this.onShortcutKeyDown}
560573
/>
561574
}
562575
{this.canPostBeForwarded &&
@@ -566,7 +579,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
566579
leadingElement={<ArrowRightBoldOutlineIcon size={18}/>}
567580
trailingElements={<ShortcutKey shortcutKey='Shift + F'/>}
568581
onClick={this.handleForwardMenuItemActivated}
569-
onKeyDown={this.onShortcutKeyDown}
570582
/>
571583
}
572584
<ChannelPermissionGate
@@ -585,7 +597,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
585597
}
586598
leadingElement={<EmoticonPlusOutlineIcon size={18}/>}
587599
onClick={this.handleAddReactionMenuItemActivated}
588-
onKeyDown={this.onShortcutKeyDown}
589600
/>
590601
}
591602
</ChannelPermissionGate>
@@ -604,7 +615,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
604615
labels={followPostLabel()}
605616
leadingElement={isFollowingThread ? <MessageMinusOutlineIcon size={18}/> : <MessageCheckOutlineIcon size={18}/>}
606617
onClick={this.handleSetThreadFollow}
607-
onKeyDown={this.onShortcutKeyDown}
608618
/>
609619
}
610620
{Boolean(!isSystemMessage && !this.props.channelIsArchived && this.props.location !== Locations.SEARCH) &&
@@ -619,7 +629,14 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
619629
leadingElement={<MarkAsUnreadIcon size={18}/>}
620630
trailingElements={<ShortcutKey shortcutKey='U'/>}
621631
onClick={this.handleMarkPostAsUnread}
622-
onKeyDown={this.onShortcutKeyDown}
632+
/>
633+
}
634+
{!isSystemMessage &&
635+
<PostReminderSubmenu
636+
userId={this.props.userId}
637+
post={this.props.post}
638+
isMilitaryTime={this.props.isMilitaryTime}
639+
timezone={this.props.timezone}
623640
/>
624641
}
625642
{!isSystemMessage &&
@@ -629,7 +646,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
629646
leadingElement={this.props.isFlagged ? <BookmarkIcon size={18}/> : <BookmarkOutlineIcon size={18}/>}
630647
trailingElements={<ShortcutKey shortcutKey='S'/>}
631648
onClick={this.handleFlagMenuItemActivated}
632-
onKeyDown={this.onShortcutKeyDown}
633649
/>
634650
}
635651
{Boolean(!isSystemMessage && !this.props.isReadOnly) &&
@@ -639,7 +655,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
639655
leadingElement={this.props.post.is_pinned ? <PinIcon size={18}/> : <PinOutlineIcon size={18}/>}
640656
trailingElements={<ShortcutKey shortcutKey='P'/>}
641657
onClick={this.handlePinMenuItemActivated}
642-
onKeyDown={this.onShortcutKeyDown}
643658
/>
644659
}
645660
{!isSystemMessage && (this.state.canEdit || this.state.canDelete) && <Menu.Separator/>}
@@ -654,7 +669,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
654669
leadingElement={<LinkVariantIcon size={18}/>}
655670
trailingElements={<ShortcutKey shortcutKey='K'/>}
656671
onClick={this.copyLink}
657-
onKeyDown={this.onShortcutKeyDown}
658672
/>
659673
}
660674
{!isSystemMessage && <Menu.Separator/>}
@@ -669,7 +683,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
669683
leadingElement={<PencilOutlineIcon size={18}/>}
670684
trailingElements={<ShortcutKey shortcutKey='E'/>}
671685
onClick={this.handleEditMenuItemActivated}
672-
onKeyDown={this.onShortcutKeyDown}
673686
/>
674687
}
675688
{!isSystemMessage &&
@@ -683,7 +696,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
683696
leadingElement={<ContentCopyIcon size={18}/>}
684697
trailingElements={<ShortcutKey shortcutKey='C'/>}
685698
onClick={this.copyText}
686-
onKeyDown={this.onShortcutKeyDown}
687699
/>
688700
}
689701
{this.state.canDelete &&
@@ -698,7 +710,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
698710
/>}
699711
onClick={this.handleDeleteMenuItemActivated}
700712
isDestructive={true}
701-
onKeyDown={this.onShortcutKeyDown}
702713
/>
703714
}
704715
</Menu.Container>

components/dot_menu/dot_menu_empty.test.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ describe('components/dot_menu/DotMenu returning empty ("")', () => {
4444
handleBindingClick: jest.fn(),
4545
postEphemeralCallResponseForPost: jest.fn(),
4646
setThreadFollow: jest.fn(),
47+
addPostReminder: jest.fn(),
4748
setGlobalItem: jest.fn(),
4849
},
4950
canEdit: false,
@@ -57,6 +58,7 @@ describe('components/dot_menu/DotMenu returning empty ("")', () => {
5758
teamId: '',
5859
threadId: 'post_id_1',
5960
userId: 'user_id_1',
61+
isMilitaryTime: false,
6062
showForwardPostNewLabel: false,
6163
};
6264

components/dot_menu/dot_menu_mobile.test.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ describe('components/dot_menu/DotMenu on mobile view', () => {
4444
handleBindingClick: jest.fn(),
4545
postEphemeralCallResponseForPost: jest.fn(),
4646
setThreadFollow: jest.fn(),
47+
addPostReminder: jest.fn(),
4748
setGlobalItem: jest.fn(),
4849
},
4950
canEdit: false,
@@ -57,6 +58,7 @@ describe('components/dot_menu/DotMenu on mobile view', () => {
5758
teamId: '',
5859
threadId: 'post_id_1',
5960
userId: 'user_id_1',
61+
isMilitaryTime: false,
6062
showForwardPostNewLabel: false,
6163
};
6264

components/dot_menu/index.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,11 @@ import {getCurrentUserId, getCurrentUserMentionKeys} from 'mattermost-redux/sele
1111
import {getCurrentTeamId, getCurrentTeam, getTeam} from 'mattermost-redux/selectors/entities/teams';
1212
import {makeGetThreadOrSynthetic} from 'mattermost-redux/selectors/entities/threads';
1313
import {getPost} from 'mattermost-redux/selectors/entities/posts';
14-
import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
15-
14+
import {getBool, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
15+
import {getCurrentUserTimezone} from 'selectors/general';
1616
import {isSystemMessage} from 'mattermost-redux/utils/post_utils';
17-
1817
import {GenericAction} from 'mattermost-redux/types/actions';
19-
2018
import {setThreadFollow} from 'mattermost-redux/actions/threads';
21-
2219
import {ModalData} from 'types/actions';
2320
import {GlobalState} from 'types/store';
2421

@@ -74,6 +71,7 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
7471
const currentTeam = getCurrentTeam(state) || {};
7572
const team = getTeam(state, channel.team_id);
7673
const teamUrl = `${getSiteURL()}/${team?.name || currentTeam.name}`;
74+
const isMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false);
7775

7876
const systemMessage = isSystemMessage(post);
7977
const collapsedThreads = isCollapsedThreadsEnabled(state);
@@ -130,6 +128,8 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
130128
threadReplyCount,
131129
isMobileView: getIsMobileView(state),
132130
showForwardPostNewLabel,
131+
timezone: getCurrentUserTimezone(state),
132+
isMilitaryTime,
133133
...ownProps,
134134
};
135135
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2+
// See LICENSE.txt for license information.
3+
4+
import React from 'react';
5+
import {useDispatch} from 'react-redux';
6+
7+
import {FormattedMessage, FormattedDate, FormattedTime, useIntl} from 'react-intl';
8+
import {ChevronRightIcon, ClockOutlineIcon} from '@mattermost/compass-icons/components';
9+
10+
import * as Menu from 'components/menu';
11+
import {getCurrentMomentForTimezone} from 'utils/timezone';
12+
import {openModal} from 'actions/views/modals';
13+
import {ModalIdentifiers} from 'utils/constants';
14+
import {toUTCUnix} from 'utils/datetime';
15+
import PostReminderCustomTimePicker from 'components/post_reminder_time_picker_modal';
16+
import {addPostReminder} from 'mattermost-redux/actions/posts';
17+
18+
import {Post} from '@mattermost/types/posts';
19+
20+
type Props = {
21+
userId: string;
22+
post: Post;
23+
isMilitaryTime: boolean;
24+
timezone?: string;
25+
}
26+
27+
const postReminderTimes = [
28+
{id: 'thirty_minutes', label: 'post_info.post_reminder.sub_menu.thirty_minutes', labelDefault: '30 mins'},
29+
{id: 'one_hour', label: 'post_info.post_reminder.sub_menu.one_hour', labelDefault: '1 hour'},
30+
{id: 'two_hours', label: 'post_info.post_reminder.sub_menu.two_hours', labelDefault: '2 hours'},
31+
{id: 'tomorrow', label: 'post_info.post_reminder.sub_menu.tomorrow', labelDefault: 'Tomorrow'},
32+
{id: 'custom', label: 'post_info.post_reminder.sub_menu.custom', labelDefault: 'Custom'},
33+
];
34+
35+
export function PostReminderSubmenu(props: Props) {
36+
const {formatMessage} = useIntl();
37+
const dispatch = useDispatch();
38+
39+
const setPostReminder = (id: string): void => {
40+
const currentDate = getCurrentMomentForTimezone(props.timezone);
41+
let endTime = currentDate;
42+
switch (id) {
43+
case 'thirty_minutes':
44+
// add 30 minutes in current time
45+
endTime = currentDate.add(30, 'minutes');
46+
break;
47+
case 'one_hour':
48+
// add 1 hour in current time
49+
endTime = currentDate.add(1, 'hour');
50+
break;
51+
case 'two_hours':
52+
// add 2 hours in current time
53+
endTime = currentDate.add(2, 'hours');
54+
break;
55+
case 'tomorrow':
56+
// add one day in current date
57+
endTime = currentDate.add(1, 'day');
58+
break;
59+
}
60+
61+
dispatch(addPostReminder(props.userId, props.post.id, toUTCUnix(endTime.toDate())));
62+
};
63+
64+
const setCustomPostReminder = (): void => {
65+
const postReminderCustomTimePicker = {
66+
modalId: ModalIdentifiers.POST_REMINDER_CUSTOM_TIME_PICKER,
67+
dialogType: PostReminderCustomTimePicker,
68+
dialogProps: {
69+
postId: props.post.id,
70+
currentDate: new Date(),
71+
},
72+
};
73+
dispatch(openModal(postReminderCustomTimePicker));
74+
};
75+
76+
const postReminderSubMenuItems =
77+
postReminderTimes.map(({id, label, labelDefault}) => {
78+
const labels = (
79+
<FormattedMessage
80+
id={label}
81+
defaultMessage={labelDefault}
82+
/>
83+
);
84+
let trailing: JSX.Element | null = null;
85+
86+
if (id === 'tomorrow') {
87+
const tomorrow = getCurrentMomentForTimezone(props.timezone).add(1, 'day').toDate();
88+
trailing = (
89+
<span className={`postReminder-${id}_timestamp`}>
90+
<FormattedDate
91+
value={tomorrow}
92+
weekday='short'
93+
/>
94+
{', '}
95+
<FormattedTime
96+
value={tomorrow}
97+
timeStyle='short'
98+
hour12={!props.isMilitaryTime}
99+
/>
100+
</span>
101+
);
102+
}
103+
return (
104+
<Menu.Item
105+
key={`remind_post_options_${id}`}
106+
id={`remind_post_options_${id}`}
107+
labels={labels}
108+
trailingElements={trailing}
109+
onClick={id === 'custom' ? () => setCustomPostReminder() : () => setPostReminder(id)}
110+
/>
111+
);
112+
});
113+
114+
return (
115+
<Menu.SubMenu
116+
id={`remind_post_${props.post.id}`}
117+
labels={
118+
<FormattedMessage
119+
id='post_info.post_reminder.menu'
120+
defaultMessage='Remind'
121+
/>
122+
}
123+
leadingElement={<ClockOutlineIcon size={18}/>}
124+
trailingElements={<ChevronRightIcon size={16}/>}
125+
menuId={`remind_post_${props.post.id}-menu`}
126+
>
127+
<h5 className={'postReminderMenuHeader'}>
128+
{formatMessage(
129+
{id: 'post_info.post_reminder.sub_menu.header',
130+
defaultMessage: 'Set a reminder for:'},
131+
)}
132+
</h5>
133+
{postReminderSubMenuItems}
134+
</Menu.SubMenu>
135+
);
136+
}

0 commit comments

Comments
 (0)