@mentions notifications
This commit is contained in:
parent
3bbe859452
commit
6b290a0f0c
29 changed files with 627 additions and 51 deletions
|
@ -3451,6 +3451,10 @@
|
||||||
"message": "Mute notifications",
|
"message": "Mute notifications",
|
||||||
"description": "Label for the mute notifications drop-down selector"
|
"description": "Label for the mute notifications drop-down selector"
|
||||||
},
|
},
|
||||||
|
"notMuted": {
|
||||||
|
"message": "Not muted",
|
||||||
|
"description": "Label when the conversation is not muted"
|
||||||
|
},
|
||||||
"muteHour": {
|
"muteHour": {
|
||||||
"message": "Mute for one hour",
|
"message": "Mute for one hour",
|
||||||
"description": "Label for muting the conversation"
|
"description": "Label for muting the conversation"
|
||||||
|
@ -4964,6 +4968,10 @@
|
||||||
"message": "When enabled, messages sent and received in this group will disappear after they've been seen.",
|
"message": "When enabled, messages sent and received in this group will disappear after they've been seen.",
|
||||||
"description": "This is the info about the disappearing messages setting"
|
"description": "This is the info about the disappearing messages setting"
|
||||||
},
|
},
|
||||||
|
"ConversationDetails--notifications": {
|
||||||
|
"message": "Notifications",
|
||||||
|
"description": "This is the label for notifications in the conversation details screen"
|
||||||
|
},
|
||||||
"ConversationDetails--group-info-label": {
|
"ConversationDetails--group-info-label": {
|
||||||
"message": "Who can edit group info",
|
"message": "Who can edit group info",
|
||||||
"description": "This is the label for the 'who can edit the group' panel"
|
"description": "This is the label for the 'who can edit the group' panel"
|
||||||
|
@ -5070,6 +5078,22 @@
|
||||||
"message": "See all",
|
"message": "See all",
|
||||||
"description": "This is a button on the conversation details to show all members"
|
"description": "This is a button on the conversation details to show all members"
|
||||||
},
|
},
|
||||||
|
"ConversationNotificationsSettings__mentions__label": {
|
||||||
|
"message": "Mentions",
|
||||||
|
"description": "In the conversation notifications settings, this is the label for the mentions option"
|
||||||
|
},
|
||||||
|
"ConversationNotificationsSettings__mentions__info": {
|
||||||
|
"message": "Receive notifications when you're mentioned in muted chats",
|
||||||
|
"description": "In the conversation notifications settings, this is the sub-label for the mentions option"
|
||||||
|
},
|
||||||
|
"ConversationNotificationsSettings__mentions__select__always-notify": {
|
||||||
|
"message": "Always notify",
|
||||||
|
"description": "In the conversation notifications settings, this is the option that always notifies you for @mentions"
|
||||||
|
},
|
||||||
|
"ConversationNotificationsSettings__mentions__select__dont-notify-for-mentions-if-muted": {
|
||||||
|
"message": "Don't notify if muted",
|
||||||
|
"description": "In the conversation notifications settings, this is the option that doesn't notify you for @mentions if the conversation is muted"
|
||||||
|
},
|
||||||
"GroupLinkManagement--clipboard": {
|
"GroupLinkManagement--clipboard": {
|
||||||
"message": "Group link copied.",
|
"message": "Group link copied.",
|
||||||
"description": "Shown in a toast when a user selects to copy group link"
|
"description": "Shown in a toast when a user selects to copy group link"
|
||||||
|
|
1
images/icons/v2/at-24.svg
Normal file
1
images/icons/v2/at-24.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m15.37 6.36a.87.87 0 0 0 -.87.87v.87a4.1 4.1 0 0 0 -3.24-1.74c-2.74 0-5 2.53-5 5.64s2.24 5.64 5 5.64a4.19 4.19 0 0 0 3.56-2.21 3.4 3.4 0 0 0 3.46 2.21c2.45 0 4.23-2.5 4.23-5.94 0-6.2-4.42-10.7-10.51-10.7a11 11 0 1 0 5.93 20.26.87.87 0 0 0 .27-1.2.87.87 0 0 0 -1.2-.27 9.25 9.25 0 1 1 -5-17c5.16 0 8.76 3.68 8.76 9 0 2.39-1.07 4.19-2.48 4.19-1 0-2-.27-2-2.4v-6.35a.88.88 0 0 0 -.91-.87zm-4.11 9.53c-1.78 0-3.26-1.74-3.26-3.89s1.45-3.89 3.23-3.89 3.27 1.74 3.27 3.89-1.45 3.89-3.24 3.89z"/></svg>
|
After Width: | Height: | Size: 563 B |
1
images/icons/v2/bell-disabled-outline-24.svg
Normal file
1
images/icons/v2/bell-disabled-outline-24.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m21.057 4.1-.957-1.156-2.786 2.318a5.981 5.981 0 0 0 -11.176 1.465l-1.427 6.843a7.284 7.284 0 0 1 -1.749 3.651l-2.019 1.679.957 1.156zm-14.877 9.776 1.42-6.832a4.5 4.5 0 0 1 8.533-.8l-10.323 8.605a3.552 3.552 0 0 0 .37-.973zm15.82 3.624a1.5 1.5 0 0 1 -1.5 1.5h-14.983l1.8-1.5h13.175a5.511 5.511 0 0 1 -2.664-3.606l-.915-4.387 1.306-1.088 1.074 5.151a4.033 4.033 0 0 0 1.975 2.646 1.486 1.486 0 0 1 .732 1.284zm-12.45 3h4.9a2.5 2.5 0 0 1 -4.9 0z"/></svg>
|
After Width: | Height: | Size: 545 B |
1
images/icons/v2/bell-disabled-solid-24.svg
Normal file
1
images/icons/v2/bell-disabled-solid-24.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m1.9 20.056-.957-1.156 2.122-1.769a6.1 6.1 0 0 0 1.646-3.561l1.427-6.843a5.981 5.981 0 0 1 11.176-1.465l2.786-2.318.957 1.156zm10.1 2.444a2.5 2.5 0 0 0 2.45-2h-4.9a2.5 2.5 0 0 0 2.45 2zm9.264-6.284a4.033 4.033 0 0 1 -1.975-2.646l-1.074-5.151-12.698 10.581h14.983a1.5 1.5 0 0 0 .764-2.784z"/></svg>
|
After Width: | Height: | Size: 389 B |
1
images/icons/v2/sound-outline-24.svg
Normal file
1
images/icons/v2/sound-outline-24.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m19.778 19.778-1.06-1.06a9.512 9.512 0 0 0 0-13.436l1.06-1.06a11.012 11.012 0 0 1 0 15.556zm-.778-7.778a6.957 6.957 0 0 0 -2.051-4.95l-1.06 1.061a5.5 5.5 0 0 1 0 7.778l1.06 1.06a6.953 6.953 0 0 0 2.051-4.949zm-5-8.863v17.726a.5.5 0 0 1 -.5.5.494.494 0 0 1 -.335-.132l-5.165-4.731h-4a2 2 0 0 1 -2-2v-5a2 2 0 0 1 2-2h4l5.162-4.732a.494.494 0 0 1 .335-.132.5.5 0 0 1 .503.501zm-1.5 13.363v-9l.25-2.75-1.41 1.723-2.757 2.527h-4.583a.5.5 0 0 0 -.5.5v5a.5.5 0 0 0 .5.5h4.583l2.757 2.527 1.41 1.723z"/></svg>
|
After Width: | Height: | Size: 593 B |
|
@ -94,6 +94,9 @@ const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
||||||
const {
|
const {
|
||||||
createMessageDetail,
|
createMessageDetail,
|
||||||
} = require('../../ts/state/roots/createMessageDetail');
|
} = require('../../ts/state/roots/createMessageDetail');
|
||||||
|
const {
|
||||||
|
createConversationNotificationsSettings,
|
||||||
|
} = require('../../ts/state/roots/createConversationNotificationsSettings');
|
||||||
const {
|
const {
|
||||||
createGroupV2Permissions,
|
createGroupV2Permissions,
|
||||||
} = require('../../ts/state/roots/createGroupV2Permissions');
|
} = require('../../ts/state/roots/createGroupV2Permissions');
|
||||||
|
@ -363,6 +366,7 @@ exports.setup = (options = {}) => {
|
||||||
createGroupV2Permissions,
|
createGroupV2Permissions,
|
||||||
createLeftPane,
|
createLeftPane,
|
||||||
createMessageDetail,
|
createMessageDetail,
|
||||||
|
createConversationNotificationsSettings,
|
||||||
createPendingInvites,
|
createPendingInvites,
|
||||||
createSafetyNumberViewer,
|
createSafetyNumberViewer,
|
||||||
createShortcutGuideModal,
|
createShortcutGuideModal,
|
||||||
|
|
|
@ -90,12 +90,13 @@ message GroupV1Record {
|
||||||
}
|
}
|
||||||
|
|
||||||
message GroupV2Record {
|
message GroupV2Record {
|
||||||
optional bytes masterKey = 1;
|
optional bytes masterKey = 1;
|
||||||
optional bool blocked = 2;
|
optional bool blocked = 2;
|
||||||
optional bool whitelisted = 3;
|
optional bool whitelisted = 3;
|
||||||
optional bool archived = 4;
|
optional bool archived = 4;
|
||||||
optional bool markedUnread = 5;
|
optional bool markedUnread = 5;
|
||||||
optional uint64 mutedUntilTimestamp = 6;
|
optional uint64 mutedUntilTimestamp = 6;
|
||||||
|
optional bool dontNotifyForMentionsIfMuted = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AccountRecord {
|
message AccountRecord {
|
||||||
|
|
|
@ -2862,6 +2862,51 @@ button.module-conversation-details__action-button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--notifications {
|
||||||
|
&::after {
|
||||||
|
-webkit-mask: url('../images/icons/v2/sound-outline-24.svg') no-repeat
|
||||||
|
center;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--mute {
|
||||||
|
&::after {
|
||||||
|
@include light-theme {
|
||||||
|
-webkit-mask: url('../images/icons/v2/bell-disabled-outline-24.svg')
|
||||||
|
no-repeat center;
|
||||||
|
background-color: $color-gray-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
-webkit-mask: url('../images/icons/v2/bell-disabled-solid-24.svg')
|
||||||
|
no-repeat center;
|
||||||
|
background-color: $color-gray-15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--mention {
|
||||||
|
&::after {
|
||||||
|
-webkit-mask: url('../images/icons/v2/at-24.svg') no-repeat center;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&--lock {
|
&--lock {
|
||||||
&::after {
|
&::after {
|
||||||
-webkit-mask: url(../images/icons/v2/lock-outline-24.svg) no-repeat
|
-webkit-mask: url(../images/icons/v2/lock-outline-24.svg) no-repeat
|
||||||
|
|
|
@ -29,3 +29,16 @@ story.add('Normal', () => {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('With disabled options', () => (
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 'a', text: 'Apples' },
|
||||||
|
{ value: 'b', text: 'Bananas', disabled: true },
|
||||||
|
{ value: 'c', text: 'Cabbage' },
|
||||||
|
{ value: 'd', text: 'Durian', disabled: true },
|
||||||
|
]}
|
||||||
|
onChange={action('onChange')}
|
||||||
|
value="c"
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React, { ChangeEvent } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export type Option = Readonly<{
|
export type Option = Readonly<{
|
||||||
|
disabled?: boolean;
|
||||||
text: string;
|
text: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
}>;
|
}>;
|
||||||
|
@ -26,9 +27,14 @@ export function Select(props: PropsType): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(['module-select', moduleClassName])}>
|
<div className={classNames(['module-select', moduleClassName])}>
|
||||||
<select value={value} onChange={onSelectChange}>
|
<select value={value} onChange={onSelectChange}>
|
||||||
{options.map(({ text, value: optionValue }) => {
|
{options.map(({ disabled, text, value: optionValue }) => {
|
||||||
return (
|
return (
|
||||||
<option value={optionValue} key={optionValue} aria-label={text}>
|
<option
|
||||||
|
disabled={disabled}
|
||||||
|
value={optionValue}
|
||||||
|
key={optionValue}
|
||||||
|
aria-label={text}
|
||||||
|
>
|
||||||
{text}
|
{text}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import Measure from 'react-measure';
|
import Measure from 'react-measure';
|
||||||
import moment from 'moment';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
@ -19,9 +18,8 @@ import { InContactsIcon } from '../InContactsIcon';
|
||||||
|
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
import { ConversationType } from '../../state/ducks/conversations';
|
import { ConversationType } from '../../state/ducks/conversations';
|
||||||
import { MuteOption, getMuteOptions } from '../../util/getMuteOptions';
|
import { getMuteOptions } from '../../util/getMuteOptions';
|
||||||
import * as expirationTimer from '../../util/expirationTimer';
|
import * as expirationTimer from '../../util/expirationTimer';
|
||||||
import { isMuted } from '../../util/isMuted';
|
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||||
|
|
||||||
|
@ -395,38 +393,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
onMoveToInbox,
|
onMoveToInbox,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const muteOptions: Array<MuteOption> = [];
|
const muteOptions = getMuteOptions(muteExpiresAt, i18n);
|
||||||
if (isMuted(muteExpiresAt)) {
|
|
||||||
const expires = moment(muteExpiresAt);
|
|
||||||
|
|
||||||
let muteExpirationLabel: string;
|
|
||||||
if (Number(muteExpiresAt) >= Number.MAX_SAFE_INTEGER) {
|
|
||||||
muteExpirationLabel = i18n('muteExpirationLabelAlways');
|
|
||||||
} else {
|
|
||||||
const muteExpirationUntil = moment().isSame(expires, 'day')
|
|
||||||
? expires.format('hh:mm A')
|
|
||||||
: expires.format('M/D/YY, hh:mm A');
|
|
||||||
|
|
||||||
muteExpirationLabel = i18n('muteExpirationLabel', [
|
|
||||||
muteExpirationUntil,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
muteOptions.push(
|
|
||||||
...[
|
|
||||||
{
|
|
||||||
name: muteExpirationLabel,
|
|
||||||
disabled: true,
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n('unmute'),
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
muteOptions.push(...getMuteOptions(i18n));
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const disappearingTitle = i18n('disappearingMessages') as any;
|
const disappearingTitle = i18n('disappearingMessages') as any;
|
||||||
|
|
|
@ -65,6 +65,9 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
||||||
showGroupChatColorEditor: action('showGroupChatColorEditor'),
|
showGroupChatColorEditor: action('showGroupChatColorEditor'),
|
||||||
showGroupLinkManagement: action('showGroupLinkManagement'),
|
showGroupLinkManagement: action('showGroupLinkManagement'),
|
||||||
showGroupV2Permissions: action('showGroupV2Permissions'),
|
showGroupV2Permissions: action('showGroupV2Permissions'),
|
||||||
|
showConversationNotificationsSettings: action(
|
||||||
|
'showConversationNotificationsSettings'
|
||||||
|
),
|
||||||
showPendingInvites: action('showPendingInvites'),
|
showPendingInvites: action('showPendingInvites'),
|
||||||
showLightboxForMedia: action('showLightboxForMedia'),
|
showLightboxForMedia: action('showLightboxForMedia'),
|
||||||
updateGroupAttributes: async () => {
|
updateGroupAttributes: async () => {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React, { useState, ReactNode } from 'react';
|
||||||
|
|
||||||
import { ConversationType } from '../../../state/ducks/conversations';
|
import { ConversationType } from '../../../state/ducks/conversations';
|
||||||
import { assert } from '../../../util/assert';
|
import { assert } from '../../../util/assert';
|
||||||
|
import { getMutedUntilText } from '../../../util/getMutedUntilText';
|
||||||
|
|
||||||
import { LocalizerType } from '../../../types/Util';
|
import { LocalizerType } from '../../../types/Util';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../LightboxGallery';
|
||||||
|
@ -62,6 +63,7 @@ export type StateProps = {
|
||||||
selectedMediaItem: MediaItemType,
|
selectedMediaItem: MediaItemType,
|
||||||
media: Array<MediaItemType>
|
media: Array<MediaItemType>
|
||||||
) => void;
|
) => void;
|
||||||
|
showConversationNotificationsSettings: () => void;
|
||||||
updateGroupAttributes: (
|
updateGroupAttributes: (
|
||||||
_: Readonly<{
|
_: Readonly<{
|
||||||
avatar?: undefined | ArrayBuffer;
|
avatar?: undefined | ArrayBuffer;
|
||||||
|
@ -95,6 +97,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
showGroupV2Permissions,
|
showGroupV2Permissions,
|
||||||
showPendingInvites,
|
showPendingInvites,
|
||||||
showLightboxForMedia,
|
showLightboxForMedia,
|
||||||
|
showConversationNotificationsSettings,
|
||||||
updateGroupAttributes,
|
updateGroupAttributes,
|
||||||
onBlock,
|
onBlock,
|
||||||
onLeave,
|
onLeave,
|
||||||
|
@ -284,6 +287,21 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<PanelRow
|
||||||
|
icon={
|
||||||
|
<ConversationDetailsIcon
|
||||||
|
ariaLabel={i18n('ConversationDetails--notifications')}
|
||||||
|
icon="notifications"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n('ConversationDetails--notifications')}
|
||||||
|
onClick={showConversationNotificationsSettings}
|
||||||
|
right={
|
||||||
|
conversation.muteExpiresAt
|
||||||
|
? getMutedUntilText(conversation.muteExpiresAt, i18n)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
|
|
||||||
<ConversationDetailsMembershipList
|
<ConversationDetailsMembershipList
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
||||||
|
import enMessages from '../../../../_locales/en/messages.json';
|
||||||
|
import { ConversationNotificationsSettings } from './ConversationNotificationsSettings';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const story = storiesOf(
|
||||||
|
'Components/Conversation/ConversationDetails/ConversationNotificationsSettings',
|
||||||
|
module
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCommonProps = () => ({
|
||||||
|
muteExpiresAt: undefined,
|
||||||
|
conversationType: 'group' as const,
|
||||||
|
dontNotifyForMentionsIfMuted: false,
|
||||||
|
i18n,
|
||||||
|
setDontNotifyForMentionsIfMuted: action('setDontNotifyForMentionsIfMuted'),
|
||||||
|
setMuteExpiration: action('setMuteExpiration'),
|
||||||
|
});
|
||||||
|
|
||||||
|
story.add('Group conversation, all default', () => (
|
||||||
|
<ConversationNotificationsSettings {...getCommonProps()} />
|
||||||
|
));
|
||||||
|
|
||||||
|
story.add('Group conversation, muted', () => (
|
||||||
|
<ConversationNotificationsSettings
|
||||||
|
{...getCommonProps()}
|
||||||
|
muteExpiresAt={Date.UTC(2099, 5, 9)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
story.add('Group conversation, @mentions muted', () => (
|
||||||
|
<ConversationNotificationsSettings
|
||||||
|
{...getCommonProps()}
|
||||||
|
dontNotifyForMentionsIfMuted
|
||||||
|
/>
|
||||||
|
));
|
|
@ -0,0 +1,129 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { FunctionComponent, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { ConversationTypeType } from '../../../state/ducks/conversations';
|
||||||
|
import { LocalizerType } from '../../../types/Util';
|
||||||
|
import { PanelSection } from './PanelSection';
|
||||||
|
import { PanelRow } from './PanelRow';
|
||||||
|
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
|
||||||
|
import { Select } from '../../Select';
|
||||||
|
import { isMuted } from '../../../util/isMuted';
|
||||||
|
import { assert } from '../../../util/assert';
|
||||||
|
import { getMuteOptions } from '../../../util/getMuteOptions';
|
||||||
|
import { parseIntOrThrow } from '../../../util/parseIntOrThrow';
|
||||||
|
|
||||||
|
type PropsType = {
|
||||||
|
conversationType: ConversationTypeType;
|
||||||
|
dontNotifyForMentionsIfMuted: boolean;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
muteExpiresAt: undefined | number;
|
||||||
|
setDontNotifyForMentionsIfMuted: (
|
||||||
|
dontNotifyForMentionsIfMuted: boolean
|
||||||
|
) => unknown;
|
||||||
|
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConversationNotificationsSettings: FunctionComponent<PropsType> = ({
|
||||||
|
conversationType,
|
||||||
|
dontNotifyForMentionsIfMuted,
|
||||||
|
i18n,
|
||||||
|
muteExpiresAt,
|
||||||
|
setMuteExpiration,
|
||||||
|
setDontNotifyForMentionsIfMuted,
|
||||||
|
}) => {
|
||||||
|
// This assertion is here to prevent accidental usage of this component in an untested
|
||||||
|
// context.
|
||||||
|
assert(
|
||||||
|
conversationType === 'group',
|
||||||
|
'<ConversationNotificationsSettings> SHOULD work for non-group conversations, but it has not been tested there'
|
||||||
|
);
|
||||||
|
|
||||||
|
const muteOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
...(isMuted(muteExpiresAt)
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
disabled: true,
|
||||||
|
text: i18n('notMuted'),
|
||||||
|
value: -1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
...getMuteOptions(muteExpiresAt, i18n).map(
|
||||||
|
({ disabled, name, value }) => ({
|
||||||
|
disabled,
|
||||||
|
text: name,
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[i18n, muteExpiresAt]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onMuteChange = (rawValue: string) => {
|
||||||
|
const ms = parseIntOrThrow(
|
||||||
|
rawValue,
|
||||||
|
'NotificationSettings: mute ms was not an integer'
|
||||||
|
);
|
||||||
|
setMuteExpiration(ms);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeDontNotifyForMentionsIfMuted = (rawValue: string) => {
|
||||||
|
setDontNotifyForMentionsIfMuted(rawValue === 'yes');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="conversation-details-panel">
|
||||||
|
<PanelSection>
|
||||||
|
<PanelRow
|
||||||
|
icon={
|
||||||
|
<ConversationDetailsIcon
|
||||||
|
ariaLabel={i18n('muteNotificationsTitle')}
|
||||||
|
icon="mute"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n('muteNotificationsTitle')}
|
||||||
|
right={
|
||||||
|
<Select options={muteOptions} onChange={onMuteChange} value={-1} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{conversationType === 'group' && (
|
||||||
|
<PanelRow
|
||||||
|
icon={
|
||||||
|
<ConversationDetailsIcon
|
||||||
|
ariaLabel={i18n(
|
||||||
|
'ConversationNotificationsSettings__mentions__label'
|
||||||
|
)}
|
||||||
|
icon="mention"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={i18n('ConversationNotificationsSettings__mentions__label')}
|
||||||
|
info={i18n('ConversationNotificationsSettings__mentions__info')}
|
||||||
|
right={
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
text: i18n(
|
||||||
|
'ConversationNotificationsSettings__mentions__select__always-notify'
|
||||||
|
),
|
||||||
|
value: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: i18n(
|
||||||
|
'ConversationNotificationsSettings__mentions__select__dont-notify-for-mentions-if-muted'
|
||||||
|
),
|
||||||
|
value: 'yes',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={onChangeDontNotifyForMentionsIfMuted}
|
||||||
|
value={dontNotifyForMentionsIfMuted ? 'yes' : 'no'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PanelSection>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
1
ts/model-types.d.ts
vendored
1
ts/model-types.d.ts
vendored
|
@ -219,6 +219,7 @@ export type ConversationAttributesType = {
|
||||||
messageCountBeforeMessageRequests?: number | null;
|
messageCountBeforeMessageRequests?: number | null;
|
||||||
messageRequestResponseType?: number;
|
messageRequestResponseType?: number;
|
||||||
muteExpiresAt?: number;
|
muteExpiresAt?: number;
|
||||||
|
dontNotifyForMentionsIfMuted?: boolean;
|
||||||
profileAvatar?: null | {
|
profileAvatar?: null | {
|
||||||
hash: string;
|
hash: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
|
|
@ -1452,6 +1452,7 @@ export class ConversationModel extends window.Backbone
|
||||||
announcementsOnlyReady: this.canBeAnnouncementGroup(),
|
announcementsOnlyReady: this.canBeAnnouncementGroup(),
|
||||||
expireTimer: this.get('expireTimer'),
|
expireTimer: this.get('expireTimer'),
|
||||||
muteExpiresAt: this.get('muteExpiresAt')!,
|
muteExpiresAt: this.get('muteExpiresAt')!,
|
||||||
|
dontNotifyForMentionsIfMuted: this.get('dontNotifyForMentionsIfMuted'),
|
||||||
name: this.get('name')!,
|
name: this.get('name')!,
|
||||||
phoneNumber: this.getNumber()!,
|
phoneNumber: this.getNumber()!,
|
||||||
profileName: this.getProfileName()!,
|
profileName: this.getProfileName()!,
|
||||||
|
@ -4787,6 +4788,7 @@ export class ConversationModel extends window.Backbone
|
||||||
// [X] whitelisted
|
// [X] whitelisted
|
||||||
// [X] archived
|
// [X] archived
|
||||||
// [X] markedUnread
|
// [X] markedUnread
|
||||||
|
// [X] dontNotifyForMentionsIfMuted
|
||||||
captureChange(logMessage: string): void {
|
captureChange(logMessage: string): void {
|
||||||
if (!window.Signal.RemoteConfig.isEnabled('desktop.storageWrite3')) {
|
if (!window.Signal.RemoteConfig.isEnabled('desktop.storageWrite3')) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
@ -4863,7 +4865,17 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isMuted()) {
|
if (this.isMuted()) {
|
||||||
return;
|
if (this.get('dontNotifyForMentionsIfMuted')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||||
|
const mentionsMe = (message.get('bodyRanges') || []).some(
|
||||||
|
range => range.mentionUuid && range.mentionUuid === ourUuid
|
||||||
|
);
|
||||||
|
if (!mentionsMe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isIncoming(message.attributes) && !reaction) {
|
if (!isIncoming(message.attributes) && !reaction) {
|
||||||
|
@ -5054,6 +5066,17 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDontNotifyForMentionsIfMuted(newValue: boolean): void {
|
||||||
|
const previousValue = Boolean(this.get('dontNotifyForMentionsIfMuted'));
|
||||||
|
if (previousValue === newValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set({ dontNotifyForMentionsIfMuted: newValue });
|
||||||
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
this.captureChange('dontNotifyForMentionsIfMuted');
|
||||||
|
}
|
||||||
|
|
||||||
acknowledgeGroupMemberNameCollisions(
|
acknowledgeGroupMemberNameCollisions(
|
||||||
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
|
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
|
||||||
): void {
|
): void {
|
||||||
|
|
|
@ -319,6 +319,9 @@ export async function toGroupV2Record(
|
||||||
groupV2Record.mutedUntilTimestamp = getSafeLongFromTimestamp(
|
groupV2Record.mutedUntilTimestamp = getSafeLongFromTimestamp(
|
||||||
conversation.get('muteExpiresAt')
|
conversation.get('muteExpiresAt')
|
||||||
);
|
);
|
||||||
|
groupV2Record.dontNotifyForMentionsIfMuted = Boolean(
|
||||||
|
conversation.get('dontNotifyForMentionsIfMuted')
|
||||||
|
);
|
||||||
|
|
||||||
applyUnknownFields(groupV2Record, conversation);
|
applyUnknownFields(groupV2Record, conversation);
|
||||||
|
|
||||||
|
@ -655,6 +658,9 @@ export async function mergeGroupV2Record(
|
||||||
conversation.set({
|
conversation.set({
|
||||||
isArchived: Boolean(groupV2Record.archived),
|
isArchived: Boolean(groupV2Record.archived),
|
||||||
markedUnread: Boolean(groupV2Record.markedUnread),
|
markedUnread: Boolean(groupV2Record.markedUnread),
|
||||||
|
dontNotifyForMentionsIfMuted: Boolean(
|
||||||
|
groupV2Record.dontNotifyForMentionsIfMuted
|
||||||
|
),
|
||||||
storageID,
|
storageID,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,7 @@ export type ConversationType = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
}>;
|
}>;
|
||||||
muteExpiresAt?: number;
|
muteExpiresAt?: number;
|
||||||
|
dontNotifyForMentionsIfMuted?: boolean;
|
||||||
type: ConversationTypeType;
|
type: ConversationTypeType;
|
||||||
isMe: boolean;
|
isMe: boolean;
|
||||||
lastUpdated?: number;
|
lastUpdated?: number;
|
||||||
|
|
21
ts/state/roots/createConversationNotificationsSettings.tsx
Normal file
21
ts/state/roots/createConversationNotificationsSettings.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { Store } from 'redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SmartConversationNotificationsSettings,
|
||||||
|
OwnProps,
|
||||||
|
} from '../smart/ConversationNotificationsSettings';
|
||||||
|
|
||||||
|
export const createConversationNotificationsSettings = (
|
||||||
|
store: Store,
|
||||||
|
props: OwnProps
|
||||||
|
): React.ReactElement => (
|
||||||
|
<Provider store={store}>
|
||||||
|
<SmartConversationNotificationsSettings {...props} />
|
||||||
|
</Provider>
|
||||||
|
);
|
|
@ -28,6 +28,7 @@ export type SmartConversationDetailsProps = {
|
||||||
showGroupChatColorEditor: () => void;
|
showGroupChatColorEditor: () => void;
|
||||||
showGroupLinkManagement: () => void;
|
showGroupLinkManagement: () => void;
|
||||||
showGroupV2Permissions: () => void;
|
showGroupV2Permissions: () => void;
|
||||||
|
showConversationNotificationsSettings: () => void;
|
||||||
showPendingInvites: () => void;
|
showPendingInvites: () => void;
|
||||||
showLightboxForMedia: (
|
showLightboxForMedia: (
|
||||||
selectedMediaItem: MediaItemType,
|
selectedMediaItem: MediaItemType,
|
||||||
|
|
46
ts/state/smart/ConversationNotificationsSettings.tsx
Normal file
46
ts/state/smart/ConversationNotificationsSettings.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { ConversationNotificationsSettings } from '../../components/conversation/conversation-details/ConversationNotificationsSettings';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
import { getConversationByIdSelector } from '../selectors/conversations';
|
||||||
|
import { strictAssert } from '../../util/assert';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
conversationId: string;
|
||||||
|
setDontNotifyForMentionsIfMuted: (
|
||||||
|
dontNotifyForMentionsIfMuted: boolean
|
||||||
|
) => unknown;
|
||||||
|
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType, props: OwnProps) => {
|
||||||
|
const {
|
||||||
|
conversationId,
|
||||||
|
setDontNotifyForMentionsIfMuted,
|
||||||
|
setMuteExpiration,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const conversationSelector = getConversationByIdSelector(state);
|
||||||
|
const conversation = conversationSelector(conversationId);
|
||||||
|
strictAssert(conversation, 'Expected a conversation to be found');
|
||||||
|
|
||||||
|
return {
|
||||||
|
conversationType: conversation.type,
|
||||||
|
dontNotifyForMentionsIfMuted: Boolean(
|
||||||
|
conversation.dontNotifyForMentionsIfMuted
|
||||||
|
),
|
||||||
|
i18n: getIntl(state),
|
||||||
|
muteExpiresAt: conversation.muteExpiresAt,
|
||||||
|
setDontNotifyForMentionsIfMuted,
|
||||||
|
setMuteExpiration,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, {});
|
||||||
|
|
||||||
|
export const SmartConversationNotificationsSettings = smart(
|
||||||
|
ConversationNotificationsSettings
|
||||||
|
);
|
92
ts/test-both/util/getMuteOptions_test.ts
Normal file
92
ts/test-both/util/getMuteOptions_test.ts
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { getMuteOptions } from '../../util/getMuteOptions';
|
||||||
|
|
||||||
|
describe('getMuteOptions', () => {
|
||||||
|
const HOUR = 3600000;
|
||||||
|
const DAY = HOUR * 24;
|
||||||
|
const WEEK = DAY * 7;
|
||||||
|
const EXPECTED_DEFAULT_OPTIONS = [
|
||||||
|
{
|
||||||
|
name: 'Mute for one hour',
|
||||||
|
value: HOUR,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mute for eight hours',
|
||||||
|
value: HOUR * 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mute for one day',
|
||||||
|
value: DAY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mute for one week',
|
||||||
|
value: WEEK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mute always',
|
||||||
|
value: Number.MAX_SAFE_INTEGER,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
describe('when not muted', () => {
|
||||||
|
it('returns the 5 default options', () => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
getMuteOptions(undefined, i18n),
|
||||||
|
EXPECTED_DEFAULT_OPTIONS
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when muted', () => {
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox();
|
||||||
|
sandbox.useFakeTimers({
|
||||||
|
now: new Date(2000, 3, 20, 12, 0, 0),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a current mute label, an "Unmute" option, and then the 5 default options', () => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
getMuteOptions(new Date(2000, 3, 20, 18, 30, 0).valueOf(), i18n),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
disabled: true,
|
||||||
|
name: 'Muted until 6:30 PM',
|
||||||
|
value: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Unmute',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
...EXPECTED_DEFAULT_OPTIONS,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the current mute label with a date if it's on a different day", () => {
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
getMuteOptions(new Date(2000, 3, 21, 18, 30, 0).valueOf(), i18n)[0],
|
||||||
|
{
|
||||||
|
disabled: true,
|
||||||
|
name: 'Muted until 04/21/2000, 6:30 PM',
|
||||||
|
value: -1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
48
ts/test-both/util/getMutedUntilText_test.ts
Normal file
48
ts/test-both/util/getMutedUntilText_test.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { getMutedUntilText } from '../../util/getMutedUntilText';
|
||||||
|
|
||||||
|
describe('getMutedUntilText', () => {
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox();
|
||||||
|
sandbox.useFakeTimers({
|
||||||
|
now: new Date(2000, 3, 20, 12, 0, 0),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an "always" label if passed a large number', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getMutedUntilText(Number.MAX_SAFE_INTEGER, i18n),
|
||||||
|
'Muted always'
|
||||||
|
);
|
||||||
|
assert.strictEqual(getMutedUntilText(Infinity, i18n), 'Muted always');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the time if the mute expires later today', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getMutedUntilText(new Date(2000, 3, 20, 18, 30, 0).valueOf(), i18n),
|
||||||
|
'Muted until 6:30 PM'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the date and time if the mute expires on another day', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getMutedUntilText(new Date(2000, 3, 21, 18, 30, 0).valueOf(), i18n),
|
||||||
|
'Muted until 04/21/2000, 6:30 PM'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { getMutedUntilText } from './getMutedUntilText';
|
||||||
|
import { isMuted } from './isMuted';
|
||||||
|
|
||||||
export type MuteOption = {
|
export type MuteOption = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -10,8 +12,24 @@ export type MuteOption = {
|
||||||
value: number;
|
value: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getMuteOptions(i18n: LocalizerType): Array<MuteOption> {
|
export function getMuteOptions(
|
||||||
|
muteExpiresAt: undefined | number,
|
||||||
|
i18n: LocalizerType
|
||||||
|
): Array<MuteOption> {
|
||||||
return [
|
return [
|
||||||
|
...(isMuted(muteExpiresAt)
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: getMutedUntilText(muteExpiresAt, i18n),
|
||||||
|
disabled: true,
|
||||||
|
value: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n('unmute'),
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
name: i18n('muteHour'),
|
name: i18n('muteHour'),
|
||||||
value: moment.duration(1, 'hour').as('milliseconds'),
|
value: moment.duration(1, 'hour').as('milliseconds'),
|
||||||
|
|
26
ts/util/getMutedUntilText.ts
Normal file
26
ts/util/getMutedUntilText.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import moment from 'moment';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns something like "Muted until 6:09 PM", localized.
|
||||||
|
*
|
||||||
|
* Shouldn't be called with `0`.
|
||||||
|
*/
|
||||||
|
export function getMutedUntilText(
|
||||||
|
muteExpiresAt: number,
|
||||||
|
i18n: LocalizerType
|
||||||
|
): string {
|
||||||
|
if (Number(muteExpiresAt) >= Number.MAX_SAFE_INTEGER) {
|
||||||
|
return i18n('muteExpirationLabelAlways');
|
||||||
|
}
|
||||||
|
|
||||||
|
const expires = moment(muteExpiresAt);
|
||||||
|
const muteExpirationUntil = moment().isSame(expires, 'day')
|
||||||
|
? expires.format('LT')
|
||||||
|
: expires.format('L, LT');
|
||||||
|
|
||||||
|
return i18n('muteExpirationLabel', [muteExpirationUntil]);
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
export function isMuted(muteExpiresAt: undefined | number): boolean {
|
export function isMuted(
|
||||||
|
muteExpiresAt: undefined | number
|
||||||
|
): muteExpiresAt is number {
|
||||||
return Boolean(muteExpiresAt && Date.now() < muteExpiresAt);
|
return Boolean(muteExpiresAt && Date.now() < muteExpiresAt);
|
||||||
}
|
}
|
||||||
|
|
|
@ -515,6 +515,14 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
return expires.format('M/D/YY, hh:mm A');
|
return expires.format('M/D/YY, hh:mm A');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setMuteExpiration(ms = 0): void {
|
||||||
|
const { model }: { model: ConversationModel } = this;
|
||||||
|
|
||||||
|
model.setMuteExpiration(
|
||||||
|
ms >= Number.MAX_SAFE_INTEGER ? ms : Date.now() + ms
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
setPin(value: boolean) {
|
setPin(value: boolean) {
|
||||||
const { model }: { model: ConversationModel } = this;
|
const { model }: { model: ConversationModel } = this;
|
||||||
|
|
||||||
|
@ -556,10 +564,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
: model.getTitle();
|
: model.getTitle();
|
||||||
searchInConversation(model.id, name);
|
searchInConversation(model.id, name);
|
||||||
},
|
},
|
||||||
onSetMuteNotifications: (ms: number) =>
|
onSetMuteNotifications: this.setMuteExpiration.bind(this),
|
||||||
model.setMuteExpiration(
|
|
||||||
ms >= Number.MAX_SAFE_INTEGER ? ms : Date.now() + ms
|
|
||||||
),
|
|
||||||
onSetPin: this.setPin.bind(this),
|
onSetPin: this.setPin.bind(this),
|
||||||
// These are view only and don't update the Conversation model, so they
|
// These are view only and don't update the Conversation model, so they
|
||||||
// need a manual update call.
|
// need a manual update call.
|
||||||
|
@ -3206,6 +3211,28 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
view.render();
|
view.render();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showConversationNotificationsSettings() {
|
||||||
|
const { model }: { model: ConversationModel } = this;
|
||||||
|
|
||||||
|
const view = new Whisper.ReactWrapperView({
|
||||||
|
className: 'panel',
|
||||||
|
JSX: window.Signal.State.Roots.createConversationNotificationsSettings(
|
||||||
|
window.reduxStore,
|
||||||
|
{
|
||||||
|
conversationId: model.id,
|
||||||
|
setDontNotifyForMentionsIfMuted: model.setDontNotifyForMentionsIfMuted.bind(
|
||||||
|
model
|
||||||
|
),
|
||||||
|
setMuteExpiration: this.setMuteExpiration.bind(this),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
view.headerTitle = window.i18n('ConversationDetails--notifications');
|
||||||
|
|
||||||
|
this.listenBack(view);
|
||||||
|
view.render();
|
||||||
|
},
|
||||||
|
|
||||||
showChatColorEditor() {
|
showChatColorEditor() {
|
||||||
const { model }: { model: ConversationModel } = this;
|
const { model }: { model: ConversationModel } = this;
|
||||||
|
|
||||||
|
@ -3268,6 +3295,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
showGroupChatColorEditor: this.showChatColorEditor.bind(this),
|
showGroupChatColorEditor: this.showChatColorEditor.bind(this),
|
||||||
showGroupLinkManagement: this.showGroupLinkManagement.bind(this),
|
showGroupLinkManagement: this.showGroupLinkManagement.bind(this),
|
||||||
showGroupV2Permissions: this.showGroupV2Permissions.bind(this),
|
showGroupV2Permissions: this.showGroupV2Permissions.bind(this),
|
||||||
|
showConversationNotificationsSettings: this.showConversationNotificationsSettings.bind(
|
||||||
|
this
|
||||||
|
),
|
||||||
showPendingInvites: this.showPendingInvites.bind(this),
|
showPendingInvites: this.showPendingInvites.bind(this),
|
||||||
showLightboxForMedia: this.showLightboxForMedia.bind(this),
|
showLightboxForMedia: this.showLightboxForMedia.bind(this),
|
||||||
updateGroupAttributes: model.updateGroupAttributesV2.bind(model),
|
updateGroupAttributes: model.updateGroupAttributesV2.bind(model),
|
||||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -54,6 +54,7 @@ import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
|
||||||
import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions';
|
import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions';
|
||||||
import { createLeftPane } from './state/roots/createLeftPane';
|
import { createLeftPane } from './state/roots/createLeftPane';
|
||||||
import { createMessageDetail } from './state/roots/createMessageDetail';
|
import { createMessageDetail } from './state/roots/createMessageDetail';
|
||||||
|
import { createConversationNotificationsSettings } from './state/roots/createConversationNotificationsSettings';
|
||||||
import { createPendingInvites } from './state/roots/createPendingInvites';
|
import { createPendingInvites } from './state/roots/createPendingInvites';
|
||||||
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
||||||
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
||||||
|
@ -437,6 +438,7 @@ declare global {
|
||||||
createGroupV2Permissions: typeof createGroupV2Permissions;
|
createGroupV2Permissions: typeof createGroupV2Permissions;
|
||||||
createLeftPane: typeof createLeftPane;
|
createLeftPane: typeof createLeftPane;
|
||||||
createMessageDetail: typeof createMessageDetail;
|
createMessageDetail: typeof createMessageDetail;
|
||||||
|
createConversationNotificationsSettings: typeof createConversationNotificationsSettings;
|
||||||
createPendingInvites: typeof createPendingInvites;
|
createPendingInvites: typeof createPendingInvites;
|
||||||
createSafetyNumberViewer: typeof createSafetyNumberViewer;
|
createSafetyNumberViewer: typeof createSafetyNumberViewer;
|
||||||
createShortcutGuideModal: typeof createShortcutGuideModal;
|
createShortcutGuideModal: typeof createShortcutGuideModal;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue