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.
265 lines
8.1 KiB
TypeScript
265 lines
8.1 KiB
TypeScript
1 year ago
|
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,
|
||
|
};
|