Merge pull request #1682 from Bilb/muting-conversation

add a HeaderOption to change the notification settings for each convo
pull/1681/head
Audric Ackermann 4 years ago committed by GitHub
commit 0d9882ccfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1022,6 +1022,22 @@
"description": "Conversation menu option to enable disappearing messages", "description": "Conversation menu option to enable disappearing messages",
"androidKey": "conversation_expiring_off__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": { "changeNickname": {
"message": "Change Nickname", "message": "Change Nickname",
"description": "Conversation menu option to change user nickname" "description": "Conversation menu option to change user nickname"

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

@ -724,21 +724,10 @@ class MessageInner extends React.PureComponent<MessageRegularProps, State> {
const width = this.getWidth(); const width = this.getWidth();
const isShowingImage = this.isShowingImage(); 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 isIncoming = direction === 'incoming';
const shouldHightlight = mentionMe && isIncoming && isPublic;
const shouldMarkReadWhenVisible = isIncoming && isUnread; const shouldMarkReadWhenVisible = isIncoming && isUnread;
const divClasses = ['session-message-wrapper']; const divClasses = ['session-message-wrapper'];
if (shouldHightlight) {
//divClasses.push('message-highlighted');
}
if (selected) { if (selected) {
divClasses.push('message-selected'); divClasses.push('message-selected');
} }

@ -30,7 +30,11 @@ import { getMessageById, getPubkeysInPublicConversation } from '../../../data/da
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager'; import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
import { deleteOpenGroupMessages } from '../../../interactions/conversation'; import { deleteOpenGroupMessages } from '../../../interactions/conversation';
import { ConversationTypeEnum } from '../../../models/conversation'; import {
ConversationNotificationSetting,
ConversationNotificationSettingType,
ConversationTypeEnum,
} from '../../../models/conversation';
import { updateMentionsMembers } from '../../../state/ducks/mentionsInput'; import { updateMentionsMembers } from '../../../state/ducks/mentionsInput';
import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage'; import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage';
@ -347,6 +351,14 @@ export class SessionConversation extends React.Component<Props, State> {
const members = conversation.get('members') || []; const members = conversation.get('members') || [];
// exclude mentions_only settings for private chats as this does not make much sense
const notificationForConvo = ConversationNotificationSetting.filter(n =>
conversation.isPrivate() ? n !== 'mentions_only' : true
).map((n: ConversationNotificationSettingType) => {
// this link to the notificationForConvo_all, notificationForConvo_mentions_only, ...
return { value: n, name: window.i18n(`notificationForConvo_${n}`) };
});
const headerProps = { const headerProps = {
id: conversation.id, id: conversation.id,
name: conversation.getName(), name: conversation.getName(),
@ -369,10 +381,13 @@ export class SessionConversation extends React.Component<Props, State> {
name: item.getName(), name: item.getName(),
value: item.get('seconds'), value: item.get('seconds'),
})), })),
notificationForConvo,
currentNotificationSetting: conversation.get('triggerNotificationsFor'),
hasNickname: !!conversation.getNickname(), hasNickname: !!conversation.getNickname(),
selectionMode: !!selectedMessages.length, selectionMode: !!selectedMessages.length,
onSetDisappearingMessages: conversation.updateExpirationTimer, onSetDisappearingMessages: conversation.updateExpirationTimer,
onSetNotificationForConvo: conversation.setNotificationOption,
onDeleteMessages: conversation.deleteMessages, onDeleteMessages: conversation.deleteMessages,
onDeleteSelectedMessages: this.deleteSelectedMessages, onDeleteSelectedMessages: this.deleteSelectedMessages,
onChangeNickname: conversation.changeNickname, onChangeNickname: conversation.changeNickname,

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

@ -1,7 +1,11 @@
import React from 'react'; import React from 'react';
import { LocalizerType } from '../../../types/Util'; import { LocalizerType } from '../../../types/Util';
import { TimerOption } from '../../conversation/ConversationHeader'; import { NotificationForConvoOption, TimerOption } from '../../conversation/ConversationHeader';
import { Item, Submenu } from 'react-contexify'; import { Item, Submenu } from 'react-contexify';
import {
ConversationNotificationSetting,
ConversationNotificationSettingType,
} from '../../../models/conversation';
function showTimerOptions( function showTimerOptions(
isPublic: boolean, isPublic: boolean,
@ -12,6 +16,14 @@ function showTimerOptions(
return !isPublic && !left && !isKickedFromGroup && !isBlocked; 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 { function showMemberMenu(isPublic: boolean, isGroup: boolean): boolean {
return !isPublic && isGroup; return !isPublic && isGroup;
} }
@ -223,6 +235,41 @@ export function getDisappearingMenuItem(
return null; 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 { export function isRtlBody(): boolean {
return ($('body') as any).hasClass('rtl'); return ($('body') as any).hasClass('rtl');
} }

@ -40,12 +40,21 @@ import { ConversationInteraction } from '../interactions';
import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil'; import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil';
import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUtils'; import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUtils';
import { NotificationForConvoOption } from '../components/conversation/ConversationHeader';
export enum ConversationTypeEnum { export enum ConversationTypeEnum {
GROUP = 'group', GROUP = 'group',
PRIVATE = 'private', 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 { export interface ConversationAttributes {
profileName?: string; profileName?: string;
id: string; id: string;
@ -81,6 +90,7 @@ export interface ConversationAttributes {
profileAvatar?: any; profileAvatar?: any;
profileKey?: string; profileKey?: string;
accessKey?: any; accessKey?: any;
triggerNotificationsFor: ConversationNotificationSettingType;
} }
export interface ConversationAttributesOptionals { export interface ConversationAttributesOptionals {
@ -116,6 +126,7 @@ export interface ConversationAttributesOptionals {
profileAvatar?: any; profileAvatar?: any;
profileKey?: string; profileKey?: string;
accessKey?: any; accessKey?: any;
triggerNotificationsFor?: ConversationNotificationSettingType;
} }
/** /**
@ -143,6 +154,7 @@ export const fillConvoAttributesWithDefaults = (
expireTimer: 0, expireTimer: 0,
mentionedUs: false, mentionedUs: false,
active_at: 0, 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; return this.id;
} }
if (this.isPublic()) {
return `opengroup(${this.id})`;
}
return `group(${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( public async updateExpirationTimer(
providedExpireTimer: any, providedExpireTimer: any,
providedSource?: string, providedSource?: string,
@ -1402,6 +1426,25 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
const conversationId = this.id; 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( const convo = await ConversationController.getInstance().getOrCreateAndWait(
message.get('source'), message.get('source'),
ConversationTypeEnum.PRIVATE ConversationTypeEnum.PRIVATE

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

Loading…
Cancel
Save