Moves conversation.getProps out of models
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
a0d7c36e3b
commit
7c1957c30d
21 changed files with 810 additions and 598 deletions
19
ts/groups.ts
19
ts/groups.ts
|
@ -68,6 +68,7 @@ import { getGroupSizeHardLimit } from './groups/limits';
|
||||||
import {
|
import {
|
||||||
isGroupV1 as getIsGroupV1,
|
isGroupV1 as getIsGroupV1,
|
||||||
isGroupV2 as getIsGroupV2,
|
isGroupV2 as getIsGroupV2,
|
||||||
|
isGroupV2,
|
||||||
isMe,
|
isMe,
|
||||||
} from './util/whatTypeOfConversation';
|
} from './util/whatTypeOfConversation';
|
||||||
import * as Bytes from './Bytes';
|
import * as Bytes from './Bytes';
|
||||||
|
@ -356,14 +357,20 @@ export async function getPreJoinGroupInfo(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildGroupLink(conversation: ConversationModel): string {
|
export function buildGroupLink(
|
||||||
const { masterKey, groupInviteLinkPassword } = conversation.attributes;
|
conversation: ConversationAttributesType
|
||||||
|
): string | undefined {
|
||||||
|
if (!isGroupV2(conversation)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { masterKey, groupInviteLinkPassword } = conversation;
|
||||||
|
|
||||||
|
if (!groupInviteLinkPassword) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
strictAssert(masterKey, 'buildGroupLink requires the master key!');
|
strictAssert(masterKey, 'buildGroupLink requires the master key!');
|
||||||
strictAssert(
|
|
||||||
groupInviteLinkPassword,
|
|
||||||
'buildGroupLink requires the groupInviteLinkPassword!'
|
|
||||||
);
|
|
||||||
|
|
||||||
const bytes = Proto.GroupInviteLink.encode({
|
const bytes = Proto.GroupInviteLink.encode({
|
||||||
v1Contents: {
|
v1Contents: {
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import {
|
import { compact, has, isNumber, throttle, debounce } from 'lodash';
|
||||||
compact,
|
|
||||||
has,
|
|
||||||
isNumber,
|
|
||||||
throttle,
|
|
||||||
debounce,
|
|
||||||
head,
|
|
||||||
sortBy,
|
|
||||||
} from 'lodash';
|
|
||||||
import { batch as batchDispatch } from 'react-redux';
|
import { batch as batchDispatch } from 'react-redux';
|
||||||
import { v4 as generateGuid } from 'uuid';
|
import { v4 as generateGuid } from 'uuid';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
|
@ -23,16 +15,21 @@ import type {
|
||||||
QuotedMessageType,
|
QuotedMessageType,
|
||||||
SenderKeyInfoType,
|
SenderKeyInfoType,
|
||||||
} from '../model-types.d';
|
} from '../model-types.d';
|
||||||
|
import { getConversation } from '../util/getConversation';
|
||||||
import { drop } from '../util/drop';
|
import { drop } from '../util/drop';
|
||||||
import { isShallowEqual } from '../util/isShallowEqual';
|
import { isShallowEqual } from '../util/isShallowEqual';
|
||||||
import { memoizeByThis } from '../util/memoizeByThis';
|
|
||||||
import { getInitials } from '../util/getInitials';
|
import { getInitials } from '../util/getInitials';
|
||||||
import { normalizeUuid } from '../util/normalizeUuid';
|
import { normalizeUuid } from '../util/normalizeUuid';
|
||||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
|
||||||
import type { AttachmentType, ThumbnailType } from '../types/Attachment';
|
import type { AttachmentType, ThumbnailType } from '../types/Attachment';
|
||||||
import { toDayMillis } from '../util/timestamp';
|
import { toDayMillis } from '../util/timestamp';
|
||||||
import { isVoiceMessage } from '../types/Attachment';
|
import { areWeAdmin } from '../util/areWeAdmin';
|
||||||
|
import { isBlocked } from '../util/isBlocked';
|
||||||
|
import { getAboutText } from '../util/getAboutText';
|
||||||
|
import { getAvatarPath } from '../util/avatarUtils';
|
||||||
|
import { getDraftPreview } from '../util/getDraftPreview';
|
||||||
|
import { hasDraft } from '../util/hasDraft';
|
||||||
import type { CallHistoryDetailsType } from '../types/Calling';
|
import type { CallHistoryDetailsType } from '../types/Calling';
|
||||||
import { CallMode } from '../types/Calling';
|
import { CallMode } from '../types/Calling';
|
||||||
import * as Conversation from '../types/Conversation';
|
import * as Conversation from '../types/Conversation';
|
||||||
|
@ -50,7 +47,6 @@ import type {
|
||||||
import type {
|
import type {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
DraftPreviewType,
|
DraftPreviewType,
|
||||||
LastMessageType,
|
|
||||||
} from '../state/ducks/conversations';
|
} from '../state/ducks/conversations';
|
||||||
import type {
|
import type {
|
||||||
AvatarColorType,
|
AvatarColorType,
|
||||||
|
@ -83,10 +79,9 @@ import {
|
||||||
} from '../Crypto';
|
} from '../Crypto';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
import type { DraftBodyRanges } from '../types/BodyRange';
|
import type { DraftBodyRanges } from '../types/BodyRange';
|
||||||
import { BodyRange, hydrateRanges } from '../types/BodyRange';
|
import { BodyRange } from '../types/BodyRange';
|
||||||
import { migrateColor } from '../util/migrateColor';
|
import { migrateColor } from '../util/migrateColor';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
import { dropNull } from '../util/dropNull';
|
|
||||||
import { notificationService } from '../services/notifications';
|
import { notificationService } from '../services/notifications';
|
||||||
import { storageServiceUploadJob } from '../services/storage';
|
import { storageServiceUploadJob } from '../services/storage';
|
||||||
import { scheduleOptimizeFTS } from '../services/ftsOptimizer';
|
import { scheduleOptimizeFTS } from '../services/ftsOptimizer';
|
||||||
|
@ -135,7 +130,6 @@ import type { ReactionModel } from '../messageModifiers/Reactions';
|
||||||
import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady';
|
import { isAnnouncementGroupReady } from '../util/isAnnouncementGroupReady';
|
||||||
import { getProfile } from '../util/getProfile';
|
import { getProfile } from '../util/getProfile';
|
||||||
import { SEALED_SENDER } from '../types/SealedSender';
|
import { SEALED_SENDER } from '../types/SealedSender';
|
||||||
import { getAvatarData } from '../util/getAvatarData';
|
|
||||||
import { createIdenticon } from '../util/createIdenticon';
|
import { createIdenticon } from '../util/createIdenticon';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import * as Errors from '../types/errors';
|
import * as Errors from '../types/errors';
|
||||||
|
@ -150,21 +144,21 @@ import { getSendTarget } from '../util/getSendTarget';
|
||||||
import { getRecipients } from '../util/getRecipients';
|
import { getRecipients } from '../util/getRecipients';
|
||||||
import { validateConversation } from '../util/validateConversation';
|
import { validateConversation } from '../util/validateConversation';
|
||||||
import { isSignalConversation } from '../util/isSignalConversation';
|
import { isSignalConversation } from '../util/isSignalConversation';
|
||||||
import { isMemberRequestingToJoin } from '../util/isMemberRequestingToJoin';
|
|
||||||
import { removePendingMember } from '../util/removePendingMember';
|
import { removePendingMember } from '../util/removePendingMember';
|
||||||
import { isMemberPending } from '../util/isMemberPending';
|
import {
|
||||||
|
isMember,
|
||||||
|
isMemberAwaitingApproval,
|
||||||
|
isMemberBanned,
|
||||||
|
isMemberPending,
|
||||||
|
isMemberRequestingToJoin,
|
||||||
|
} from '../util/groupMembershipUtils';
|
||||||
import { imageToBlurHash } from '../util/imageToBlurHash';
|
import { imageToBlurHash } from '../util/imageToBlurHash';
|
||||||
import { ReceiptType } from '../types/Receipt';
|
import { ReceiptType } from '../types/Receipt';
|
||||||
import { getQuoteAttachment } from '../util/makeQuote';
|
import { getQuoteAttachment } from '../util/makeQuote';
|
||||||
import { stripNewlinesForLeftPane } from '../util/stripNewlinesForLeftPane';
|
|
||||||
import { findAndFormatContact } from '../util/findAndFormatContact';
|
|
||||||
import { deriveProfileKeyVersion } from '../util/zkgroup';
|
import { deriveProfileKeyVersion } from '../util/zkgroup';
|
||||||
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||||
import { validateTransition } from '../util/callHistoryDetails';
|
import { validateTransition } from '../util/callHistoryDetails';
|
||||||
|
|
||||||
const EMPTY_ARRAY: Readonly<[]> = [];
|
|
||||||
const EMPTY_GROUP_COLLISIONS: GroupNameCollisionsWithIdsByTitle = {};
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
|
@ -260,7 +254,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
private cachedIdenticon?: CachedIdenticon;
|
private cachedIdenticon?: CachedIdenticon;
|
||||||
|
|
||||||
private isFetchingUUID?: boolean;
|
public isFetchingUUID?: boolean;
|
||||||
|
|
||||||
private lastIsTyping?: boolean;
|
private lastIsTyping?: boolean;
|
||||||
|
|
||||||
|
@ -465,45 +459,12 @@ export class ConversationModel extends window.Backbone
|
||||||
return isMemberPending(this.attributes, uuid);
|
return isMemberPending(this.attributes, uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private isMemberBanned(uuid: UUID): boolean {
|
|
||||||
if (!isGroupV2(this.attributes)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const bannedMembersV2 = this.get('bannedMembersV2');
|
|
||||||
|
|
||||||
if (!bannedMembersV2 || !bannedMembersV2.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bannedMembersV2.some(member => member.uuid === uuid.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
isMemberAwaitingApproval(uuid: UUID): boolean {
|
isMemberAwaitingApproval(uuid: UUID): boolean {
|
||||||
if (!isGroupV2(this.attributes)) {
|
return isMemberAwaitingApproval(this.attributes, uuid);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const pendingAdminApprovalV2 = this.get('pendingAdminApprovalV2');
|
|
||||||
|
|
||||||
if (!pendingAdminApprovalV2 || !pendingAdminApprovalV2.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pendingAdminApprovalV2.some(
|
|
||||||
member => member.uuid === uuid.toString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isMember(uuid: UUID): boolean {
|
isMember(uuid: UUID): boolean {
|
||||||
if (!isGroupV2(this.attributes)) {
|
return isMember(this.attributes, uuid);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const membersV2 = this.get('membersV2');
|
|
||||||
|
|
||||||
if (!membersV2 || !membersV2.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return membersV2.some(item => item.uuid === uuid.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateExpirationTimerInGroupV2(
|
async updateExpirationTimerInGroupV2(
|
||||||
|
@ -898,22 +859,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
isBlocked(): boolean {
|
isBlocked(): boolean {
|
||||||
const uuid = this.get('uuid');
|
return isBlocked(this.attributes);
|
||||||
if (uuid) {
|
|
||||||
return window.storage.blocked.isUuidBlocked(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
const e164 = this.get('e164');
|
|
||||||
if (e164) {
|
|
||||||
return window.storage.blocked.isBlocked(e164);
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupId = this.get('groupId');
|
|
||||||
if (groupId) {
|
|
||||||
return window.storage.blocked.isGroupBlocked(groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
block({ viaStorageServiceSync = false } = {}): void {
|
block({ viaStorageServiceSync = false } = {}): void {
|
||||||
|
@ -1090,49 +1036,11 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
hasDraft(): boolean {
|
hasDraft(): boolean {
|
||||||
const draftAttachments = this.get('draftAttachments') || [];
|
return hasDraft(this.attributes);
|
||||||
|
|
||||||
return (this.get('draft') ||
|
|
||||||
this.get('quotedMessageId') ||
|
|
||||||
draftAttachments.length > 0) as boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getDraftPreview(): DraftPreviewType {
|
getDraftPreview(): DraftPreviewType {
|
||||||
const draft = this.get('draft');
|
return getDraftPreview(this.attributes);
|
||||||
|
|
||||||
const rawBodyRanges = this.get('draftBodyRanges') || [];
|
|
||||||
const bodyRanges = hydrateRanges(rawBodyRanges, findAndFormatContact);
|
|
||||||
|
|
||||||
if (draft) {
|
|
||||||
return {
|
|
||||||
text: stripNewlinesForLeftPane(draft),
|
|
||||||
bodyRanges,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const draftAttachments = this.get('draftAttachments') || [];
|
|
||||||
if (draftAttachments.length > 0) {
|
|
||||||
if (isVoiceMessage(draftAttachments[0])) {
|
|
||||||
return {
|
|
||||||
text: window.i18n('icu:message--getNotificationText--voice-message'),
|
|
||||||
prefix: '🎤',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
text: window.i18n('icu:Conversation--getDraftPreview--attachment'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const quotedMessageId = this.get('quotedMessageId');
|
|
||||||
if (quotedMessageId) {
|
|
||||||
return {
|
|
||||||
text: window.i18n('icu:Conversation--getDraftPreview--quote'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: window.i18n('icu:Conversation--getDraftPreview--draft'),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bumpTyping(): void {
|
bumpTyping(): void {
|
||||||
|
@ -1891,7 +1799,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { oldCachedProps } = this;
|
const { oldCachedProps } = this;
|
||||||
const newCachedProps = this.getProps();
|
const newCachedProps = getConversation(this);
|
||||||
|
|
||||||
if (oldCachedProps && isShallowEqual(oldCachedProps, newCachedProps)) {
|
if (oldCachedProps && isShallowEqual(oldCachedProps, newCachedProps)) {
|
||||||
this.cachedProps = oldCachedProps;
|
this.cachedProps = oldCachedProps;
|
||||||
|
@ -1905,179 +1813,6 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
|
||||||
private getProps(): ConversationType {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const color = this.getColor()!;
|
|
||||||
|
|
||||||
const typingValues = Object.values(this.contactTypingTimers || {});
|
|
||||||
const typingMostRecent = head(sortBy(typingValues, 'timestamp'));
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const timestamp = this.get('timestamp')!;
|
|
||||||
const draftTimestamp = this.get('draftTimestamp');
|
|
||||||
const draftPreview = this.getDraftPreview();
|
|
||||||
const draftText = dropNull(this.get('draft'));
|
|
||||||
const draftEditMessage = this.get('draftEditMessage');
|
|
||||||
const shouldShowDraft = Boolean(
|
|
||||||
this.hasDraft() && draftTimestamp && draftTimestamp >= timestamp
|
|
||||||
);
|
|
||||||
const inboxPosition = this.get('inbox_position');
|
|
||||||
const messageRequestsEnabled = window.Signal.RemoteConfig.isEnabled(
|
|
||||||
'desktop.messageRequests'
|
|
||||||
);
|
|
||||||
const ourConversationId =
|
|
||||||
window.ConversationController.getOurConversationId();
|
|
||||||
|
|
||||||
let groupVersion: undefined | 1 | 2;
|
|
||||||
if (isGroupV1(this.attributes)) {
|
|
||||||
groupVersion = 1;
|
|
||||||
} else if (isGroupV2(this.attributes)) {
|
|
||||||
groupVersion = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedGroupMembers = isGroupV2(this.attributes)
|
|
||||||
? this.getMembers()
|
|
||||||
.sort((left, right) =>
|
|
||||||
sortConversationTitles(left, right, this.intlCollator)
|
|
||||||
)
|
|
||||||
.map(member => member.format())
|
|
||||||
.filter(isNotNil)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const { customColor, customColorId } = this.getCustomColorData();
|
|
||||||
|
|
||||||
const ourACI = window.textsecure.storage.user.getUuid(UUIDKind.ACI);
|
|
||||||
const ourPNI = window.textsecure.storage.user.getUuid(UUIDKind.PNI);
|
|
||||||
|
|
||||||
// TODO: DESKTOP-720
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
uuid: this.get('uuid'),
|
|
||||||
pni: this.get('pni'),
|
|
||||||
e164: this.get('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(this.attributes, ourConversationId)
|
|
||||||
? dropNull(this.get('username'))
|
|
||||||
: undefined,
|
|
||||||
|
|
||||||
about: this.getAboutText(),
|
|
||||||
aboutText: this.get('about'),
|
|
||||||
aboutEmoji: this.get('aboutEmoji'),
|
|
||||||
acceptedMessageRequest: this.getAccepted(),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
activeAt: this.get('active_at')!,
|
|
||||||
areWePending:
|
|
||||||
ourACI &&
|
|
||||||
(this.isMemberPending(ourACI) ||
|
|
||||||
Boolean(
|
|
||||||
ourPNI && !this.isMember(ourACI) && this.isMemberPending(ourPNI)
|
|
||||||
)),
|
|
||||||
areWePendingApproval: Boolean(
|
|
||||||
ourConversationId && ourACI && this.isMemberAwaitingApproval(ourACI)
|
|
||||||
),
|
|
||||||
areWeAdmin: this.areWeAdmin(),
|
|
||||||
avatars: getAvatarData(this.attributes),
|
|
||||||
badges: this.get('badges') ?? EMPTY_ARRAY,
|
|
||||||
canChangeTimer: this.canChangeTimer(),
|
|
||||||
canEditGroupInfo: this.canEditGroupInfo(),
|
|
||||||
canAddNewMembers: this.canAddNewMembers(),
|
|
||||||
avatarPath: this.getAbsoluteAvatarPath(),
|
|
||||||
avatarHash: this.getAvatarHash(),
|
|
||||||
unblurredAvatarPath: this.getAbsoluteUnblurredAvatarPath(),
|
|
||||||
profileAvatarPath: this.getAbsoluteProfileAvatarPath(),
|
|
||||||
color,
|
|
||||||
conversationColor: this.getConversationColor(),
|
|
||||||
customColor,
|
|
||||||
customColorId,
|
|
||||||
discoveredUnregisteredAt: this.get('discoveredUnregisteredAt'),
|
|
||||||
draftBodyRanges: this.getDraftBodyRanges(),
|
|
||||||
draftPreview,
|
|
||||||
draftText,
|
|
||||||
draftEditMessage,
|
|
||||||
familyName: this.get('profileFamilyName'),
|
|
||||||
firstName: this.get('profileName'),
|
|
||||||
groupDescription: this.get('description'),
|
|
||||||
groupVersion,
|
|
||||||
groupId: this.get('groupId'),
|
|
||||||
groupLink: this.getGroupLink(),
|
|
||||||
hideStory: Boolean(this.get('hideStory')),
|
|
||||||
inboxPosition,
|
|
||||||
isArchived: this.get('isArchived'),
|
|
||||||
isBlocked: this.isBlocked(),
|
|
||||||
removalStage: this.get('removalStage'),
|
|
||||||
isMe: isMe(this.attributes),
|
|
||||||
isGroupV1AndDisabled: this.isGroupV1AndDisabled(),
|
|
||||||
isPinned: this.get('isPinned'),
|
|
||||||
isUntrusted: this.isUntrusted(),
|
|
||||||
isVerified: this.isVerified(),
|
|
||||||
isFetchingUUID: this.isFetchingUUID,
|
|
||||||
lastMessage: this.getLastMessage(),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
lastUpdated: this.get('timestamp')!,
|
|
||||||
left: Boolean(this.get('left')),
|
|
||||||
markedUnread: this.get('markedUnread'),
|
|
||||||
membersCount: this.getMembersCount(),
|
|
||||||
memberships: this.getMemberships(),
|
|
||||||
hasMessages: (this.get('messageCount') ?? 0) > 0,
|
|
||||||
pendingMemberships: this.getPendingMemberships(),
|
|
||||||
pendingApprovalMemberships: this.getPendingApprovalMemberships(),
|
|
||||||
bannedMemberships: this.getBannedMemberships(),
|
|
||||||
profileKey: this.get('profileKey'),
|
|
||||||
messageRequestsEnabled,
|
|
||||||
accessControlAddFromInviteLink:
|
|
||||||
this.get('accessControl')?.addFromInviteLink,
|
|
||||||
accessControlAttributes: this.get('accessControl')?.attributes,
|
|
||||||
accessControlMembers: this.get('accessControl')?.members,
|
|
||||||
announcementsOnly: Boolean(this.get('announcementsOnly')),
|
|
||||||
announcementsOnlyReady: this.canBeAnnouncementGroup(),
|
|
||||||
expireTimer: this.get('expireTimer'),
|
|
||||||
muteExpiresAt: this.get('muteExpiresAt'),
|
|
||||||
dontNotifyForMentionsIfMuted: this.get('dontNotifyForMentionsIfMuted'),
|
|
||||||
name: this.get('name'),
|
|
||||||
systemGivenName: this.get('systemGivenName'),
|
|
||||||
systemFamilyName: this.get('systemFamilyName'),
|
|
||||||
systemNickname: this.get('systemNickname'),
|
|
||||||
phoneNumber: this.getNumber(),
|
|
||||||
profileName: this.getProfileName(),
|
|
||||||
profileSharing: this.get('profileSharing'),
|
|
||||||
publicParams: this.get('publicParams'),
|
|
||||||
secretParams: this.get('secretParams'),
|
|
||||||
shouldShowDraft,
|
|
||||||
sortedGroupMembers,
|
|
||||||
timestamp,
|
|
||||||
title: this.getTitle(),
|
|
||||||
titleNoDefault: this.getTitleNoDefault(),
|
|
||||||
typingContactId: typingMostRecent?.senderId,
|
|
||||||
searchableTitle: isMe(this.attributes)
|
|
||||||
? window.i18n('icu:noteToSelf')
|
|
||||||
: this.getTitle(),
|
|
||||||
unreadCount: this.get('unreadCount') || 0,
|
|
||||||
unreadMentionsCount: this.get('unreadMentionsCount'),
|
|
||||||
...(isDirectConversation(this.attributes)
|
|
||||||
? {
|
|
||||||
type: 'direct' as const,
|
|
||||||
sharedGroupNames: this.get('sharedGroupNames') || EMPTY_ARRAY,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: 'group' as const,
|
|
||||||
acknowledgedGroupNameCollisions:
|
|
||||||
this.get('acknowledgedGroupNameCollisions') ||
|
|
||||||
EMPTY_GROUP_COLLISIONS,
|
|
||||||
sharedGroupNames: EMPTY_ARRAY,
|
|
||||||
storySendMode: this.getGroupStorySendMode(),
|
|
||||||
}),
|
|
||||||
voiceNotePlaybackRate: this.get('voiceNotePlaybackRate'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateE164(e164?: string | null): void {
|
updateE164(e164?: string | null): void {
|
||||||
const oldValue = this.get('e164');
|
const oldValue = this.get('e164');
|
||||||
if (e164 === oldValue) {
|
if (e164 === oldValue) {
|
||||||
|
@ -2246,26 +1981,6 @@ export class ConversationModel extends window.Backbone
|
||||||
window.Signal.Data.updateConversation(this.attributes);
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMembersCount(): number | undefined {
|
|
||||||
if (isDirectConversation(this.attributes)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberList = this.get('membersV2') || this.get('members');
|
|
||||||
|
|
||||||
// We'll fail over if the member list is empty
|
|
||||||
if (memberList && memberList.length) {
|
|
||||||
return memberList.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
const temporaryMemberCount = this.get('temporaryMemberCount');
|
|
||||||
if (isNumber(temporaryMemberCount)) {
|
|
||||||
return temporaryMemberCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
incrementSentMessageCount({ dry = false }: { dry?: boolean } = {}):
|
incrementSentMessageCount({ dry = false }: { dry?: boolean } = {}):
|
||||||
| Partial<ConversationAttributesType>
|
| Partial<ConversationAttributesType>
|
||||||
| undefined {
|
| undefined {
|
||||||
|
@ -2627,7 +2342,7 @@ export class ConversationModel extends window.Backbone
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isMemberBanned(uuid)) {
|
if (isMemberBanned(this.attributes, uuid)) {
|
||||||
log.warn('addBannedMember: Member is already banned!');
|
log.warn('addBannedMember: Member is already banned!');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -3037,21 +2752,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
getAboutText(): string | undefined {
|
getAboutText(): string | undefined {
|
||||||
if (!this.get('about')) {
|
return getAboutText(this.attributes);
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emoji = this.get('aboutEmoji');
|
|
||||||
const text = this.get('about');
|
|
||||||
|
|
||||||
if (!emoji) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
return window.i18n('icu:message--getNotificationText--text-with-emoji', {
|
|
||||||
text,
|
|
||||||
emoji,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3859,54 +3560,6 @@ export class ConversationModel extends window.Backbone
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDraftBodyRanges = memoizeByThis(
|
|
||||||
(): DraftBodyRanges | undefined => {
|
|
||||||
return this.get('draftBodyRanges');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private getLastMessage = memoizeByThis((): LastMessageType | undefined => {
|
|
||||||
if (this.get('lastMessageDeletedForEveryone')) {
|
|
||||||
return { deletedForEveryone: true };
|
|
||||||
}
|
|
||||||
const lastMessageText = this.get('lastMessage');
|
|
||||||
if (!lastMessageText) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawBodyRanges = this.get('lastMessageBodyRanges') || [];
|
|
||||||
const bodyRanges = hydrateRanges(rawBodyRanges, findAndFormatContact);
|
|
||||||
|
|
||||||
const text = stripNewlinesForLeftPane(lastMessageText);
|
|
||||||
const prefix = this.get('lastMessagePrefix');
|
|
||||||
|
|
||||||
return {
|
|
||||||
author: dropNull(this.get('lastMessageAuthor')),
|
|
||||||
bodyRanges,
|
|
||||||
deletedForEveryone: false,
|
|
||||||
prefix,
|
|
||||||
status: dropNull(this.get('lastMessageStatus')),
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
private getMemberships = memoizeByThis(
|
|
||||||
(): ReadonlyArray<{
|
|
||||||
uuid: UUIDStringType;
|
|
||||||
isAdmin: boolean;
|
|
||||||
}> => {
|
|
||||||
if (!isGroupV2(this.attributes)) {
|
|
||||||
return EMPTY_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
const members = this.get('membersV2') || [];
|
|
||||||
return members.map(member => ({
|
|
||||||
isAdmin: member.role === Proto.Member.Role.ADMINISTRATOR,
|
|
||||||
uuid: member.uuid,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
getGroupLink(): string | undefined {
|
getGroupLink(): string | undefined {
|
||||||
if (!isGroupV2(this.attributes)) {
|
if (!isGroupV2(this.attributes)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -3916,49 +3569,9 @@ export class ConversationModel extends window.Backbone
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.Signal.Groups.buildGroupLink(this);
|
return window.Signal.Groups.buildGroupLink(this.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPendingMemberships = memoizeByThis(
|
|
||||||
(): ReadonlyArray<{
|
|
||||||
addedByUserId?: UUIDStringType;
|
|
||||||
uuid: UUIDStringType;
|
|
||||||
}> => {
|
|
||||||
if (!isGroupV2(this.attributes)) {
|
|
||||||
return EMPTY_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
const members = this.get('pendingMembersV2') || [];
|
|
||||||
return members.map(member => ({
|
|
||||||
addedByUserId: member.addedByUserId,
|
|
||||||
uuid: member.uuid,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private getPendingApprovalMemberships = memoizeByThis(
|
|
||||||
(): ReadonlyArray<{ uuid: UUIDStringType }> => {
|
|
||||||
if (!isGroupV2(this.attributes)) {
|
|
||||||
return EMPTY_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
const members = this.get('pendingAdminApprovalV2') || [];
|
|
||||||
return members.map(member => ({
|
|
||||||
uuid: member.uuid,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private getBannedMemberships = memoizeByThis(
|
|
||||||
(): ReadonlyArray<UUIDStringType> => {
|
|
||||||
if (!isGroupV2(this.attributes)) {
|
|
||||||
return EMPTY_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (this.get('bannedMembersV2') || []).map(member => member.uuid);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
getMembers(
|
getMembers(
|
||||||
options: { includePendingMembers?: boolean } = {}
|
options: { includePendingMembers?: boolean } = {}
|
||||||
): Array<ConversationModel> {
|
): Array<ConversationModel> {
|
||||||
|
@ -5324,45 +4937,8 @@ export class ConversationModel extends window.Backbone
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAvatarPath(): undefined | string {
|
|
||||||
const shouldShowProfileAvatar =
|
|
||||||
isMe(this.attributes) ||
|
|
||||||
window.storage.get('preferContactAvatars') === false;
|
|
||||||
const avatar = shouldShowProfileAvatar
|
|
||||||
? this.get('profileAvatar') || this.get('avatar')
|
|
||||||
: this.get('avatar') || this.get('profileAvatar');
|
|
||||||
return avatar?.path || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAvatarHash(): undefined | string {
|
|
||||||
const avatar = isMe(this.attributes)
|
|
||||||
? this.get('profileAvatar') || this.get('avatar')
|
|
||||||
: this.get('avatar') || this.get('profileAvatar');
|
|
||||||
return avatar?.hash || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAbsoluteAvatarPath(): string | undefined {
|
|
||||||
const avatarPath = this.getAvatarPath();
|
|
||||||
if (isSignalConversation(this.attributes)) {
|
|
||||||
return avatarPath;
|
|
||||||
}
|
|
||||||
return avatarPath ? getAbsoluteAttachmentPath(avatarPath) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAbsoluteProfileAvatarPath(): string | undefined {
|
|
||||||
const avatarPath = this.get('profileAvatar')?.path;
|
|
||||||
return avatarPath ? getAbsoluteAttachmentPath(avatarPath) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
getAbsoluteUnblurredAvatarPath(): string | undefined {
|
|
||||||
const unblurredAvatarPath = this.get('unblurredAvatarPath');
|
|
||||||
return unblurredAvatarPath
|
|
||||||
? getAbsoluteAttachmentPath(unblurredAvatarPath)
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
unblurAvatar(): void {
|
unblurAvatar(): void {
|
||||||
const avatarPath = this.getAvatarPath();
|
const avatarPath = getAvatarPath(this.attributes);
|
||||||
if (avatarPath) {
|
if (avatarPath) {
|
||||||
this.set('unblurredAvatarPath', avatarPath);
|
this.set('unblurredAvatarPath', avatarPath);
|
||||||
} else {
|
} else {
|
||||||
|
@ -5370,78 +4946,8 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private canChangeTimer(): boolean {
|
|
||||||
if (isDirectConversation(this.attributes)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isGroupV1AndDisabled()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isGroupV2(this.attributes)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessControlEnum = Proto.AccessControl.AccessRequired;
|
|
||||||
const accessControl = this.get('accessControl');
|
|
||||||
const canAnyoneChangeTimer =
|
|
||||||
accessControl &&
|
|
||||||
(accessControl.attributes === accessControlEnum.ANY ||
|
|
||||||
accessControl.attributes === accessControlEnum.MEMBER);
|
|
||||||
if (canAnyoneChangeTimer) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.areWeAdmin();
|
|
||||||
}
|
|
||||||
|
|
||||||
canEditGroupInfo(): boolean {
|
|
||||||
if (!isGroupV2(this.attributes)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.get('left')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
this.areWeAdmin() ||
|
|
||||||
this.get('accessControl')?.attributes ===
|
|
||||||
Proto.AccessControl.AccessRequired.MEMBER
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
canAddNewMembers(): boolean {
|
|
||||||
if (!isGroupV2(this.attributes)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.get('left')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
this.areWeAdmin() ||
|
|
||||||
this.get('accessControl')?.members ===
|
|
||||||
Proto.AccessControl.AccessRequired.MEMBER
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
areWeAdmin(): boolean {
|
areWeAdmin(): boolean {
|
||||||
if (!isGroupV2(this.attributes)) {
|
return areWeAdmin(this.attributes);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberEnum = Proto.Member.Role;
|
|
||||||
const members = this.get('membersV2') || [];
|
|
||||||
const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
|
|
||||||
const me = members.find(item => item.uuid === ourUuid);
|
|
||||||
if (!me) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return me.role === memberEnum.ADMINISTRATOR;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set of items to captureChanges on:
|
// Set of items to captureChanges on:
|
||||||
|
@ -5582,7 +5088,7 @@ export class ConversationModel extends window.Backbone
|
||||||
});
|
});
|
||||||
|
|
||||||
let notificationIconUrl;
|
let notificationIconUrl;
|
||||||
const avatarPath = this.getAvatarPath();
|
const avatarPath = getAvatarPath(this.attributes);
|
||||||
if (avatarPath) {
|
if (avatarPath) {
|
||||||
notificationIconUrl = getAbsoluteAttachmentPath(avatarPath);
|
notificationIconUrl = getAbsoluteAttachmentPath(avatarPath);
|
||||||
} else if (isMessageInDirectConversation) {
|
} else if (isMessageInDirectConversation) {
|
||||||
|
@ -6034,15 +5540,3 @@ window.Whisper.ConversationCollection = window.Backbone.Collection.extend({
|
||||||
return -(m.get('active_at') || 0);
|
return -(m.get('active_at') || 0);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type SortableByTitle = {
|
|
||||||
getTitle: () => string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortConversationTitles = (
|
|
||||||
left: SortableByTitle,
|
|
||||||
right: SortableByTitle,
|
|
||||||
collator: Intl.Collator
|
|
||||||
) => {
|
|
||||||
return collator.compare(left.getTitle(), right.getTitle());
|
|
||||||
};
|
|
||||||
|
|
|
@ -117,7 +117,7 @@ import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMe
|
||||||
import type { ShowToastActionType } from './toast';
|
import type { ShowToastActionType } from './toast';
|
||||||
import { SHOW_TOAST } from './toast';
|
import { SHOW_TOAST } from './toast';
|
||||||
import { ToastType } from '../../types/Toast';
|
import { ToastType } from '../../types/Toast';
|
||||||
import { isMemberRequestingToJoin } from '../../util/isMemberRequestingToJoin';
|
import { isMemberRequestingToJoin } from '../../util/groupMembershipUtils';
|
||||||
import { removePendingMember } from '../../util/removePendingMember';
|
import { removePendingMember } from '../../util/removePendingMember';
|
||||||
import { denyPendingApprovalRequest } from '../../util/denyPendingApprovalRequest';
|
import { denyPendingApprovalRequest } from '../../util/denyPendingApprovalRequest';
|
||||||
import { SignalService as Proto } from '../../protobuf';
|
import { SignalService as Proto } from '../../protobuf';
|
||||||
|
|
27
ts/util/areWeAdmin.ts
Normal file
27
ts/util/areWeAdmin.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types';
|
||||||
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
import { isGroupV2 } from './whatTypeOfConversation';
|
||||||
|
|
||||||
|
export function areWeAdmin(
|
||||||
|
attributes: Pick<
|
||||||
|
ConversationAttributesType,
|
||||||
|
'groupId' | 'groupVersion' | 'membersV2'
|
||||||
|
>
|
||||||
|
): boolean {
|
||||||
|
if (!isGroupV2(attributes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberEnum = Proto.Member.Role;
|
||||||
|
const members = attributes.membersV2 || [];
|
||||||
|
const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
|
||||||
|
const me = members.find(item => item.uuid === ourUuid);
|
||||||
|
if (!me) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return me.role === memberEnum.ADMINISTRATOR;
|
||||||
|
}
|
56
ts/util/avatarUtils.ts
Normal file
56
ts/util/avatarUtils.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import { isMe } from './whatTypeOfConversation';
|
||||||
|
import { isSignalConversation } from './isSignalConversation';
|
||||||
|
|
||||||
|
export function getAvatarHash(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): undefined | string {
|
||||||
|
const avatar = isMe(conversationAttrs)
|
||||||
|
? conversationAttrs.profileAvatar || conversationAttrs.avatar
|
||||||
|
: conversationAttrs.avatar || conversationAttrs.profileAvatar;
|
||||||
|
return avatar?.hash || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvatarPath(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): undefined | string {
|
||||||
|
const shouldShowProfileAvatar =
|
||||||
|
isMe(conversationAttrs) ||
|
||||||
|
window.storage.get('preferContactAvatars') === false;
|
||||||
|
const avatar = shouldShowProfileAvatar
|
||||||
|
? conversationAttrs.profileAvatar || conversationAttrs.avatar
|
||||||
|
: conversationAttrs.avatar || conversationAttrs.profileAvatar;
|
||||||
|
return avatar?.path || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAbsoluteAvatarPath(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): string | undefined {
|
||||||
|
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
|
||||||
|
const avatarPath = getAvatarPath(conversationAttrs);
|
||||||
|
if (isSignalConversation(conversationAttrs)) {
|
||||||
|
return avatarPath;
|
||||||
|
}
|
||||||
|
return avatarPath ? getAbsoluteAttachmentPath(avatarPath) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAbsoluteProfileAvatarPath(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): string | undefined {
|
||||||
|
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
|
||||||
|
const avatarPath = conversationAttrs.profileAvatar?.path;
|
||||||
|
return avatarPath ? getAbsoluteAttachmentPath(avatarPath) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAbsoluteUnblurredAvatarPath(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): string | undefined {
|
||||||
|
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
|
||||||
|
const { unblurredAvatarPath } = conversationAttrs;
|
||||||
|
return unblurredAvatarPath
|
||||||
|
? getAbsoluteAttachmentPath(unblurredAvatarPath)
|
||||||
|
: undefined;
|
||||||
|
}
|
25
ts/util/canAddNewMembers.ts
Normal file
25
ts/util/canAddNewMembers.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
import { isGroupV2 } from './whatTypeOfConversation';
|
||||||
|
import { areWeAdmin } from './areWeAdmin';
|
||||||
|
|
||||||
|
export function canAddNewMembers(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): boolean {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversationAttrs.left) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
areWeAdmin(conversationAttrs) ||
|
||||||
|
conversationAttrs.accessControl?.members ===
|
||||||
|
Proto.AccessControl.AccessRequired.MEMBER
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,25 +1,23 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { UUID } from '../types/UUID';
|
|
||||||
import type { ConversationAttributesType } from '../model-types.d';
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import { isAnnouncementGroupReady } from './isAnnouncementGroupReady';
|
||||||
import { isGroupV2 } from './whatTypeOfConversation';
|
import { isGroupV2 } from './whatTypeOfConversation';
|
||||||
|
|
||||||
export function isMemberRequestingToJoin(
|
export function canBeAnnouncementGroup(
|
||||||
conversationAttrs: Pick<
|
conversationAttrs: Pick<
|
||||||
ConversationAttributesType,
|
ConversationAttributesType,
|
||||||
'groupId' | 'groupVersion' | 'pendingAdminApprovalV2'
|
'groupId' | 'groupVersion' | 'pendingAdminApprovalV2'
|
||||||
>,
|
>
|
||||||
uuid: UUID
|
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!isGroupV2(conversationAttrs)) {
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const { pendingAdminApprovalV2 } = conversationAttrs;
|
|
||||||
|
|
||||||
if (!pendingAdminApprovalV2 || !pendingAdminApprovalV2.length) {
|
if (!isAnnouncementGroupReady()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pendingAdminApprovalV2.some(item => item.uuid === uuid.toString());
|
return true;
|
||||||
}
|
}
|
39
ts/util/canChangeTimer.ts
Normal file
39
ts/util/canChangeTimer.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types';
|
||||||
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
import { areWeAdmin } from './areWeAdmin';
|
||||||
|
import {
|
||||||
|
isDirectConversation,
|
||||||
|
isGroupV1,
|
||||||
|
isGroupV2,
|
||||||
|
} from './whatTypeOfConversation';
|
||||||
|
|
||||||
|
export function canChangeTimer(
|
||||||
|
attributes: ConversationAttributesType
|
||||||
|
): boolean {
|
||||||
|
if (isDirectConversation(attributes)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGroupV1(attributes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGroupV2(attributes)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessControlEnum = Proto.AccessControl.AccessRequired;
|
||||||
|
const { accessControl } = attributes;
|
||||||
|
const canAnyoneChangeTimer =
|
||||||
|
accessControl &&
|
||||||
|
(accessControl.attributes === accessControlEnum.ANY ||
|
||||||
|
accessControl.attributes === accessControlEnum.MEMBER);
|
||||||
|
if (canAnyoneChangeTimer) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return areWeAdmin(attributes);
|
||||||
|
}
|
25
ts/util/canEditGroupInfo.ts
Normal file
25
ts/util/canEditGroupInfo.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
import { isGroupV2 } from './whatTypeOfConversation';
|
||||||
|
import { areWeAdmin } from './areWeAdmin';
|
||||||
|
|
||||||
|
export function canEditGroupInfo(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): boolean {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversationAttrs.left) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
areWeAdmin(conversationAttrs) ||
|
||||||
|
conversationAttrs.accessControl?.attributes ===
|
||||||
|
Proto.AccessControl.AccessRequired.MEMBER
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import type { UUID } from '../types/UUID';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { UUIDKind } from '../types/UUID';
|
import { UUIDKind } from '../types/UUID';
|
||||||
import { getConversationIdForLogging } from './idForLogging';
|
import { getConversationIdForLogging } from './idForLogging';
|
||||||
import { isMemberRequestingToJoin } from './isMemberRequestingToJoin';
|
import { isMemberRequestingToJoin } from './groupMembershipUtils';
|
||||||
|
|
||||||
export async function denyPendingApprovalRequest(
|
export async function denyPendingApprovalRequest(
|
||||||
conversationAttributes: ConversationAttributesType,
|
conversationAttributes: ConversationAttributesType,
|
||||||
|
|
24
ts/util/getAboutText.ts
Normal file
24
ts/util/getAboutText.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types';
|
||||||
|
|
||||||
|
export function getAboutText(
|
||||||
|
attributes: ConversationAttributesType
|
||||||
|
): string | undefined {
|
||||||
|
if (!attributes.about) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emoji = attributes.aboutEmoji;
|
||||||
|
const text = attributes.about;
|
||||||
|
|
||||||
|
if (!emoji) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.i18n('icu:message--getNotificationText--text-with-emoji', {
|
||||||
|
text,
|
||||||
|
emoji,
|
||||||
|
});
|
||||||
|
}
|
240
ts/util/getConversation.ts
Normal file
240
ts/util/getConversation.ts
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import memoizee from 'memoizee';
|
||||||
|
import { head, 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 { UUIDKind } from '../types/UUID';
|
||||||
|
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 = Object.values(model.contactTypingTimers || {});
|
||||||
|
const typingMostRecent = head(sortBy(typingValues, 'timestamp'));
|
||||||
|
|
||||||
|
const ourACI = window.textsecure.storage.user.getUuid(UUIDKind.ACI);
|
||||||
|
const ourPNI = window.textsecure.storage.user.getUuid(UUIDKind.PNI);
|
||||||
|
|
||||||
|
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 messageRequestsEnabled = window.Signal.RemoteConfig.isEnabled(
|
||||||
|
'desktop.messageRequests'
|
||||||
|
);
|
||||||
|
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,
|
||||||
|
uuid: attributes.uuid,
|
||||||
|
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,
|
||||||
|
messageRequestsEnabled,
|
||||||
|
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,
|
||||||
|
publicParams: attributes.publicParams,
|
||||||
|
secretParams: attributes.secretParams,
|
||||||
|
shouldShowDraft,
|
||||||
|
sortedGroupMembers,
|
||||||
|
timestamp: dropNull(timestamp),
|
||||||
|
title: getTitle(attributes),
|
||||||
|
titleNoDefault: getTitleNoDefault(attributes),
|
||||||
|
typingContactId: typingMostRecent?.senderId,
|
||||||
|
searchableTitle: isMe(attributes)
|
||||||
|
? window.i18n('icu:noteToSelf')
|
||||||
|
: getTitle(attributes),
|
||||||
|
unreadCount: attributes.unreadCount || 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,
|
||||||
|
};
|
||||||
|
}
|
49
ts/util/getDraftPreview.ts
Normal file
49
ts/util/getDraftPreview.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types';
|
||||||
|
import type { DraftPreviewType } from '../state/ducks/conversations';
|
||||||
|
import { findAndFormatContact } from './findAndFormatContact';
|
||||||
|
import { hydrateRanges } from '../types/BodyRange';
|
||||||
|
import { isVoiceMessage } from '../types/Attachment';
|
||||||
|
import { stripNewlinesForLeftPane } from './stripNewlinesForLeftPane';
|
||||||
|
|
||||||
|
export function getDraftPreview(
|
||||||
|
attributes: ConversationAttributesType
|
||||||
|
): DraftPreviewType {
|
||||||
|
const { draft } = attributes;
|
||||||
|
|
||||||
|
const rawBodyRanges = attributes.draftBodyRanges || [];
|
||||||
|
const bodyRanges = hydrateRanges(rawBodyRanges, findAndFormatContact);
|
||||||
|
|
||||||
|
if (draft) {
|
||||||
|
return {
|
||||||
|
text: stripNewlinesForLeftPane(draft),
|
||||||
|
bodyRanges,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const draftAttachments = attributes.draftAttachments || [];
|
||||||
|
if (draftAttachments.length > 0) {
|
||||||
|
if (isVoiceMessage(draftAttachments[0])) {
|
||||||
|
return {
|
||||||
|
text: window.i18n('icu:message--getNotificationText--voice-message'),
|
||||||
|
prefix: '🎤',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: window.i18n('icu:Conversation--getDraftPreview--attachment'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { quotedMessageId } = attributes;
|
||||||
|
if (quotedMessageId) {
|
||||||
|
return {
|
||||||
|
text: window.i18n('icu:Conversation--getDraftPreview--quote'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: window.i18n('icu:Conversation--getDraftPreview--draft'),
|
||||||
|
};
|
||||||
|
}
|
36
ts/util/getLastMessage.ts
Normal file
36
ts/util/getLastMessage.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import type { LastMessageType } from '../state/ducks/conversations';
|
||||||
|
import { dropNull } from './dropNull';
|
||||||
|
import { findAndFormatContact } from './findAndFormatContact';
|
||||||
|
import { hydrateRanges } from '../types/BodyRange';
|
||||||
|
import { stripNewlinesForLeftPane } from './stripNewlinesForLeftPane';
|
||||||
|
|
||||||
|
export function getLastMessage(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): LastMessageType | undefined {
|
||||||
|
if (conversationAttrs.lastMessageDeletedForEveryone) {
|
||||||
|
return { deletedForEveryone: true };
|
||||||
|
}
|
||||||
|
const lastMessageText = conversationAttrs.lastMessage;
|
||||||
|
if (!lastMessageText) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawBodyRanges = conversationAttrs.lastMessageBodyRanges || [];
|
||||||
|
const bodyRanges = hydrateRanges(rawBodyRanges, findAndFormatContact);
|
||||||
|
|
||||||
|
const text = stripNewlinesForLeftPane(lastMessageText);
|
||||||
|
const prefix = conversationAttrs.lastMessagePrefix;
|
||||||
|
|
||||||
|
return {
|
||||||
|
author: dropNull(conversationAttrs.lastMessageAuthor),
|
||||||
|
bodyRanges,
|
||||||
|
deletedForEveryone: false,
|
||||||
|
prefix,
|
||||||
|
status: dropNull(conversationAttrs.lastMessageStatus),
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
}
|
186
ts/util/groupMembershipUtils.ts
Normal file
186
ts/util/groupMembershipUtils.ts
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import isNumber from 'lodash/isNumber';
|
||||||
|
import type { ConversationAttributesType } from '../model-types.d';
|
||||||
|
import type { UUID, UUIDStringType } from '../types/UUID';
|
||||||
|
import { SignalService as Proto } from '../protobuf';
|
||||||
|
import { isDirectConversation, isGroupV2 } from './whatTypeOfConversation';
|
||||||
|
|
||||||
|
export function isMemberPending(
|
||||||
|
conversationAttrs: Pick<
|
||||||
|
ConversationAttributesType,
|
||||||
|
'groupId' | 'groupVersion' | 'pendingMembersV2'
|
||||||
|
>,
|
||||||
|
uuid: UUID
|
||||||
|
): boolean {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { pendingMembersV2 } = conversationAttrs;
|
||||||
|
|
||||||
|
if (!pendingMembersV2 || !pendingMembersV2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingMembersV2.some(item => item.uuid === uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMemberBanned(
|
||||||
|
conversationAttrs: Pick<
|
||||||
|
ConversationAttributesType,
|
||||||
|
'groupId' | 'groupVersion' | 'bannedMembersV2'
|
||||||
|
>,
|
||||||
|
uuid: UUID
|
||||||
|
): boolean {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { bannedMembersV2 } = conversationAttrs;
|
||||||
|
|
||||||
|
if (!bannedMembersV2 || !bannedMembersV2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bannedMembersV2.some(member => member.uuid === uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMemberAwaitingApproval(
|
||||||
|
conversationAttrs: Pick<
|
||||||
|
ConversationAttributesType,
|
||||||
|
'groupId' | 'groupVersion' | 'pendingAdminApprovalV2'
|
||||||
|
>,
|
||||||
|
uuid: UUID
|
||||||
|
): boolean {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { pendingAdminApprovalV2 } = conversationAttrs;
|
||||||
|
|
||||||
|
if (!pendingAdminApprovalV2 || !pendingAdminApprovalV2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingAdminApprovalV2.some(member => member.uuid === uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMember(
|
||||||
|
conversationAttrs: Pick<
|
||||||
|
ConversationAttributesType,
|
||||||
|
'groupId' | 'groupVersion' | 'membersV2'
|
||||||
|
>,
|
||||||
|
uuid: UUID
|
||||||
|
): boolean {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { membersV2 } = conversationAttrs;
|
||||||
|
|
||||||
|
if (!membersV2 || !membersV2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return membersV2.some(item => item.uuid === uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMemberRequestingToJoin(
|
||||||
|
conversationAttrs: Pick<
|
||||||
|
ConversationAttributesType,
|
||||||
|
'groupId' | 'groupVersion' | 'pendingAdminApprovalV2'
|
||||||
|
>,
|
||||||
|
uuid: UUID
|
||||||
|
): boolean {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { pendingAdminApprovalV2 } = conversationAttrs;
|
||||||
|
|
||||||
|
if (!pendingAdminApprovalV2 || !pendingAdminApprovalV2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingAdminApprovalV2.some(item => item.uuid === uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_ARRAY: Readonly<[]> = [];
|
||||||
|
|
||||||
|
export function getBannedMemberships(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): ReadonlyArray<UUIDStringType> {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return EMPTY_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { bannedMembersV2 } = conversationAttrs;
|
||||||
|
|
||||||
|
return (bannedMembersV2 || []).map(member => member.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPendingMemberships(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): ReadonlyArray<{
|
||||||
|
addedByUserId?: UUIDStringType;
|
||||||
|
uuid: UUIDStringType;
|
||||||
|
}> {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return EMPTY_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = conversationAttrs.pendingMembersV2 || [];
|
||||||
|
return members.map(member => ({
|
||||||
|
addedByUserId: member.addedByUserId,
|
||||||
|
uuid: member.uuid,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPendingApprovalMemberships(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): ReadonlyArray<{ uuid: UUIDStringType }> {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return EMPTY_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = conversationAttrs.pendingAdminApprovalV2 || [];
|
||||||
|
return members.map(member => ({
|
||||||
|
uuid: member.uuid,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMembersCount(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): number | undefined {
|
||||||
|
if (isDirectConversation(conversationAttrs)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberList = conversationAttrs.membersV2 || conversationAttrs.members;
|
||||||
|
|
||||||
|
// We'll fail over if the member list is empty
|
||||||
|
if (memberList && memberList.length) {
|
||||||
|
return memberList.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { temporaryMemberCount } = conversationAttrs;
|
||||||
|
if (isNumber(temporaryMemberCount)) {
|
||||||
|
return temporaryMemberCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMemberships(
|
||||||
|
conversationAttrs: ConversationAttributesType
|
||||||
|
): ReadonlyArray<{
|
||||||
|
uuid: UUIDStringType;
|
||||||
|
isAdmin: boolean;
|
||||||
|
}> {
|
||||||
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
|
return EMPTY_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = conversationAttrs.membersV2 || [];
|
||||||
|
return members.map(member => ({
|
||||||
|
isAdmin: member.role === Proto.Member.Role.ADMINISTRATOR,
|
||||||
|
uuid: member.uuid,
|
||||||
|
}));
|
||||||
|
}
|
12
ts/util/hasDraft.ts
Normal file
12
ts/util/hasDraft.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types';
|
||||||
|
|
||||||
|
export function hasDraft(attributes: ConversationAttributesType): boolean {
|
||||||
|
const draftAttachments = attributes.draftAttachments || [];
|
||||||
|
|
||||||
|
return (attributes.draft ||
|
||||||
|
attributes.quotedMessageId ||
|
||||||
|
draftAttachments.length > 0) as boolean;
|
||||||
|
}
|
21
ts/util/isBlocked.ts
Normal file
21
ts/util/isBlocked.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { ConversationAttributesType } from '../model-types';
|
||||||
|
|
||||||
|
export function isBlocked(attributes: ConversationAttributesType): boolean {
|
||||||
|
const { e164, groupId, uuid } = attributes;
|
||||||
|
if (uuid) {
|
||||||
|
return window.storage.blocked.isUuidBlocked(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e164) {
|
||||||
|
return window.storage.blocked.isBlocked(e164);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
return window.storage.blocked.isGroupBlocked(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -1,25 +0,0 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import type { UUID } from '../types/UUID';
|
|
||||||
import type { ConversationAttributesType } from '../model-types.d';
|
|
||||||
import { isGroupV2 } from './whatTypeOfConversation';
|
|
||||||
|
|
||||||
export function isMemberPending(
|
|
||||||
conversationAttrs: Pick<
|
|
||||||
ConversationAttributesType,
|
|
||||||
'groupId' | 'groupVersion' | 'pendingMembersV2'
|
|
||||||
>,
|
|
||||||
uuid: UUID
|
|
||||||
): boolean {
|
|
||||||
if (!isGroupV2(conversationAttrs)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const { pendingMembersV2 } = conversationAttrs;
|
|
||||||
|
|
||||||
if (!pendingMembersV2 || !pendingMembersV2.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pendingMembersV2.some(item => item.uuid === uuid.toString());
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
|
|
||||||
export function memoizeByThis<Owner extends Record<string, unknown>, Result>(
|
|
||||||
fn: () => Result
|
|
||||||
): () => Result {
|
|
||||||
const lastValueMap = new WeakMap<Owner, Result>();
|
|
||||||
return function memoizedFn(this: Owner): Result {
|
|
||||||
const lastValue = lastValueMap.get(this);
|
|
||||||
const newValue = fn();
|
|
||||||
if (lastValue !== undefined && isEqual(lastValue, newValue)) {
|
|
||||||
return lastValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastValueMap.set(this, newValue);
|
|
||||||
return newValue;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -2,8 +2,9 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { sample } from 'lodash';
|
import { sample } from 'lodash';
|
||||||
import type { AvatarColorType } from '../types/Colors';
|
|
||||||
import { AvatarColors } from '../types/Colors';
|
import { AvatarColors } from '../types/Colors';
|
||||||
|
import type { ConversationAttributesType } from '../model-types';
|
||||||
|
import type { AvatarColorType, CustomColorType } from '../types/Colors';
|
||||||
|
|
||||||
const NEW_COLOR_NAMES = new Set(AvatarColors);
|
const NEW_COLOR_NAMES = new Set(AvatarColors);
|
||||||
|
|
||||||
|
@ -14,3 +15,20 @@ export function migrateColor(color?: string): AvatarColorType {
|
||||||
|
|
||||||
return sample(AvatarColors) || AvatarColors[0];
|
return sample(AvatarColors) || AvatarColors[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCustomColorData(conversation: ConversationAttributesType): {
|
||||||
|
customColor?: CustomColorType;
|
||||||
|
customColorId?: string;
|
||||||
|
} {
|
||||||
|
if (conversation.conversationColor !== 'custom') {
|
||||||
|
return {
|
||||||
|
customColor: undefined,
|
||||||
|
customColorId: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
customColor: conversation.customColor,
|
||||||
|
customColorId: conversation.customColorId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import type { SignalService as Proto } from '../protobuf';
|
||||||
import type { UUID } from '../types/UUID';
|
import type { UUID } from '../types/UUID';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { getConversationIdForLogging } from './idForLogging';
|
import { getConversationIdForLogging } from './idForLogging';
|
||||||
import { isMemberPending } from './isMemberPending';
|
import { isMemberPending } from './groupMembershipUtils';
|
||||||
import { isNotNil } from './isNotNil';
|
import { isNotNil } from './isNotNil';
|
||||||
|
|
||||||
export async function removePendingMember(
|
export async function removePendingMember(
|
||||||
|
|
Loading…
Reference in a new issue