You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/notifications/formatNotifications.ts

265 lines
8.1 KiB
TypeScript

import { PubkeyType } from 'libsession_util_nodejs';
import { cloneDeep } from 'lodash';
import { usernameForQuoteOrFullPkOutsideRedux } from '../hooks/useParamSelector';
import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../interactions/conversationInteractions';
import { DataExtractionNotificationMsg, MessageGroupUpdate } from '../models/messageType';
import { SignalService } from '../protobuf';
import { ConvoHub } from '../session/conversations';
import { UserUtils } from '../session/utils';
import { PreConditionFailed } from '../session/utils/errors';
import { CallNotificationType, InteractionNotificationType } from '../state/ducks/conversations';
import { assertUnreachable } from '../types/sqlSharedTypes';
/**
* @returns true if the array contains only a single item being 'You', 'you' or our device pubkey
*/
export function arrayContainsUsOnly(arrayToCheck: Array<string> | undefined) {
return (
arrayToCheck &&
arrayToCheck.length === 1 &&
(arrayToCheck[0] === UserUtils.getOurPubKeyStrFromCache() ||
arrayToCheck[0].toLowerCase() === 'you')
);
}
function formatGroupUpdateNotification(groupUpdate: MessageGroupUpdate) {
const us = UserUtils.getOurPubKeyStrFromCache();
if (groupUpdate.name) {
return window.i18n('titleIsNow', [groupUpdate.name]);
}
if (groupUpdate.avatarChange) {
return window.i18n('groupAvatarChange');
}
if (groupUpdate.left) {
if (groupUpdate.left.length !== 1) {
return null;
}
if (arrayContainsUsOnly(groupUpdate.left)) {
return window.i18n('youLeftTheGroup');
}
// no more than one can send a leave message at a time
return window.i18n('leftTheGroup', [
ConvoHub.use().getContactProfileNameOrShortenedPubKey(groupUpdate.left[0]),
]);
}
if (groupUpdate.joined) {
if (!groupUpdate.joined.length) {
return null;
}
return changeOfMembersV2({
type: 'added',
us,
changedWithNames: mapIdsWithNames(
groupUpdate.joined,
groupUpdate.joined.map(usernameForQuoteOrFullPkOutsideRedux)
),
});
}
if (groupUpdate.joinedWithHistory) {
if (!groupUpdate.joinedWithHistory.length) {
return null;
}
return changeOfMembersV2({
type: 'addedWithHistory',
us,
changedWithNames: mapIdsWithNames(
groupUpdate.joinedWithHistory,
groupUpdate.joinedWithHistory.map(usernameForQuoteOrFullPkOutsideRedux)
),
});
}
if (groupUpdate.kicked) {
if (!groupUpdate.kicked.length) {
return null;
}
if (arrayContainsUsOnly(groupUpdate.kicked)) {
return window.i18n('youGotKickedFromGroup');
}
return changeOfMembersV2({
type: 'removed',
us,
changedWithNames: mapIdsWithNames(
groupUpdate.kicked,
groupUpdate.kicked.map(usernameForQuoteOrFullPkOutsideRedux)
),
});
}
if (groupUpdate.promoted) {
if (!groupUpdate.promoted.length) {
return null;
}
return changeOfMembersV2({
type: 'promoted',
us,
changedWithNames: mapIdsWithNames(
groupUpdate.promoted,
groupUpdate.promoted.map(usernameForQuoteOrFullPkOutsideRedux)
),
});
}
throw new Error('group_update getDescription() case not taken care of');
}
function formatDataExtractionNotification(
dataExtractionNotification: DataExtractionNotificationMsg
) {
const { Type } = SignalService.DataExtractionNotification;
const isScreenshot = dataExtractionNotification.type === Type.SCREENSHOT;
return window.i18n(isScreenshot ? 'tookAScreenshot' : 'savedTheFile', [
ConvoHub.use().getContactProfileNameOrShortenedPubKey(dataExtractionNotification.source),
]);
}
function formatInteractionNotification(
interactionNotification: InteractionNotificationType,
conversationId: string
) {
const { interactionType, interactionStatus } = interactionNotification;
// NOTE For now we only show interaction errors in the message history
if (interactionStatus === ConversationInteractionStatus.Error) {
const convo = ConvoHub.use().get(conversationId);
if (convo) {
const isGroup = !convo.isPrivate();
const isCommunity = convo.isPublic();
switch (interactionType) {
case ConversationInteractionType.Hide:
// there is no text for hiding changes
return '';
case ConversationInteractionType.Leave:
return isCommunity
? window.i18n('leaveCommunityFailed')
: isGroup
? window.i18n('leaveGroupFailed')
: window.i18n('deleteConversationFailed');
default:
assertUnreachable(
interactionType,
`Message.getDescription: Missing case error "${interactionType}"`
);
}
}
}
window.log.error('formatInteractionNotification: Unsupported case');
return null;
}
function formatCallNotification(
callNotificationType: CallNotificationType,
conversationId: string
) {
const displayName = ConvoHub.use().getContactProfileNameOrShortenedPubKey(conversationId);
if (callNotificationType === 'missed-call') {
return window.i18n('callMissed', [displayName]);
}
if (callNotificationType === 'started-call') {
return window.i18n('startedACall', [displayName]);
}
if (callNotificationType === 'answered-a-call') {
return window.i18n('answeredACall', [displayName]);
}
window.log.error('formatCallNotification: Unsupported notification type');
return null;
}
export type IdWithName = { sessionId: PubkeyType; name: string };
/**
* When we are part of a change, we display the You first, and then others.
* This function is used to check if we are part of the list.
* - if yes: returns {weArePart: true, others: changedWithoutUs}
* - if yes: returns {weArePart: false, others: changed}
*/
function moveUsToStart(
changed: Array<IdWithName>,
us: PubkeyType
): {
sortedWithUsFirst: Array<IdWithName>;
} {
const usAt = changed.findIndex(m => m.sessionId === us);
if (usAt <= -1) {
// we are not in it
return { sortedWithUsFirst: changed };
}
const usItem = changed.at(usAt);
if (!usItem) {
throw new PreConditionFailed('"we" should have been there');
}
// deepClone because splice mutates the array
const changedCopy = cloneDeep(changed);
changedCopy.splice(usAt, 1);
return { sortedWithUsFirst: [usItem, ...changedCopy] };
}
function changeOfMembersV2({
changedWithNames,
type,
us,
}: {
type: 'added' | 'addedWithHistory' | 'promoted' | 'removed';
changedWithNames: Array<IdWithName>;
us: PubkeyType;
}): string {
const { sortedWithUsFirst } = moveUsToStart(changedWithNames, us);
if (changedWithNames.length === 0) {
throw new PreConditionFailed('change must always have an associated change');
}
const subject =
sortedWithUsFirst.length === 1 && sortedWithUsFirst[0].sessionId === us
? 'You'
: sortedWithUsFirst.length === 1
? 'One'
: sortedWithUsFirst.length === 2
? 'Two'
: 'Others';
const action =
type === 'addedWithHistory'
? 'JoinedWithHistory'
: type === 'added'
? 'Joined'
: type === 'promoted'
? 'Promoted'
: ('Removed' as const);
const key = `group${subject}${action}` as const;
const sortedWithUsOrCount =
subject === 'Others'
? [sortedWithUsFirst[0].name, (sortedWithUsFirst.length - 1).toString()]
: sortedWithUsFirst.map(m => m.name);
return window.i18n(key, sortedWithUsOrCount);
}
function mapIdsWithNames(changed: Array<PubkeyType>, names: Array<string>): Array<IdWithName> {
if (!changed.length || !names.length) {
throw new PreConditionFailed('mapIdsWithNames needs a change');
}
if (changed.length !== names.length) {
throw new PreConditionFailed('mapIdsWithNames needs a the same length to map them together');
}
return changed.map((sessionId, index) => {
return { sessionId, name: names[index] };
});
}
export const FormatNotifications = {
arrayContainsUsOnly,
formatCallNotification,
formatInteractionNotification,
formatDataExtractionNotification,
formatGroupUpdateNotification,
changeOfMembersV2,
mapIdsWithNames,
};