add a HeaderOption to change the notification settings for each convo

This commit is contained in:
Audric Ackermann 2021-06-07 14:20:07 +10:00
parent f9dfe4290a
commit 4a98d911a2
No known key found for this signature in database
GPG key ID: 999F434D76324AD4
8 changed files with 148 additions and 14 deletions

View file

@ -1022,6 +1022,22 @@
"description": "Conversation menu option to enable disappearing messages",
"androidKey": "conversation_expiring_off__disappearing_messages"
},
"notificationForConvo": {
"message": "Notifications",
"description": "Conversation menu option to change the notification setting for this conversation"
},
"notificationForConvo_all": {
"message": "All",
"description": "Menu item to allow notification for this conversation for all messages"
},
"notificationForConvo_disabled": {
"message": "Disabled",
"description": "Menu item to deny notification for this conversation for all messages"
},
"notificationForConvo_mentions_only": {
"message": "Mentions only",
"description": "Menu item to allow notification for this conversation for all messages mentioning us"
},
"changeNickname": {
"message": "Change Nickname",
"description": "Conversation menu option to change user nickname"

View file

@ -15,12 +15,18 @@ import {
} from '../session/menu/ConversationHeaderMenu';
import { contextMenu } from 'react-contexify';
import { DefaultTheme, withTheme } from 'styled-components';
import { ConversationNotificationSettingType } from '../../models/conversation';
export interface TimerOption {
name: string;
value: number;
}
export interface NotificationForConvoOption {
name: string;
value: ConversationNotificationSettingType;
}
interface Props {
id: string;
name?: string;
@ -46,6 +52,8 @@ interface Props {
expirationSettingName?: string;
showBackButton: boolean;
timerOptions: Array<TimerOption>;
notificationForConvo: Array<NotificationForConvoOption>;
currentNotificationSetting: ConversationNotificationSettingType;
hasNickname?: boolean;
isBlocked: boolean;
@ -56,6 +64,7 @@ interface Props {
onInviteContacts: () => void;
onSetDisappearingMessages: (seconds: number) => void;
onSetNotificationForConvo: (selected: ConversationNotificationSettingType) => void;
onDeleteMessages: () => void;
onDeleteContact: () => void;
onChangeNickname?: () => void;

View file

@ -724,21 +724,10 @@ class MessageInner extends React.PureComponent<MessageRegularProps, State> {
const width = this.getWidth();
const isShowingImage = this.isShowingImage();
// We parse the message later, but we still need to do an early check
// to see if the message mentions us, so we can display the entire
// message differently
const regex = new RegExp(`@${PubKey.regexForPubkeys}`, 'g');
const mentions = (text ? text.match(regex) : []) as Array<string>;
const mentionMe = mentions && mentions.some(m => UserUtils.isUsFromCache(m.slice(1)));
const isIncoming = direction === 'incoming';
const shouldHightlight = mentionMe && isIncoming && isPublic;
const shouldMarkReadWhenVisible = isIncoming && isUnread;
const divClasses = ['session-message-wrapper'];
if (shouldHightlight) {
//divClasses.push('message-highlighted');
}
if (selected) {
divClasses.push('message-selected');
}

View file

@ -30,7 +30,11 @@ import { getMessageById, getPubkeysInPublicConversation } from '../../../data/da
import autoBind from 'auto-bind';
import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
import { deleteOpenGroupMessages } from '../../../interactions/conversation';
import { ConversationTypeEnum } from '../../../models/conversation';
import {
ConversationNotificationSetting,
ConversationNotificationSettingType,
ConversationTypeEnum,
} from '../../../models/conversation';
import { updateMentionsMembers } from '../../../state/ducks/mentionsInput';
import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage';
@ -369,10 +373,18 @@ export class SessionConversation extends React.Component<Props, State> {
name: item.getName(),
value: item.get('seconds'),
})),
notificationForConvo: ConversationNotificationSetting.map(
(n: ConversationNotificationSettingType) => {
// this link to the notificationForConvo_all, notificationForConvo_mentions_only, ...
return { value: n, name: window.i18n(`notificationForConvo_${n}`) };
}
),
currentNotificationSetting: conversation.get('triggerNotificationsFor'),
hasNickname: !!conversation.getNickname(),
selectionMode: !!selectedMessages.length,
onSetDisappearingMessages: conversation.updateExpirationTimer,
onSetNotificationForConvo: conversation.setNotificationOption,
onDeleteMessages: conversation.deleteMessages,
onDeleteSelectedMessages: this.deleteSelectedMessages,
onChangeNickname: conversation.changeNickname,

View file

@ -12,10 +12,12 @@ import {
getInviteContactMenuItem,
getLeaveGroupMenuItem,
getMarkAllReadMenuItem,
getNotificationForConvoMenuItem,
getRemoveModeratorsMenuItem,
getUpdateGroupNameMenuItem,
} from './Menu';
import { TimerOption } from '../../conversation/ConversationHeader';
import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader';
import { ConversationNotificationSettingType } from '../../../models/conversation';
export type PropsConversationHeaderMenu = {
triggerId: string;
@ -26,6 +28,8 @@ export type PropsConversationHeaderMenu = {
isGroup: boolean;
isAdmin: boolean;
timerOptions: Array<TimerOption>;
notificationForConvo: Array<NotificationForConvoOption>;
currentNotificationSetting: ConversationNotificationSettingType;
isPrivate: boolean;
isBlocked: boolean;
hasNickname?: boolean;
@ -45,6 +49,7 @@ export type PropsConversationHeaderMenu = {
onBlockUser: () => void;
onUnblockUser: () => void;
onSetDisappearingMessages: (seconds: number) => void;
onSetNotificationForConvo: (selected: ConversationNotificationSettingType) => void;
};
export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
@ -60,6 +65,8 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
isPrivate,
left,
hasNickname,
notificationForConvo,
currentNotificationSetting,
onClearNickname,
onChangeNickname,
@ -75,6 +82,7 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
onBlockUser,
onUnblockUser,
onSetDisappearingMessages,
onSetNotificationForConvo,
} = props;
return (
@ -88,6 +96,15 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
onSetDisappearingMessages,
window.i18n
)}
{getNotificationForConvoMenuItem(
isKickedFromGroup,
left,
isBlocked,
notificationForConvo,
currentNotificationSetting,
onSetNotificationForConvo,
window.i18n
)}
{getBlockMenuItem(isMe, isPrivate, isBlocked, onBlockUser, onUnblockUser, window.i18n)}
{getCopyMenuItem(isPublic, isGroup, onCopyPublicKey, window.i18n)}

View file

@ -1,7 +1,11 @@
import React from 'react';
import { LocalizerType } from '../../../types/Util';
import { TimerOption } from '../../conversation/ConversationHeader';
import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader';
import { Item, Submenu } from 'react-contexify';
import {
ConversationNotificationSetting,
ConversationNotificationSettingType,
} from '../../../models/conversation';
function showTimerOptions(
isPublic: boolean,
@ -12,6 +16,14 @@ function showTimerOptions(
return !isPublic && !left && !isKickedFromGroup && !isBlocked;
}
function showNotificationConvo(
isKickedFromGroup: boolean,
left: boolean,
isBlocked: boolean
): boolean {
return !left && !isKickedFromGroup && !isBlocked;
}
function showMemberMenu(isPublic: boolean, isGroup: boolean): boolean {
return !isPublic && isGroup;
}
@ -223,6 +235,41 @@ export function getDisappearingMenuItem(
return null;
}
export function getNotificationForConvoMenuItem(
isKickedFromGroup: boolean | undefined,
left: boolean | undefined,
isBlocked: boolean | undefined,
notificationForConvoOptions: Array<NotificationForConvoOption>,
currentNotificationSetting: ConversationNotificationSettingType,
action: (selected: ConversationNotificationSettingType) => any,
i18n: LocalizerType
): JSX.Element | null {
if (showNotificationConvo(Boolean(isKickedFromGroup), Boolean(left), Boolean(isBlocked))) {
// const isRtlMode = isRtlBody();
return (
// Remove the && false to make context menu work with RTL support
<Submenu
label={i18n('notificationForConvo') as any}
// rtl={isRtlMode && false}
>
{(notificationForConvoOptions || []).map(item => (
// tslint:disable-next-line: use-simple-attributes
<Item
key={item.value}
onClick={() => {
action(item.value);
}}
disabled={item.value === currentNotificationSetting}
>
{item.name}
</Item>
))}
</Submenu>
);
}
return null;
}
export function isRtlBody(): boolean {
return ($('body') as any).hasClass('rtl');
}

View file

@ -40,12 +40,21 @@ import { ConversationInteraction } from '../interactions';
import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil';
import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUtils';
import { NotificationForConvoOption } from '../components/conversation/ConversationHeader';
export enum ConversationTypeEnum {
GROUP = 'group',
PRIVATE = 'private',
}
/**
* all: all notifications enabled, the default
* disabled: no notifications at all
* mentions_only: trigger a notification only on mentions of ourself
*/
export const ConversationNotificationSetting = ['all', 'disabled', 'mentions_only'] as const;
export type ConversationNotificationSettingType = typeof ConversationNotificationSetting[number];
export interface ConversationAttributes {
profileName?: string;
id: string;
@ -81,6 +90,7 @@ export interface ConversationAttributes {
profileAvatar?: any;
profileKey?: string;
accessKey?: any;
triggerNotificationsFor: ConversationNotificationSettingType;
}
export interface ConversationAttributesOptionals {
@ -116,6 +126,7 @@ export interface ConversationAttributesOptionals {
profileAvatar?: any;
profileKey?: string;
accessKey?: any;
triggerNotificationsFor?: ConversationNotificationSettingType;
}
/**
@ -143,6 +154,7 @@ export const fillConvoAttributesWithDefaults = (
expireTimer: 0,
mentionedUs: false,
active_at: 0,
triggerNotificationsFor: 'all', // if the settings is not set in the db, this is the default
});
};
@ -185,6 +197,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return this.id;
}
if (this.isPublic()) {
return `opengroup(${this.id})`;
}
return `group(${this.id})`;
}
@ -777,6 +793,14 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
}
public async setNotificationOption(selected: ConversationNotificationSettingType) {
const existingSettings = this.get('triggerNotificationsFor');
if (existingSettings !== selected) {
this.set({ triggerNotificationsFor: selected });
await this.commit();
}
}
public async updateExpirationTimer(
providedExpireTimer: any,
providedSource?: string,
@ -1402,6 +1426,25 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
const conversationId = this.id;
// make sure the notifications are not muted for this convo (and not the source convo)
const convNotif = this.get('triggerNotificationsFor');
if (convNotif === 'disabled') {
window?.log?.info('notifications disabled for convo', this.idForLogging());
return;
}
if (convNotif === 'mentions_only') {
// check if the message has ourselves as mentions
const regex = new RegExp(`@${PubKey.regexForPubkeys}`, 'g');
const text = message.get('body');
const mentions = text?.match(regex) || ([] as Array<string>);
const mentionMe = mentions && mentions.some(m => UserUtils.isUsFromCache(m.slice(1)));
if (!mentionMe) {
window?.log?.info('notifications disabled for non mentions for convo', conversationId);
return;
}
}
const convo = await ConversationController.getInstance().getOrCreateAndWait(
message.get('source'),
ConversationTypeEnum.PRIVATE

View file

@ -86,6 +86,7 @@ export class MockConversation {
lastMessageStatus: null,
lastMessage: null,
zombies: [],
triggerNotificationsFor: 'all',
};
}