243 lines
9 KiB
TypeScript
243 lines
9 KiB
TypeScript
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import memoizee from 'memoizee';
|
|
import { sortBy } from 'lodash';
|
|
import type { ConversationModel } from '../models/conversations';
|
|
import type { ConversationType } from '../state/ducks/conversations';
|
|
import type { ConversationAttributesType } from '../model-types';
|
|
import type { GroupNameCollisionsWithIdsByTitle } from './groupMemberNameCollisions';
|
|
import { StorySendMode } from '../types/Stories';
|
|
import { areWeAdmin } from './areWeAdmin';
|
|
import { buildGroupLink } from '../groups';
|
|
import { canAddNewMembers } from './canAddNewMembers';
|
|
import { canBeAnnouncementGroup } from './canBeAnnouncementGroup';
|
|
import { canChangeTimer } from './canChangeTimer';
|
|
import { canEditGroupInfo } from './canEditGroupInfo';
|
|
import { dropNull } from './dropNull';
|
|
import { getAboutText } from './getAboutText';
|
|
import {
|
|
getAbsoluteAvatarPath,
|
|
getAbsoluteUnblurredAvatarPath,
|
|
getAbsoluteProfileAvatarPath,
|
|
getAvatarHash,
|
|
} from './avatarUtils';
|
|
import { getAvatarData } from './getAvatarData';
|
|
import { getConversationMembers } from './getConversationMembers';
|
|
import { getCustomColorData, migrateColor } from './migrateColor';
|
|
import { getDraftPreview } from './getDraftPreview';
|
|
import { getLastMessage } from './getLastMessage';
|
|
import {
|
|
getNumber,
|
|
getProfileName,
|
|
getTitle,
|
|
getTitleNoDefault,
|
|
canHaveUsername,
|
|
} from './getTitle';
|
|
import { hasDraft } from './hasDraft';
|
|
import { isBlocked } from './isBlocked';
|
|
import { isConversationAccepted } from './isConversationAccepted';
|
|
import {
|
|
isDirectConversation,
|
|
isGroupV1,
|
|
isGroupV2,
|
|
isMe,
|
|
} from './whatTypeOfConversation';
|
|
import {
|
|
getBannedMemberships,
|
|
getMembersCount,
|
|
getMemberships,
|
|
getPendingApprovalMemberships,
|
|
getPendingMemberships,
|
|
isMember,
|
|
isMemberAwaitingApproval,
|
|
isMemberPending,
|
|
} from './groupMembershipUtils';
|
|
import { isNotNil } from './isNotNil';
|
|
|
|
const EMPTY_ARRAY: Readonly<[]> = [];
|
|
const EMPTY_GROUP_COLLISIONS: GroupNameCollisionsWithIdsByTitle = {};
|
|
|
|
const getCollator = memoizee((): Intl.Collator => {
|
|
return new Intl.Collator(undefined, { sensitivity: 'base' });
|
|
});
|
|
|
|
function sortConversationTitles(
|
|
left: ConversationAttributesType,
|
|
right: ConversationAttributesType
|
|
) {
|
|
return getCollator().compare(getTitle(left), getTitle(right));
|
|
}
|
|
|
|
// Note: this should never be called directly. Use conversation.format() instead, which
|
|
// maintains a cache, and protects against reentrant calls.
|
|
// Note: When writing code inside this function, do not call .format() on a conversation
|
|
// unless you are sure that it's not this very same conversation.
|
|
// Note: If you start relying on an attribute that is in
|
|
// `ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE`, remove it from that list.
|
|
export function getConversation(model: ConversationModel): ConversationType {
|
|
const { attributes } = model;
|
|
const typingValues = sortBy(
|
|
Object.values(model.contactTypingTimers || {}),
|
|
'timestamp'
|
|
);
|
|
const typingContactIdTimestamps = Object.fromEntries(
|
|
typingValues.map(({ senderId, timestamp }) => [senderId, timestamp])
|
|
);
|
|
|
|
const ourAci = window.textsecure.storage.user.getAci();
|
|
const ourPni = window.textsecure.storage.user.getPni();
|
|
|
|
const color = migrateColor(attributes.color);
|
|
|
|
const { draftTimestamp, draftEditMessage, timestamp } = attributes;
|
|
const draftPreview = getDraftPreview(attributes);
|
|
const draftText = dropNull(attributes.draft);
|
|
const shouldShowDraft = Boolean(
|
|
hasDraft(attributes) && draftTimestamp && draftTimestamp >= (timestamp || 0)
|
|
);
|
|
const inboxPosition = attributes.inbox_position;
|
|
const ourConversationId =
|
|
window.ConversationController.getOurConversationId();
|
|
|
|
let groupVersion: undefined | 1 | 2;
|
|
if (isGroupV1(attributes)) {
|
|
groupVersion = 1;
|
|
} else if (isGroupV2(attributes)) {
|
|
groupVersion = 2;
|
|
}
|
|
|
|
const sortedGroupMembers = isGroupV2(attributes)
|
|
? getConversationMembers(attributes)
|
|
.sort((left, right) => sortConversationTitles(left, right))
|
|
.map(member => window.ConversationController.get(member.id)?.format())
|
|
.filter(isNotNil)
|
|
: undefined;
|
|
|
|
const { customColor, customColorId } = getCustomColorData(attributes);
|
|
|
|
// TODO: DESKTOP-720
|
|
return {
|
|
id: attributes.id,
|
|
serviceId: attributes.serviceId,
|
|
pni: attributes.pni,
|
|
e164: attributes.e164,
|
|
|
|
// We had previously stored `null` instead of `undefined` in some cases. We should
|
|
// be able to remove this `dropNull` once usernames have gone to production.
|
|
username: canHaveUsername(attributes, ourConversationId)
|
|
? dropNull(attributes.username)
|
|
: undefined,
|
|
|
|
about: getAboutText(attributes),
|
|
aboutText: attributes.about,
|
|
aboutEmoji: attributes.aboutEmoji,
|
|
acceptedMessageRequest: isConversationAccepted(attributes),
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
activeAt: attributes.active_at!,
|
|
areWePending:
|
|
ourAci &&
|
|
(isMemberPending(attributes, ourAci) ||
|
|
Boolean(
|
|
ourPni &&
|
|
!isMember(attributes, ourAci) &&
|
|
isMemberPending(attributes, ourPni)
|
|
)),
|
|
areWePendingApproval: Boolean(
|
|
ourConversationId &&
|
|
ourAci &&
|
|
isMemberAwaitingApproval(attributes, ourAci)
|
|
),
|
|
areWeAdmin: areWeAdmin(attributes),
|
|
avatars: getAvatarData(attributes),
|
|
badges: attributes.badges ?? EMPTY_ARRAY,
|
|
canChangeTimer: canChangeTimer(attributes),
|
|
canEditGroupInfo: canEditGroupInfo(attributes),
|
|
canAddNewMembers: canAddNewMembers(attributes),
|
|
avatarPath: getAbsoluteAvatarPath(attributes),
|
|
avatarHash: getAvatarHash(attributes),
|
|
unblurredAvatarPath: getAbsoluteUnblurredAvatarPath(attributes),
|
|
profileAvatarPath: getAbsoluteProfileAvatarPath(attributes),
|
|
color,
|
|
conversationColor: attributes.conversationColor,
|
|
customColor,
|
|
customColorId,
|
|
discoveredUnregisteredAt: attributes.discoveredUnregisteredAt,
|
|
draftBodyRanges: attributes.draftBodyRanges,
|
|
draftPreview,
|
|
draftText,
|
|
draftEditMessage,
|
|
familyName: attributes.profileFamilyName,
|
|
firstName: attributes.profileName,
|
|
groupDescription: attributes.description,
|
|
groupVersion,
|
|
groupId: attributes.groupId,
|
|
groupLink: buildGroupLink(attributes),
|
|
hideStory: Boolean(attributes.hideStory),
|
|
inboxPosition,
|
|
isArchived: attributes.isArchived,
|
|
isBlocked: isBlocked(attributes),
|
|
removalStage: attributes.removalStage,
|
|
isMe: isMe(attributes),
|
|
isGroupV1AndDisabled: isGroupV1(attributes),
|
|
isPinned: attributes.isPinned,
|
|
isUntrusted: model.isUntrusted(),
|
|
isVerified: model.isVerified(),
|
|
isFetchingUUID: model.isFetchingUUID,
|
|
lastMessage: getLastMessage(attributes),
|
|
lastUpdated: dropNull(timestamp),
|
|
left: Boolean(attributes.left),
|
|
markedUnread: attributes.markedUnread,
|
|
membersCount: getMembersCount(attributes),
|
|
memberships: getMemberships(attributes),
|
|
hasMessages: (attributes.messageCount ?? 0) > 0,
|
|
pendingMemberships: getPendingMemberships(attributes),
|
|
pendingApprovalMemberships: getPendingApprovalMemberships(attributes),
|
|
bannedMemberships: getBannedMemberships(attributes),
|
|
profileKey: attributes.profileKey,
|
|
accessControlAddFromInviteLink: attributes.accessControl?.addFromInviteLink,
|
|
accessControlAttributes: attributes.accessControl?.attributes,
|
|
accessControlMembers: attributes.accessControl?.members,
|
|
announcementsOnly: Boolean(attributes.announcementsOnly),
|
|
announcementsOnlyReady: canBeAnnouncementGroup(attributes),
|
|
expireTimer: attributes.expireTimer,
|
|
muteExpiresAt: attributes.muteExpiresAt,
|
|
dontNotifyForMentionsIfMuted: attributes.dontNotifyForMentionsIfMuted,
|
|
name: attributes.name,
|
|
systemGivenName: attributes.systemGivenName,
|
|
systemFamilyName: attributes.systemFamilyName,
|
|
systemNickname: attributes.systemNickname,
|
|
phoneNumber: getNumber(attributes),
|
|
profileName: getProfileName(attributes),
|
|
profileSharing: attributes.profileSharing,
|
|
profileLastUpdatedAt: attributes.profileLastUpdatedAt,
|
|
sharingPhoneNumber: attributes.sharingPhoneNumber,
|
|
publicParams: attributes.publicParams,
|
|
secretParams: attributes.secretParams,
|
|
shouldShowDraft,
|
|
sortedGroupMembers,
|
|
timestamp: dropNull(timestamp),
|
|
title: getTitle(attributes),
|
|
titleNoDefault: getTitleNoDefault(attributes),
|
|
typingContactIdTimestamps,
|
|
searchableTitle: isMe(attributes)
|
|
? window.i18n('icu:noteToSelf')
|
|
: getTitle(attributes),
|
|
unreadCount: attributes.unreadCount || 0,
|
|
unreadMentionsCount: attributes.unreadMentionsCount || 0,
|
|
...(isDirectConversation(attributes)
|
|
? {
|
|
type: 'direct' as const,
|
|
sharedGroupNames: attributes.sharedGroupNames || EMPTY_ARRAY,
|
|
}
|
|
: {
|
|
type: 'group' as const,
|
|
acknowledgedGroupNameCollisions:
|
|
attributes.acknowledgedGroupNameCollisions ||
|
|
EMPTY_GROUP_COLLISIONS,
|
|
sharedGroupNames: EMPTY_ARRAY,
|
|
storySendMode: attributes.storySendMode ?? StorySendMode.IfActive,
|
|
}),
|
|
voiceNotePlaybackRate: attributes.voiceNotePlaybackRate,
|
|
};
|
|
}
|