Use UUIDs in group database schema

This commit is contained in:
Fedor Indutny 2021-10-26 15:59:08 -07:00 committed by GitHub
parent 74fde10ff5
commit 63fcdbe787
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 4530 additions and 3664 deletions

View File

@ -378,14 +378,12 @@ try {
});
const { imageToBlurHash } = require('./ts/util/imageToBlurHash');
const { isValidGuid } = require('./ts/util/isValidGuid');
const { ActiveWindowService } = require('./ts/services/ActiveWindowService');
window.imageToBlurHash = imageToBlurHash;
window.emojiData = require('emoji-datasource');
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
window.getGuid = require('uuid/v4');
const activeWindowService = new ActiveWindowService();
activeWindowService.initialize(window.document, ipc);
@ -401,8 +399,6 @@ try {
reducedMotionSetting: Boolean(config.reducedMotionSetting),
};
window.isValidGuid = isValidGuid;
window.React = require('react');
window.ReactDOM = require('react-dom');
window.moment = require('moment');

View File

@ -34,7 +34,6 @@ const MAX_ANIMATED_STICKER_BYTE_LENGTH = 300 * 1024;
window.ROOT_PATH = window.location.href.startsWith('file') ? '../../' : '/';
window.getEnvironment = getEnvironment;
window.getVersion = () => config.version;
window.getGuid = require('uuid/v4');
window.PQueue = require('p-queue').default;
window.Backbone = require('backbone');

View File

@ -8,7 +8,6 @@ const chaiAsPromised = require('chai-as-promised');
const { Crypto } = require('../ts/context/Crypto');
const { setEnvironment, Environment } = require('../ts/environment');
const { isValidGuid } = require('../ts/util/isValidGuid');
chai.use(chaiAsPromised);
@ -31,7 +30,6 @@ global.window = {
get: key => storageMap.get(key),
put: async (key, value) => storageMap.set(key, value),
},
isValidGuid,
};
// For ducks/network.getEmptyState()

View File

@ -13,10 +13,9 @@ import type {
import type { ConversationModel } from './models/conversations';
import { maybeDeriveGroupV2Id } from './groups';
import { assert } from './util/assert';
import { isValidGuid } from './util/isValidGuid';
import { map, reduce } from './util/iterables';
import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
import { UUID } from './types/UUID';
import { UUID, isValidUuid } from './types/UUID';
import { Address } from './types/Address';
import { QualifiedAddress } from './types/QualifiedAddress';
import * as log from './logging/log';
@ -25,7 +24,7 @@ const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
const {
getAllConversations,
getAllGroupsInvolvingId,
getAllGroupsInvolvingUuid,
getMessagesBySentAt,
migrateConversationMessages,
removeConversation,
@ -181,7 +180,7 @@ export class ConversationController {
return conversation;
}
const id = window.getGuid();
const id = UUID.generate().toString();
if (type === 'group') {
conversation = this._conversations.add({
@ -193,7 +192,7 @@ export class ConversationController {
version: 2,
...additionalInitialProps,
});
} else if (window.isValidGuid(identifier)) {
} else if (isValidUuid(identifier)) {
conversation = this._conversations.add({
id,
uuid: identifier,
@ -617,7 +616,7 @@ export class ConversationController {
}
const obsoleteId = obsolete.get('id');
const obsoleteUuid = obsolete.get('uuid');
const obsoleteUuid = obsolete.getUuid();
const currentId = current.get('id');
log.warn('combineConversations: Combining two conversations', {
obsolete: obsoleteId,
@ -643,13 +642,13 @@ export class ConversationController {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
ourUuid,
identifier: obsoleteUuid,
identifier: obsoleteUuid.toString(),
});
await Promise.all(
deviceIds.map(async deviceId => {
const addr = new QualifiedAddress(
ourUuid,
Address.create(obsoleteUuid, deviceId)
new Address(obsoleteUuid, deviceId)
);
await window.textsecure.storage.protocol.removeSession(addr);
})
@ -661,14 +660,14 @@ export class ConversationController {
if (obsoleteUuid) {
await window.textsecure.storage.protocol.removeIdentityKey(
new UUID(obsoleteUuid)
obsoleteUuid
);
}
log.warn(
'combineConversations: Ensure that all V1 groups have new conversationId instead of old'
);
const groups = await this.getAllGroupsInvolvingId(obsoleteId);
const groups = await this.getAllGroupsInvolvingUuid(obsoleteUuid);
groups.forEach(group => {
const members = group.get('members');
const withoutObsolete = without(members, obsoleteId);
@ -737,10 +736,10 @@ export class ConversationController {
return null;
}
async getAllGroupsInvolvingId(
conversationId: string
async getAllGroupsInvolvingUuid(
uuid: UUID
): Promise<Array<ConversationModel>> {
const groups = await getAllGroupsInvolvingId(conversationId, {
const groups = await getAllGroupsInvolvingUuid(uuid.toString(), {
ConversationCollection: window.Whisper.ConversationCollection,
});
return groups.map(group => {
@ -836,7 +835,7 @@ export class ConversationController {
// Clean up the conversations that have UUID as their e164.
const e164 = conversation.get('e164');
const uuid = conversation.get('uuid');
if (isValidGuid(e164) && uuid) {
if (isValidUuid(e164) && uuid) {
conversation.set({ e164: undefined });
updateConversation(conversation.attributes);

View File

@ -12,6 +12,8 @@ import { calculateAgreement, generateKeyPair } from './Curve';
import * as log from './logging/log';
import { HashType, CipherType } from './types/Crypto';
import { ProfileDecryptError } from './types/errors';
import { UUID } from './types/UUID';
import type { UUIDStringType } from './types/UUID';
export { HashType, CipherType };
@ -457,7 +459,7 @@ export function uuidToBytes(uuid: string): Uint8Array {
);
}
export function bytesToUuid(bytes: Uint8Array): undefined | string {
export function bytesToUuid(bytes: Uint8Array): undefined | UUIDStringType {
if (bytes.byteLength !== 16) {
log.warn(
'bytesToUuid: received an Uint8Array of invalid length. ' +
@ -473,7 +475,7 @@ export function bytesToUuid(bytes: Uint8Array): undefined | string {
return undefined;
}
export function splitUuids(buffer: Uint8Array): Array<string | null> {
export function splitUuids(buffer: Uint8Array): Array<UUIDStringType | null> {
const uuids = [];
for (let i = 0; i < buffer.byteLength; i += 16) {
const bytes = getBytes(buffer, i, 16);
@ -487,7 +489,7 @@ export function splitUuids(buffer: Uint8Array): Array<string | null> {
];
const uuid = chunks.join('-');
if (uuid !== '00000000-0000-0000-0000-000000000000') {
uuids.push(uuid);
uuids.push(UUID.cast(uuid));
} else {
uuids.push(null);
}

View File

@ -994,7 +994,7 @@ export class SignalProtocolStore extends EventsMixin {
id,
version: 2,
ourUuid: qualifiedAddress.ourUuid.toString(),
conversationId: new UUID(conversationId).toString(),
conversationId,
uuid: uuid.toString(),
deviceId,
record: record.serialize().toString('base64'),
@ -1394,7 +1394,7 @@ export class SignalProtocolStore extends EventsMixin {
return undefined;
}
const conversationId = new UUID(conversation.id).toString();
const conversationId = conversation.id;
const record = this.identityKeys.get(`conversation:${conversationId}`);
if (!record) {
return undefined;

View File

@ -3608,6 +3608,7 @@ export async function startApp(): Promise<void> {
messageSentAt: timestamp,
receiptTimestamp: envelopeTimestamp,
sourceConversationId,
sourceUuid,
sourceDevice,
type,
});
@ -3791,6 +3792,7 @@ export async function startApp(): Promise<void> {
messageSentAt: timestamp,
receiptTimestamp: envelopeTimestamp,
sourceConversationId,
sourceUuid,
sourceDevice,
type: MessageReceiptType.Delivery,
});

View File

@ -40,12 +40,13 @@ import type {
StartCallType,
} from '../state/ducks/calling';
import type { LocalizerType } from '../types/Util';
import type { UUIDStringType } from '../types/UUID';
import { missingCaseError } from '../util/missingCaseError';
const GROUP_CALL_RING_DURATION = 60 * 1000;
type MeType = ConversationType & {
uuid: string;
uuid: UUIDStringType;
};
export type PropsType = {

View File

@ -3,7 +3,6 @@
import * as React from 'react';
import { times } from 'lodash';
import { v4 as generateUuid } from 'uuid';
import { storiesOf } from '@storybook/react';
import { boolean, select, number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
@ -21,7 +20,10 @@ import type { PropsType } from './CallScreen';
import { CallScreen } from './CallScreen';
import { setupI18n } from '../util/setupI18n';
import { missingCaseError } from '../util/missingCaseError';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import {
getDefaultConversation,
getDefaultConversationWithUuid,
} from '../test-both/helpers/getDefaultConversation';
import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource';
import enMessages from '../../_locales/en/messages.json';
@ -286,10 +288,9 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,
...getDefaultConversation({
...getDefaultConversationWithUuid({
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
title: `Participant ${index + 1}`,
uuid: generateUuid(),
}),
}));

View File

@ -38,6 +38,7 @@ import { GroupCallRemoteParticipants } from './GroupCallRemoteParticipants';
import type { LocalizerType } from '../types/Util';
import { NeedsScreenRecordingPermissionsModal } from './NeedsScreenRecordingPermissionsModal';
import { missingCaseError } from '../util/missingCaseError';
import type { UUIDStringType } from '../types/UUID';
import * as KeyboardLayout from '../services/keyboardLayout';
import { useActivateSpeakerViewOnPresenting } from '../hooks/useActivateSpeakerViewOnPresenting';
@ -57,7 +58,7 @@ export type PropsType = {
phoneNumber?: string;
profileName?: string;
title: string;
uuid: string;
uuid: UUIDStringType;
};
openSystemPreferencesAction: () => unknown;
setGroupCallVideoRequest: (_: Array<GroupCallVideoRequest>) => void;

View File

@ -6,15 +6,18 @@ import { times } from 'lodash';
import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { v4 as generateUuid } from 'uuid';
import { AvatarColors } from '../types/Colors';
import type { ConversationType } from '../state/ducks/conversations';
import type { PropsType } from './CallingLobby';
import { CallingLobby } from './CallingLobby';
import { setupI18n } from '../util/setupI18n';
import { UUID } from '../types/UUID';
import enMessages from '../../_locales/en/messages.json';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import {
getDefaultConversation,
getDefaultConversationWithUuid,
} from '../test-both/helpers/getDefaultConversation';
const i18n = setupI18n('en', enMessages);
@ -60,8 +63,8 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
isCallFull: boolean('isCallFull', overrideProps.isCallFull || false),
me: overrideProps.me || {
color: AvatarColors[0],
id: generateUuid(),
uuid: generateUuid(),
id: UUID.generate().toString(),
uuid: UUID.generate().toString(),
},
onCallCanceled: action('on-call-canceled'),
onJoinCall: action('on-join-call'),
@ -81,8 +84,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
};
const fakePeekedParticipant = (conversationProps: Partial<ConversationType>) =>
getDefaultConversation({
uuid: generateUuid(),
getDefaultConversationWithUuid({
...conversationProps,
});
@ -106,8 +108,8 @@ story.add('No Camera, local avatar', () => {
me: {
avatarPath: '/fixtures/kitten-4-112-112.jpg',
color: AvatarColors[0],
id: generateUuid(),
uuid: generateUuid(),
id: UUID.generate().toString(),
uuid: UUID.generate().toString(),
},
});
return <CallingLobby {...props} />;
@ -141,11 +143,11 @@ story.add('Group Call - 1 peeked participant', () => {
});
story.add('Group Call - 1 peeked participant (self)', () => {
const uuid = generateUuid();
const uuid = UUID.generate().toString();
const props = createProps({
isGroupCall: true,
me: {
id: generateUuid(),
id: UUID.generate().toString(),
uuid,
},
peekedParticipants: [fakePeekedParticipant({ title: 'Ash', uuid })],

View File

@ -19,6 +19,7 @@ import {
} from './CallingLobbyJoinButton';
import type { AvatarColorType } from '../types/Colors';
import type { LocalizerType } from '../types/Util';
import type { UUIDStringType } from '../types/UUID';
import { useIsOnline } from '../hooks/useIsOnline';
import * as KeyboardLayout from '../services/keyboardLayout';
import type { ConversationType } from '../state/ducks/conversations';
@ -52,7 +53,7 @@ export type PropsType = {
avatarPath?: string;
id: string;
color?: AvatarColorType;
uuid: string;
uuid: UUIDStringType;
};
onCallCanceled: () => void;
onJoinCall: () => void;

View File

@ -5,13 +5,12 @@ import * as React from 'react';
import { sample } from 'lodash';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { v4 as generateUuid } from 'uuid';
import type { PropsType } from './CallingParticipantsList';
import { CallingParticipantsList } from './CallingParticipantsList';
import { AvatarColors } from '../types/Colors';
import type { GroupCallRemoteParticipantType } from '../types/Calling';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { getDefaultConversationWithUuid } from '../test-both/helpers/getDefaultConversation';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
@ -27,14 +26,13 @@ function createParticipant(
presenting: Boolean(participantProps.presenting),
sharingScreen: Boolean(participantProps.sharingScreen),
videoAspectRatio: 1.3,
...getDefaultConversation({
...getDefaultConversationWithUuid({
avatarPath: participantProps.avatarPath,
color: sample(AvatarColors),
isBlocked: Boolean(participantProps.isBlocked),
name: participantProps.name,
profileName: participantProps.title,
title: String(participantProps.title),
uuid: generateUuid(),
}),
};
}

View File

@ -16,6 +16,7 @@ import type { EmojiPickDataType } from './emoji/EmojiPicker';
import { convertShortName } from './emoji/lib';
import type { LocalizerType, BodyRangeType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations';
import { isValidUuid } from '../types/UUID';
import { MentionBlot } from '../quill/mentions/blot';
import {
matchEmojiImage,
@ -465,7 +466,7 @@ export function CompositionInput(props: Props): React.ReactElement {
const currentMemberUuids = currentMembers
.map(m => m.uuid)
.filter((uuid): uuid is string => uuid !== undefined);
.filter(isValidUuid);
const newDelta = getDeltaToRemoveStaleMentions(ops, currentMemberUuids);

View File

@ -4,13 +4,12 @@
import type { FC } from 'react';
import React from 'react';
import { memoize, times } from 'lodash';
import { v4 as generateUuid } from 'uuid';
import { storiesOf } from '@storybook/react';
import { number } from '@storybook/addon-knobs';
import { GroupCallOverflowArea } from './GroupCallOverflowArea';
import { setupI18n } from '../util/setupI18n';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { getDefaultConversationWithUuid } from '../test-both/helpers/getDefaultConversation';
import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource';
import { FRAME_BUFFER_SIZE } from '../calling/constants';
import enMessages from '../../_locales/en/messages.json';
@ -26,10 +25,9 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
presenting: false,
sharingScreen: false,
videoAspectRatio: 1.3,
...getDefaultConversation({
...getDefaultConversationWithUuid({
isBlocked: index === 10 || index === MAX_PARTICIPANTS - 1,
title: `Participant ${index + 1}`,
uuid: generateUuid(),
}),
}));

View File

@ -6,6 +6,7 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { setupI18n } from '../../util/setupI18n';
import { UUID } from '../../types/UUID';
import enMessages from '../../../_locales/en/messages.json';
import type { GroupV2ChangeType } from '../../groups';
import { SignalService as Proto } from '../../protobuf';
@ -14,12 +15,12 @@ import { GroupV2Change } from './GroupV2Change';
const i18n = setupI18n('en', enMessages);
const OUR_ID = 'OUR_ID';
const CONTACT_A = 'CONTACT_A';
const CONTACT_B = 'CONTACT_B';
const CONTACT_C = 'CONTACT_C';
const ADMIN_A = 'ADMIN_A';
const INVITEE_A = 'INVITEE_A';
const OUR_ID = UUID.generate().toString();
const CONTACT_A = UUID.generate().toString();
const CONTACT_B = UUID.generate().toString();
const CONTACT_C = UUID.generate().toString();
const ADMIN_A = UUID.generate().toString();
const INVITEE_A = UUID.generate().toString();
const AccessControlEnum = Proto.AccessControl.AccessRequired;
const RoleEnum = Proto.Member.Role;
@ -35,7 +36,7 @@ const renderChange = (change: GroupV2ChangeType, groupName?: string) => (
change={change}
groupName={groupName}
i18n={i18n}
ourConversationId={OUR_ID}
ourUuid={OUR_ID}
renderContact={renderContact}
/>
);
@ -62,7 +63,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
},
{
type: 'member-add',
conversationId: OUR_ID,
uuid: OUR_ID,
},
{
type: 'description',
@ -70,7 +71,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
},
{
type: 'member-privilege',
conversationId: OUR_ID,
uuid: OUR_ID,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
@ -402,7 +403,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -411,7 +412,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -419,7 +420,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -428,7 +429,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -437,7 +438,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -445,7 +446,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -461,7 +462,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-invite',
conversationId: OUR_ID,
uuid: OUR_ID,
inviter: CONTACT_B,
},
],
@ -470,7 +471,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-invite',
conversationId: OUR_ID,
uuid: OUR_ID,
inviter: CONTACT_A,
},
],
@ -481,7 +482,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-invite',
conversationId: CONTACT_A,
uuid: CONTACT_A,
inviter: CONTACT_B,
},
],
@ -491,7 +492,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-invite',
conversationId: CONTACT_B,
uuid: CONTACT_B,
inviter: CONTACT_C,
},
],
@ -500,7 +501,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-invite',
conversationId: CONTACT_A,
uuid: CONTACT_A,
inviter: CONTACT_B,
},
],
@ -511,7 +512,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-invite',
conversationId: OUR_ID,
uuid: OUR_ID,
inviter: CONTACT_A,
},
],
@ -521,7 +522,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-invite',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -530,7 +531,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-invite',
conversationId: CONTACT_A,
uuid: CONTACT_A,
inviter: OUR_ID,
},
],
@ -540,7 +541,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-invite',
conversationId: CONTACT_A,
uuid: CONTACT_A,
inviter: CONTACT_B,
},
],
@ -550,7 +551,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-invite',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -565,7 +566,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-link',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -574,7 +575,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-link',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -582,7 +583,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-link',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -597,7 +598,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-admin-approval',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -605,7 +606,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-admin-approval',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -614,7 +615,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-admin-approval',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -623,7 +624,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-admin-approval',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -631,7 +632,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-add-from-admin-approval',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -646,7 +647,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-remove',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -655,7 +656,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-remove',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -663,7 +664,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-remove',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -672,7 +673,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-remove',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -681,7 +682,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-remove',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -690,7 +691,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-remove',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -698,7 +699,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-remove',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -713,7 +714,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-privilege',
conversationId: OUR_ID,
uuid: OUR_ID,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
@ -722,7 +723,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-privilege',
conversationId: OUR_ID,
uuid: OUR_ID,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
@ -732,7 +733,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-privilege',
conversationId: CONTACT_A,
uuid: CONTACT_A,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
@ -742,7 +743,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-privilege',
conversationId: CONTACT_A,
uuid: CONTACT_A,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
@ -751,7 +752,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-privilege',
conversationId: CONTACT_A,
uuid: CONTACT_A,
newPrivilege: RoleEnum.ADMINISTRATOR,
},
],
@ -761,7 +762,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-privilege',
conversationId: OUR_ID,
uuid: OUR_ID,
newPrivilege: RoleEnum.DEFAULT,
},
],
@ -770,7 +771,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-privilege',
conversationId: OUR_ID,
uuid: OUR_ID,
newPrivilege: RoleEnum.DEFAULT,
},
],
@ -780,7 +781,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-privilege',
conversationId: CONTACT_A,
uuid: CONTACT_A,
newPrivilege: RoleEnum.DEFAULT,
},
],
@ -790,7 +791,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-privilege',
conversationId: CONTACT_A,
uuid: CONTACT_A,
newPrivilege: RoleEnum.DEFAULT,
},
],
@ -799,7 +800,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'member-privilege',
conversationId: CONTACT_A,
uuid: CONTACT_A,
newPrivilege: RoleEnum.DEFAULT,
},
],
@ -815,7 +816,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-add-one',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -823,7 +824,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-add-one',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -832,7 +833,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-add-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
},
],
})}
@ -841,7 +842,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-add-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
},
],
})}
@ -849,7 +850,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-add-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
},
],
})}
@ -896,7 +897,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
inviter: OUR_ID,
},
],
@ -906,7 +907,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
inviter: OUR_ID,
},
],
@ -916,7 +917,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
inviter: OUR_ID,
},
],
@ -925,7 +926,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
inviter: OUR_ID,
},
],
@ -935,7 +936,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
},
],
})}
@ -944,7 +945,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
inviter: CONTACT_B,
},
],
@ -955,7 +956,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: OUR_ID,
uuid: OUR_ID,
inviter: CONTACT_B,
},
],
@ -965,7 +966,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: CONTACT_B,
uuid: CONTACT_B,
inviter: CONTACT_A,
},
],
@ -976,7 +977,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
inviter: CONTACT_B,
},
],
@ -986,7 +987,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
inviter: CONTACT_B,
},
],
@ -995,7 +996,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
inviter: CONTACT_B,
},
],
@ -1006,7 +1007,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
},
],
})}
@ -1015,7 +1016,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
},
],
})}
@ -1023,7 +1024,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'pending-remove-one',
conversationId: INVITEE_A,
uuid: INVITEE_A,
},
],
})}
@ -1128,7 +1129,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'admin-approval-add-one',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -1136,7 +1137,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'admin-approval-add-one',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -1151,7 +1152,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'admin-approval-remove-one',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -1159,7 +1160,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'admin-approval-remove-one',
conversationId: OUR_ID,
uuid: OUR_ID,
},
],
})}
@ -1168,7 +1169,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'admin-approval-remove-one',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -1177,7 +1178,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'admin-approval-remove-one',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -1186,7 +1187,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'admin-approval-remove-one',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}
@ -1194,7 +1195,7 @@ storiesOf('Components/Conversation/GroupV2Change', module)
details: [
{
type: 'admin-approval-remove-one',
conversationId: CONTACT_A,
uuid: CONTACT_A,
},
],
})}

View File

@ -9,6 +9,7 @@ import type { ReplacementValuesType } from '../../types/I18N';
import type { FullJSXType } from '../Intl';
import { Intl } from '../Intl';
import type { LocalizerType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID';
import { GroupDescriptionText } from '../GroupDescriptionText';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage';
@ -21,7 +22,7 @@ import { Modal } from '../Modal';
export type PropsDataType = {
groupName?: string;
ourConversationId: string;
ourUuid: UUIDStringType;
change: GroupV2ChangeType;
};
@ -78,11 +79,11 @@ const changeToIconMap = new Map<string, GroupIconType>([
function getIcon(
detail: GroupV2ChangeDetailType,
fromId?: string
fromId?: UUIDStringType
): GroupIconType {
const changeType = detail.type;
let possibleIcon = changeToIconMap.get(changeType);
const isSameId = fromId === get(detail, 'conversationId', null);
const isSameId = fromId === get(detail, 'uuid', null);
if (isSameId) {
if (changeType === 'member-remove') {
possibleIcon = 'group-leave';
@ -103,7 +104,7 @@ function GroupV2Detail({
}: {
detail: GroupV2ChangeDetailType;
i18n: LocalizerType;
fromId?: string;
fromId?: UUIDStringType;
onButtonClick: (x: string) => unknown;
text: FullJSXType;
}): JSX.Element {
@ -132,7 +133,7 @@ function GroupV2Detail({
}
export function GroupV2Change(props: PropsType): ReactElement {
const { change, groupName, i18n, ourConversationId, renderContact } = props;
const { change, groupName, i18n, ourUuid, renderContact } = props;
const [groupDescription, setGroupDescription] = useState<
string | undefined
@ -142,7 +143,7 @@ export function GroupV2Change(props: PropsType): ReactElement {
<>
{renderChange(change, {
i18n,
ourConversationId,
ourUuid,
renderContact,
renderString: renderStringToIntl,
}).map((text: FullJSXType, index: number) => (

View File

@ -301,8 +301,8 @@ const actions = () => ({
),
checkForAccount: action('checkForAccount'),
clearChangedMessages: action('clearChangedMessages'),
clearInvitedConversationsForNewlyCreatedGroup: action(
'clearInvitedConversationsForNewlyCreatedGroup'
clearInvitedUuidsForNewlyCreatedGroup: action(
'clearInvitedUuidsForNewlyCreatedGroup'
),
setLoadCountdownStart: action('setLoadCountdownStart'),
setIsNearBottom: action('setIsNearBottom'),

View File

@ -130,7 +130,7 @@ export type PropsActionsType = {
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
) => void;
clearChangedMessages: (conversationId: string) => unknown;
clearInvitedConversationsForNewlyCreatedGroup: () => void;
clearInvitedUuidsForNewlyCreatedGroup: () => void;
closeContactSpoofingReview: () => void;
setLoadCountdownStart: (
conversationId: string,
@ -231,7 +231,7 @@ const getActions = createSelector(
const unsafe = pick(props, [
'acknowledgeGroupMemberNameCollisions',
'clearChangedMessages',
'clearInvitedConversationsForNewlyCreatedGroup',
'clearInvitedUuidsForNewlyCreatedGroup',
'closeContactSpoofingReview',
'setLoadCountdownStart',
'setIsNearBottom',
@ -1313,7 +1313,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
const {
acknowledgeGroupMemberNameCollisions,
areWeAdmin,
clearInvitedConversationsForNewlyCreatedGroup,
clearInvitedUuidsForNewlyCreatedGroup,
closeContactSpoofingReview,
contactSpoofingReview,
i18n,
@ -1566,7 +1566,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
<NewlyCreatedGroupInvitedContactsDialog
contacts={invitedContactsForNewlyCreatedGroup}
i18n={i18n}
onClose={clearInvitedConversationsForNewlyCreatedGroup}
onClose={clearInvitedUuidsForNewlyCreatedGroup}
/>
)}

View File

@ -7,6 +7,7 @@ import { times } from 'lodash';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { UUID } from '../../../types/UUID';
import { setupI18n } from '../../../util/setupI18n';
import enMessages from '../../../../_locales/en/messages.json';
import type { PropsType } from './PendingInvites';
@ -40,11 +41,13 @@ const conversation: ConversationType = {
sharedGroupNames: [],
};
const OUR_UUID = UUID.generate().toString();
const createProps = (): PropsType => ({
approvePendingMembership: action('approvePendingMembership'),
conversation,
i18n,
ourConversationId: 'abc123',
ourUuid: OUR_UUID,
pendingApprovalMemberships: times(5, () => ({
member: getDefaultConversation(),
})),
@ -52,13 +55,13 @@ const createProps = (): PropsType => ({
...times(4, () => ({
member: getDefaultConversation(),
metadata: {
addedByUserId: 'abc123',
addedByUserId: OUR_UUID,
},
})),
...times(8, () => ({
member: getDefaultConversation(),
metadata: {
addedByUserId: 'def456',
addedByUserId: UUID.generate().toString(),
},
})),
],

View File

@ -7,6 +7,7 @@ import _ from 'lodash';
import type { ConversationType } from '../../../state/ducks/conversations';
import type { LocalizerType } from '../../../types/Util';
import type { UUIDStringType } from '../../../types/UUID';
import { Avatar } from '../../Avatar';
import { ConfirmationDialog } from '../../ConfirmationDialog';
import { PanelSection } from './PanelSection';
@ -16,7 +17,7 @@ import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
export type PropsType = {
readonly conversation?: ConversationType;
readonly i18n: LocalizerType;
readonly ourConversationId?: string;
readonly ourUuid?: UUIDStringType;
readonly pendingApprovalMemberships: ReadonlyArray<GroupV2RequestingMembership>;
readonly pendingMemberships: ReadonlyArray<GroupV2PendingMembership>;
readonly approvePendingMembership: (conversationId: string) => void;
@ -25,7 +26,7 @@ export type PropsType = {
export type GroupV2PendingMembership = {
metadata: {
addedByUserId?: string;
addedByUserId?: UUIDStringType;
};
member: ConversationType;
};
@ -54,14 +55,14 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
approvePendingMembership,
conversation,
i18n,
ourConversationId,
ourUuid,
pendingMemberships,
pendingApprovalMemberships,
revokePendingMemberships,
}) => {
if (!conversation || !ourConversationId) {
if (!conversation || !ourUuid) {
throw new Error(
'PendingInvites rendered without a conversation or ourConversationId'
'PendingInvites rendered without a conversation or ourUuid'
);
}
@ -131,7 +132,7 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
i18n={i18n}
members={conversation.sortedGroupMembers || []}
memberships={pendingMemberships}
ourConversationId={ourConversationId}
ourUuid={ourUuid}
setStagedMemberships={setStagedMemberships}
/>
) : null}
@ -142,7 +143,7 @@ export const PendingInvites: React.ComponentType<PropsType> = ({
i18n={i18n}
members={conversation.sortedGroupMembers || []}
onClose={() => setStagedMemberships(null)}
ourConversationId={ourConversationId}
ourUuid={ourUuid}
revokePendingMemberships={revokePendingMemberships}
stagedMemberships={stagedMemberships}
/>
@ -156,7 +157,7 @@ function MembershipActionConfirmation({
i18n,
members,
onClose,
ourConversationId,
ourUuid,
revokePendingMemberships,
stagedMemberships,
}: {
@ -164,7 +165,7 @@ function MembershipActionConfirmation({
i18n: LocalizerType;
members: Array<ConversationType>;
onClose: () => void;
ourConversationId: string;
ourUuid: string;
revokePendingMemberships: (conversationIds: Array<string>) => void;
stagedMemberships: Array<StagedMembershipType>;
}) {
@ -216,7 +217,7 @@ function MembershipActionConfirmation({
{getConfirmationMessage({
i18n,
members,
ourConversationId,
ourUuid,
stagedMemberships,
})}
</ConfirmationDialog>
@ -226,12 +227,12 @@ function MembershipActionConfirmation({
function getConfirmationMessage({
i18n,
members,
ourConversationId,
ourUuid,
stagedMemberships,
}: Readonly<{
i18n: LocalizerType;
members: ReadonlyArray<ConversationType>;
ourConversationId: string;
ourUuid: string;
stagedMemberships: ReadonlyArray<StagedMembershipType>;
}>): string {
if (!stagedMemberships || !stagedMemberships.length) {
@ -261,8 +262,7 @@ function getConfirmationMessage({
const firstPendingMembership = firstMembership as GroupV2PendingMembership;
// Pending invite
const invitedByUs =
firstPendingMembership.metadata.addedByUserId === ourConversationId;
const invitedByUs = firstPendingMembership.metadata.addedByUserId === ourUuid;
if (invitedByUs) {
return i18n('PendingInvites--revoke-for', {
@ -364,14 +364,14 @@ function MembersPendingProfileKey({
i18n,
members,
memberships,
ourConversationId,
ourUuid,
setStagedMemberships,
}: Readonly<{
conversation: ConversationType;
i18n: LocalizerType;
members: Array<ConversationType>;
memberships: ReadonlyArray<GroupV2PendingMembership>;
ourConversationId: string;
ourUuid: string;
setStagedMemberships: (stagedMembership: Array<StagedMembershipType>) => void;
}>) {
const groupedPendingMemberships = _.groupBy(
@ -380,7 +380,7 @@ function MembersPendingProfileKey({
);
const {
[ourConversationId]: ourPendingMemberships,
[ourUuid]: ourPendingMemberships,
...otherPendingMembershipGroups
} = groupedPendingMemberships;

View File

@ -4,13 +4,14 @@
import type { FullJSXType } from './components/Intl';
import type { LocalizerType } from './types/Util';
import type { ReplacementValuesType } from './types/I18N';
import type { UUIDStringType } from './types/UUID';
import { missingCaseError } from './util/missingCaseError';
import type { GroupV2ChangeDetailType, GroupV2ChangeType } from './groups';
import { SignalService as Proto } from './protobuf';
import * as log from './logging/log';
export type SmartContactRendererType = (conversationId: string) => FullJSXType;
export type SmartContactRendererType = (uuid: UUIDStringType) => FullJSXType;
export type StringRendererType = (
id: string,
i18n: LocalizerType,
@ -18,9 +19,9 @@ export type StringRendererType = (
) => FullJSXType;
export type RenderOptionsType = {
from?: string;
from?: UUIDStringType;
i18n: LocalizerType;
ourConversationId: string;
ourUuid: UUIDStringType;
renderContact: SmartContactRendererType;
renderString: StringRendererType;
};
@ -46,14 +47,8 @@ export function renderChangeDetail(
detail: GroupV2ChangeDetailType,
options: RenderOptionsType
): FullJSXType {
const {
from,
i18n,
ourConversationId,
renderContact,
renderString,
} = options;
const fromYou = Boolean(from && from === ourConversationId);
const { from, i18n, ourUuid, renderContact, renderString } = options;
const fromYou = Boolean(from && from === ourUuid);
if (detail.type === 'create') {
if (fromYou) {
@ -214,8 +209,8 @@ export function renderChangeDetail(
return '';
}
if (detail.type === 'member-add') {
const { conversationId } = detail;
const weAreJoiner = conversationId === ourConversationId;
const { uuid } = detail;
const weAreJoiner = uuid === ourUuid;
if (weAreJoiner) {
if (fromYou) {
@ -230,25 +225,25 @@ export function renderChangeDetail(
}
if (fromYou) {
return renderString('GroupV2--member-add--other--you', i18n, [
renderContact(conversationId),
renderContact(uuid),
]);
}
if (from) {
return renderString('GroupV2--member-add--other--other', i18n, {
adderName: renderContact(from),
addeeName: renderContact(conversationId),
addeeName: renderContact(uuid),
});
}
return renderString('GroupV2--member-add--other--unknown', i18n, [
renderContact(conversationId),
renderContact(uuid),
]);
}
if (detail.type === 'member-add-from-invite') {
const { conversationId, inviter } = detail;
const weAreJoiner = conversationId === ourConversationId;
const weAreInviter = Boolean(inviter && inviter === ourConversationId);
const { uuid, inviter } = detail;
const weAreJoiner = uuid === ourUuid;
const weAreInviter = Boolean(inviter && inviter === ourUuid);
if (!from || from !== conversationId) {
if (!from || from !== uuid) {
if (weAreJoiner) {
// They can't be the same, no fromYou check here
if (from) {
@ -261,17 +256,17 @@ export function renderChangeDetail(
if (fromYou) {
return renderString('GroupV2--member-add--invited--you', i18n, {
inviteeName: renderContact(conversationId),
inviteeName: renderContact(uuid),
});
}
if (from) {
return renderString('GroupV2--member-add--invited--other', i18n, {
memberName: renderContact(from),
inviteeName: renderContact(conversationId),
inviteeName: renderContact(uuid),
});
}
return renderString('GroupV2--member-add--invited--unknown', i18n, {
inviteeName: renderContact(conversationId),
inviteeName: renderContact(uuid),
});
}
@ -288,12 +283,12 @@ export function renderChangeDetail(
}
if (weAreInviter) {
return renderString('GroupV2--member-add--from-invite--from-you', i18n, [
renderContact(conversationId),
renderContact(uuid),
]);
}
if (inviter) {
return renderString('GroupV2--member-add--from-invite--other', i18n, {
inviteeName: renderContact(conversationId),
inviteeName: renderContact(uuid),
inviterName: renderContact(inviter),
});
}
@ -301,17 +296,17 @@ export function renderChangeDetail(
'GroupV2--member-add--from-invite--other-no-from',
i18n,
{
inviteeName: renderContact(conversationId),
inviteeName: renderContact(uuid),
}
);
}
if (detail.type === 'member-add-from-link') {
const { conversationId } = detail;
const { uuid } = detail;
if (fromYou && conversationId === ourConversationId) {
if (fromYou && uuid === ourUuid) {
return renderString('GroupV2--member-add-from-link--you--you', i18n);
}
if (from && conversationId === from) {
if (from && uuid === from) {
return renderString('GroupV2--member-add-from-link--other', i18n, [
renderContact(from),
]);
@ -321,12 +316,12 @@ export function renderChangeDetail(
// from group change events, which always have a sender.
log.warn('member-add-from-link change type; we have no from!');
return renderString('GroupV2--member-add--other--unknown', i18n, [
renderContact(conversationId),
renderContact(uuid),
]);
}
if (detail.type === 'member-add-from-admin-approval') {
const { conversationId } = detail;
const weAreJoiner = conversationId === ourConversationId;
const { uuid } = detail;
const weAreJoiner = uuid === ourUuid;
if (weAreJoiner) {
if (from) {
@ -352,7 +347,7 @@ export function renderChangeDetail(
return renderString(
'GroupV2--member-add-from-admin-approval--other--you',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
if (from) {
@ -361,7 +356,7 @@ export function renderChangeDetail(
i18n,
{
adminName: renderContact(from),
joinerName: renderContact(conversationId),
joinerName: renderContact(uuid),
}
);
}
@ -372,12 +367,12 @@ export function renderChangeDetail(
return renderString(
'GroupV2--member-add-from-admin-approval--other--unknown',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
if (detail.type === 'member-remove') {
const { conversationId } = detail;
const weAreLeaver = conversationId === ourConversationId;
const { uuid } = detail;
const weAreLeaver = uuid === ourUuid;
if (weAreLeaver) {
if (fromYou) {
@ -393,10 +388,10 @@ export function renderChangeDetail(
if (fromYou) {
return renderString('GroupV2--member-remove--other--you', i18n, [
renderContact(conversationId),
renderContact(uuid),
]);
}
if (from && from === conversationId) {
if (from && from === uuid) {
return renderString('GroupV2--member-remove--other--self', i18n, [
renderContact(from),
]);
@ -404,16 +399,16 @@ export function renderChangeDetail(
if (from) {
return renderString('GroupV2--member-remove--other--other', i18n, {
adminName: renderContact(from),
memberName: renderContact(conversationId),
memberName: renderContact(uuid),
});
}
return renderString('GroupV2--member-remove--other--unknown', i18n, [
renderContact(conversationId),
renderContact(uuid),
]);
}
if (detail.type === 'member-privilege') {
const { conversationId, newPrivilege } = detail;
const weAreMember = conversationId === ourConversationId;
const { uuid, newPrivilege } = detail;
const weAreMember = uuid === ourUuid;
if (newPrivilege === RoleEnum.ADMINISTRATOR) {
if (weAreMember) {
@ -435,7 +430,7 @@ export function renderChangeDetail(
return renderString(
'GroupV2--member-privilege--promote--other--you',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
if (from) {
@ -444,14 +439,14 @@ export function renderChangeDetail(
i18n,
{
adminName: renderContact(from),
memberName: renderContact(conversationId),
memberName: renderContact(uuid),
}
);
}
return renderString(
'GroupV2--member-privilege--promote--other--unknown',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
if (newPrivilege === RoleEnum.DEFAULT) {
@ -473,7 +468,7 @@ export function renderChangeDetail(
return renderString(
'GroupV2--member-privilege--demote--other--you',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
if (from) {
@ -482,14 +477,14 @@ export function renderChangeDetail(
i18n,
{
adminName: renderContact(from),
memberName: renderContact(conversationId),
memberName: renderContact(uuid),
}
);
}
return renderString(
'GroupV2--member-privilege--demote--other--unknown',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
log.warn(
@ -498,8 +493,8 @@ export function renderChangeDetail(
return '';
}
if (detail.type === 'pending-add-one') {
const { conversationId } = detail;
const weAreInvited = conversationId === ourConversationId;
const { uuid } = detail;
const weAreInvited = uuid === ourUuid;
if (weAreInvited) {
if (from) {
return renderString('GroupV2--pending-add--one--you--other', i18n, [
@ -510,7 +505,7 @@ export function renderChangeDetail(
}
if (fromYou) {
return renderString('GroupV2--pending-add--one--other--you', i18n, [
renderContact(conversationId),
renderContact(uuid),
]);
}
if (from) {
@ -539,23 +534,23 @@ export function renderChangeDetail(
]);
}
if (detail.type === 'pending-remove-one') {
const { inviter, conversationId } = detail;
const weAreInviter = Boolean(inviter && inviter === ourConversationId);
const weAreInvited = conversationId === ourConversationId;
const sentByInvited = Boolean(from && from === conversationId);
const { inviter, uuid } = detail;
const weAreInviter = Boolean(inviter && inviter === ourUuid);
const weAreInvited = uuid === ourUuid;
const sentByInvited = Boolean(from && from === uuid);
const sentByInviter = Boolean(from && inviter && from === inviter);
if (weAreInviter) {
if (sentByInvited) {
return renderString('GroupV2--pending-remove--decline--you', i18n, [
renderContact(conversationId),
renderContact(uuid),
]);
}
if (fromYou) {
return renderString(
'GroupV2--pending-remove--revoke-invite-from-you--one--you',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
if (from) {
@ -564,14 +559,14 @@ export function renderChangeDetail(
i18n,
{
adminName: renderContact(from),
inviteeName: renderContact(conversationId),
inviteeName: renderContact(uuid),
}
);
}
return renderString(
'GroupV2--pending-remove--revoke-invite-from-you--one--unknown',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
if (sentByInvited) {
@ -635,7 +630,7 @@ export function renderChangeDetail(
}
if (detail.type === 'pending-remove-many') {
const { count, inviter } = detail;
const weAreInviter = Boolean(inviter && inviter === ourConversationId);
const weAreInviter = Boolean(inviter && inviter === ourUuid);
if (weAreInviter) {
if (fromYou) {
@ -714,19 +709,19 @@ export function renderChangeDetail(
);
}
if (detail.type === 'admin-approval-add-one') {
const { conversationId } = detail;
const weAreJoiner = conversationId === ourConversationId;
const { uuid } = detail;
const weAreJoiner = uuid === ourUuid;
if (weAreJoiner) {
return renderString('GroupV2--admin-approval-add-one--you', i18n);
}
return renderString('GroupV2--admin-approval-add-one--other', i18n, [
renderContact(conversationId),
renderContact(uuid),
]);
}
if (detail.type === 'admin-approval-remove-one') {
const { conversationId } = detail;
const weAreJoiner = conversationId === ourConversationId;
const { uuid } = detail;
const weAreJoiner = uuid === ourUuid;
if (weAreJoiner) {
if (fromYou) {
@ -745,14 +740,14 @@ export function renderChangeDetail(
return renderString(
'GroupV2--admin-approval-remove-one--other--you',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
if (from && from === conversationId) {
if (from && from === uuid) {
return renderString(
'GroupV2--admin-approval-remove-one--other--own',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
if (from) {
@ -761,7 +756,7 @@ export function renderChangeDetail(
i18n,
{
adminName: renderContact(from),
joinerName: renderContact(conversationId),
joinerName: renderContact(uuid),
}
);
}
@ -771,7 +766,7 @@ export function renderChangeDetail(
return renderString(
'GroupV2--admin-approval-remove-one--other--own',
i18n,
[renderContact(conversationId)]
[renderContact(uuid)]
);
}
if (detail.type === 'group-link-add') {

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@ import { isDirectConversation } from '../util/whatTypeOfConversation';
import { getOwn } from '../util/getOwn';
import { missingCaseError } from '../util/missingCaseError';
import { createWaitBatcher } from '../util/waitBatcher';
import type { UUIDStringType } from '../types/UUID';
import {
SendActionType,
SendStatus,
@ -34,6 +35,7 @@ export enum MessageReceiptType {
type MessageReceiptAttributesType = {
messageSentAt: number;
receiptTimestamp: number;
sourceUuid: UUIDStringType;
sourceConversationId: string;
sourceDevice: number;
type: MessageReceiptType;
@ -57,6 +59,7 @@ const deleteSentProtoBatcher = createWaitBatcher({
async function getTargetMessage(
sourceId: string,
sourceUuid: UUIDStringType,
messages: MessageModelCollectionType
): Promise<MessageModel | null> {
if (messages.length === 0) {
@ -70,9 +73,12 @@ async function getTargetMessage(
return window.MessageController.register(message.id, message);
}
const groups = await window.Signal.Data.getAllGroupsInvolvingId(sourceId, {
ConversationCollection: window.Whisper.ConversationCollection,
});
const groups = await window.Signal.Data.getAllGroupsInvolvingUuid(
sourceUuid,
{
ConversationCollection: window.Whisper.ConversationCollection,
}
);
const ids = groups.pluck('id');
ids.push(sourceId);
@ -136,6 +142,7 @@ export class MessageReceipts extends Collection<MessageReceiptModel> {
const type = receipt.get('type');
const messageSentAt = receipt.get('messageSentAt');
const sourceConversationId = receipt.get('sourceConversationId');
const sourceUuid = receipt.get('sourceUuid');
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
@ -145,7 +152,11 @@ export class MessageReceipts extends Collection<MessageReceiptModel> {
}
);
const message = await getTargetMessage(sourceConversationId, messages);
const message = await getTargetMessage(
sourceConversationId,
sourceUuid,
messages
);
if (!message) {
log.info(
'No message for receipt',

13
ts/model-types.d.ts vendored
View File

@ -26,6 +26,7 @@ import { AttachmentType, ThumbnailType } from './types/Attachment';
import { EmbeddedContactType } from './types/EmbeddedContact';
import { SignalService as Proto } from './protobuf';
import { AvatarDataType } from './types/Avatar';
import { UUIDStringType } from './types/UUID';
import AccessRequiredEnum = Proto.AccessControl.AccessRequired;
import MemberRoleEnum = Proto.Member.Role;
@ -181,7 +182,7 @@ export type MessageAttributesType = {
serverGuid?: string;
serverTimestamp?: number;
source?: string;
sourceUuid?: string;
sourceUuid?: UUIDStringType;
timestamp: number;
@ -253,7 +254,7 @@ export type ConversationAttributesType = {
version: number;
// Private core info
uuid?: string;
uuid?: UUIDStringType;
e164?: string;
// Private other fields
@ -327,7 +328,7 @@ export type ConversationAttributesType = {
};
export type GroupV2MemberType = {
conversationId: string;
uuid: UUIDStringType;
role: MemberRoleEnum;
joinedAtVersion: number;
@ -339,14 +340,14 @@ export type GroupV2MemberType = {
};
export type GroupV2PendingMemberType = {
addedByUserId?: string;
conversationId: string;
addedByUserId?: UUIDStringType;
uuid: UUIDStringType;
timestamp: number;
role: MemberRoleEnum;
};
export type GroupV2PendingAdminApprovalType = {
conversationId: string;
uuid: UUIDStringType;
timestamp: number;
};

View File

@ -46,7 +46,8 @@ import { sniffImageMimeType } from '../util/sniffImageMimeType';
import { isValidE164 } from '../util/isValidE164';
import type { MIMEType } from '../types/MIME';
import { IMAGE_JPEG, IMAGE_GIF, IMAGE_WEBP } from '../types/MIME';
import { UUID } from '../types/UUID';
import { UUID, isValidUuid } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
import { deriveAccessKey, decryptProfileName, decryptProfile } from '../Crypto';
import * as Bytes from '../Bytes';
import type { BodyRangesType } from '../types/Util';
@ -242,7 +243,7 @@ export class ConversationModel extends window.Backbone
initialize(attributes: Partial<ConversationAttributesType> = {}): void {
if (isValidE164(attributes.id, false)) {
this.set({ id: window.getGuid(), e164: attributes.id });
this.set({ id: UUID.generate().toString(), e164: attributes.id });
}
this.storeName = 'conversations';
@ -340,7 +341,7 @@ export class ConversationModel extends window.Backbone
}
}
isMemberRequestingToJoin(conversationId: string): boolean {
isMemberRequestingToJoin(id: string): boolean {
if (!isGroupV2(this.attributes)) {
return false;
}
@ -350,12 +351,11 @@ export class ConversationModel extends window.Backbone
return false;
}
return pendingAdminApprovalV2.some(
item => item.conversationId === conversationId
);
const uuid = UUID.checkedLookup(id).toString();
return pendingAdminApprovalV2.some(item => item.uuid === uuid);
}
isMemberPending(conversationId: string): boolean {
isMemberPending(id: string): boolean {
if (!isGroupV2(this.attributes)) {
return false;
}
@ -365,13 +365,11 @@ export class ConversationModel extends window.Backbone
return false;
}
return window._.any(
pendingMembersV2,
item => item.conversationId === conversationId
);
const uuid = UUID.checkedLookup(id).toString();
return window._.any(pendingMembersV2, item => item.uuid === uuid);
}
isMemberAwaitingApproval(conversationId: string): boolean {
isMemberAwaitingApproval(id: string): boolean {
if (!isGroupV2(this.attributes)) {
return false;
}
@ -381,13 +379,11 @@ export class ConversationModel extends window.Backbone
return false;
}
return window._.any(
pendingAdminApprovalV2,
item => item.conversationId === conversationId
);
const uuid = UUID.checkedLookup(id).toString();
return window._.any(pendingAdminApprovalV2, item => item.uuid === uuid);
}
isMember(conversationId: string): boolean {
isMember(id: string): boolean {
if (!isGroupV2(this.attributes)) {
throw new Error(
`isMember: Called for non-GroupV2 conversation ${this.idForLogging()}`
@ -398,11 +394,9 @@ export class ConversationModel extends window.Backbone
if (!membersV2 || !membersV2.length) {
return false;
}
const uuid = UUID.checkedLookup(id).toString();
return window._.any(
membersV2,
item => item.conversationId === conversationId
);
return window._.any(membersV2, item => item.uuid === uuid);
}
async updateExpirationTimerInGroupV2(
@ -678,7 +672,7 @@ export class ConversationModel extends window.Backbone
}
return uuid;
})
.filter((uuid): uuid is string => Boolean(uuid));
.filter(isNotNil);
if (!uuids.length) {
return undefined;
@ -1565,7 +1559,7 @@ export class ConversationModel extends window.Backbone
updateUuid(uuid?: string): void {
const oldValue = this.get('uuid');
if (uuid && uuid !== oldValue) {
this.set('uuid', uuid.toLowerCase());
this.set('uuid', UUID.cast(uuid.toLowerCase()));
window.Signal.Data.updateConversation(this.attributes);
this.trigger('idUpdated', this, 'uuid', oldValue);
}
@ -1840,6 +1834,7 @@ export class ConversationModel extends window.Backbone
approvalRequired: boolean;
}): Promise<void> {
const ourConversationId = window.ConversationController.getOurConversationIdOrThrow();
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
try {
if (approvalRequired) {
await this.modifyGroupV2({
@ -1870,7 +1865,7 @@ export class ConversationModel extends window.Backbone
this.set({
pendingAdminApprovalV2: [
{
conversationId: ourConversationId,
uuid: ourUuid,
timestamp: Date.now(),
},
],
@ -2143,14 +2138,12 @@ export class ConversationModel extends window.Backbone
}
async safeGetVerified(): Promise<number> {
const uuid = this.get('uuid');
const uuid = this.getUuid();
if (!uuid) {
return window.textsecure.storage.protocol.VerifiedStatus.DEFAULT;
}
const promise = window.textsecure.storage.protocol.getVerified(
new UUID(uuid)
);
const promise = window.textsecure.storage.protocol.getVerified(uuid);
return promise.catch(
() => window.textsecure.storage.protocol.VerifiedStatus.DEFAULT
);
@ -2227,7 +2220,7 @@ export class ConversationModel extends window.Backbone
);
}
const uuid = this.get('uuid');
const uuid = this.getUuid();
const beginningVerified = this.get('verified');
let keyChange;
if (options.viaSyncMessage) {
@ -2239,13 +2232,13 @@ export class ConversationModel extends window.Backbone
// handle the incoming key from the sync messages - need different
// behavior if that key doesn't match the current key
keyChange = await window.textsecure.storage.protocol.processVerifiedMessage(
new UUID(uuid),
uuid,
verified,
options.key || undefined
);
} else if (uuid) {
keyChange = await window.textsecure.storage.protocol.setVerified(
new UUID(uuid),
uuid,
verified
);
} else {
@ -2289,10 +2282,10 @@ export class ConversationModel extends window.Backbone
async sendVerifySyncMessage(
e164: string | undefined,
uuid: string,
uuid: UUID,
state: number
): Promise<CallbackResultType | void> {
const identifier = uuid || e164;
const identifier = uuid ? uuid.toString() : e164;
if (!identifier) {
throw new Error(
'sendVerifySyncMessage: Neither e164 nor UUID were provided'
@ -2328,7 +2321,7 @@ export class ConversationModel extends window.Backbone
await handleMessageSend(
window.textsecure.messaging.syncVerification(
e164,
uuid,
uuid.toString(),
state,
key,
options
@ -2402,20 +2395,20 @@ export class ConversationModel extends window.Backbone
);
}
const uuid = this.get('uuid');
const uuid = this.getUuid();
if (!uuid) {
log.warn(`setApproved(${this.id}): no uuid, ignoring`);
return;
}
return window.textsecure.storage.protocol.setApproval(new UUID(uuid), true);
return window.textsecure.storage.protocol.setApproval(uuid, true);
}
safeIsUntrusted(): boolean {
const uuid = this.get('uuid');
try {
const uuid = this.getUuid();
strictAssert(uuid, `No uuid for conversation: ${this.id}`);
return window.textsecure.storage.protocol.isUntrusted(new UUID(uuid));
return window.textsecure.storage.protocol.isUntrusted(uuid);
} catch (err) {
return false;
}
@ -2671,8 +2664,9 @@ export class ConversationModel extends window.Backbone
this.trigger('newmessage', model);
if (isDirectConversation(this.attributes)) {
window.ConversationController.getAllGroupsInvolvingId(this.id).then(
const uuid = this.getUuid();
if (isDirectConversation(this.attributes) && uuid) {
window.ConversationController.getAllGroupsInvolvingUuid(uuid).then(
groups => {
window._.forEach(groups, group => {
group.addVerifiedChange(this.id, verified, options);
@ -2790,8 +2784,9 @@ export class ConversationModel extends window.Backbone
this.trigger('newmessage', model);
if (isDirectConversation(this.attributes)) {
window.ConversationController.getAllGroupsInvolvingId(this.id).then(
const uuid = this.getUuid();
if (isDirectConversation(this.attributes) && uuid) {
window.ConversationController.getAllGroupsInvolvingUuid(uuid).then(
groups => {
window._.forEach(groups, group => {
group.addProfileChange(profileChange, this.id);
@ -2899,17 +2894,21 @@ export class ConversationModel extends window.Backbone
`Conversation ${this.idForLogging()}: adding change number notification`
);
const sourceUuid = this.getCheckedUuid(
'Change number notification without uuid'
);
const convos = [
this,
...(await window.ConversationController.getAllGroupsInvolvingId(this.id)),
...(await window.ConversationController.getAllGroupsInvolvingUuid(
sourceUuid
)),
];
const sourceUuid = this.get('uuid');
await Promise.all(
convos.map(convo => {
return convo.addNotification('change-number-notification', {
sourceUuid,
sourceUuid: sourceUuid.toString(),
});
})
);
@ -2998,7 +2997,7 @@ export class ConversationModel extends window.Backbone
validateUuid(): string | null {
if (isDirectConversation(this.attributes) && this.get('uuid')) {
if (window.isValidGuid(this.get('uuid'))) {
if (isValidUuid(this.get('uuid'))) {
return null;
}
@ -3037,13 +3036,14 @@ export class ConversationModel extends window.Backbone
});
}
isAdmin(conversationId: string): boolean {
isAdmin(id: string): boolean {
if (!isGroupV2(this.attributes)) {
return false;
}
const uuid = UUID.checkedLookup(id).toString();
const members = this.get('membersV2') || [];
const member = members.find(x => x.conversationId === conversationId);
const member = members.find(x => x.uuid === uuid);
if (!member) {
return false;
}
@ -3053,8 +3053,19 @@ export class ConversationModel extends window.Backbone
return member.role === MEMBER_ROLES.ADMINISTRATOR;
}
getUuid(): UUID | undefined {
const value = this.get('uuid');
return value && new UUID(value);
}
getCheckedUuid(reason: string): UUID {
const result = this.getUuid();
strictAssert(result !== undefined, reason);
return result;
}
private getMemberships(): Array<{
conversationId: string;
uuid: UUIDStringType;
isAdmin: boolean;
}> {
if (!isGroupV2(this.attributes)) {
@ -3064,7 +3075,7 @@ export class ConversationModel extends window.Backbone
const members = this.get('membersV2') || [];
return members.map(member => ({
isAdmin: member.role === Proto.Member.Role.ADMINISTRATOR,
conversationId: member.conversationId,
uuid: member.uuid,
}));
}
@ -3081,8 +3092,8 @@ export class ConversationModel extends window.Backbone
}
private getPendingMemberships(): Array<{
addedByUserId?: string;
conversationId: string;
addedByUserId?: UUIDStringType;
uuid: UUIDStringType;
}> {
if (!isGroupV2(this.attributes)) {
return [];
@ -3091,18 +3102,18 @@ export class ConversationModel extends window.Backbone
const members = this.get('pendingMembersV2') || [];
return members.map(member => ({
addedByUserId: member.addedByUserId,
conversationId: member.conversationId,
uuid: member.uuid,
}));
}
private getPendingApprovalMemberships(): Array<{ conversationId: string }> {
private getPendingApprovalMemberships(): Array<{ uuid: UUIDStringType }> {
if (!isGroupV2(this.attributes)) {
return [];
}
const members = this.get('pendingAdminApprovalV2') || [];
return members.map(member => ({
conversationId: member.conversationId,
uuid: member.uuid,
}));
}
@ -3136,6 +3147,13 @@ export class ConversationModel extends window.Backbone
return members.map(member => member.id);
}
getMemberUuids(): Array<UUID> {
const members = this.getMembers();
return members.map(member => {
return member.getCheckedUuid('Group member without uuid');
});
}
getRecipients({
includePendingMembers,
extraConversationsForSend,
@ -3356,7 +3374,7 @@ export class ConversationModel extends window.Backbone
// We are only creating this model so we can use its sync message
// sending functionality. It will not be saved to the database.
const message = new window.Whisper.Message({
id: window.getGuid(),
id: UUID.generate().toString(),
type: 'outgoing',
conversationId: this.get('id'),
sent_at: timestamp,
@ -3489,7 +3507,7 @@ export class ConversationModel extends window.Backbone
// We are only creating this model so we can use its sync message
// sending functionality. It will not be saved to the database.
const message = new window.Whisper.Message({
id: window.getGuid(),
id: UUID.generate.toString(),
type: 'outgoing',
conversationId: this.get('id'),
sent_at: timestamp,
@ -3731,7 +3749,7 @@ export class ConversationModel extends window.Backbone
}
const attributes: MessageAttributesType = {
...messageWithSchema,
id: window.getGuid(),
id: UUID.generate().toString(),
};
const model = new window.Whisper.Message(attributes);
@ -3840,9 +3858,10 @@ export class ConversationModel extends window.Backbone
const conversationId = this.id;
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
const lastMessages = await window.Signal.Data.getLastConversationMessages({
conversationId,
ourConversationId,
ourUuid,
Message: window.Whisper.Message,
});
@ -4361,12 +4380,16 @@ export class ConversationModel extends window.Backbone
return;
}
const ourGroups = await window.ConversationController.getAllGroupsInvolvingId(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
window.ConversationController.getOurConversationId()!
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourGroups = await window.ConversationController.getAllGroupsInvolvingUuid(
ourUuid
);
const theirGroups = await window.ConversationController.getAllGroupsInvolvingId(
this.id
const theirUuid = this.getUuid();
if (!theirUuid) {
return;
}
const theirGroups = await window.ConversationController.getAllGroupsInvolvingUuid(
theirUuid
);
const sharedGroups = window._.intersection(ourGroups, theirGroups);
@ -4734,8 +4757,8 @@ export class ConversationModel extends window.Backbone
const memberEnum = Proto.Member.Role;
const members = this.get('membersV2') || [];
const myId = window.ConversationController.getOurConversationId();
const me = members.find(item => item.conversationId === myId);
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
const me = members.find(item => item.uuid === ourUuid);
if (!me) {
return false;
}

View File

@ -33,6 +33,8 @@ import { SendMessageProtoError } from '../textsecure/Errors';
import * as expirationTimer from '../util/expirationTimer';
import type { ReactionType } from '../types/Reactions';
import { UUID } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
import {
copyStickerToAttachments,
deletePackReference,
@ -181,8 +183,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
INITIAL_PROTOCOL_VERSION?: number;
OUR_UUID?: string;
isSelected?: boolean;
private pendingMarkRead?: number;
@ -223,7 +223,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
this.INITIAL_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.INITIAL;
this.OUR_UUID = window.textsecure.storage.user.getUuid()?.toString();
this.on('change', this.notifyRedux);
}
@ -385,7 +384,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
conversationSelector: findAndFormatContact,
ourConversationId,
ourNumber: window.textsecure.storage.user.getNumber(),
ourUuid: this.OUR_UUID,
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
regionCode: window.storage.get('regionCode', 'ZZ'),
accountSelector: (identifier?: string) => {
const state = window.reduxStore.getState();
@ -1104,7 +1103,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return sourceDevice || window.textsecure.storage.user.getDeviceId();
}
getSourceUuid(): string | undefined {
getSourceUuid(): UUIDStringType | undefined {
if (isIncoming(this.attributes)) {
return this.get('sourceUuid');
}
@ -1114,7 +1113,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
);
}
return this.OUR_UUID;
return window.textsecure.storage.user.getUuid()?.toString();
}
getContactId(): string | undefined {
@ -2510,7 +2509,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return;
}
const messageId = window.getGuid();
const messageId = UUID.generate().toString();
// Send delivery receipts, but only for incoming sealed sender messages
// and not for messages from unaccepted conversations

View File

@ -36,8 +36,6 @@ import {
import { ourProfileKeyService } from './ourProfileKey';
import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
import * as preferredReactionEmoji from '../reactions/preferredReactionEmoji';
import { UUID } from '../types/UUID';
import * as Errors from '../types/errors';
import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
@ -107,9 +105,9 @@ export async function toContactRecord(
conversation: ConversationModel
): Promise<Proto.ContactRecord> {
const contactRecord = new Proto.ContactRecord();
const uuid = conversation.get('uuid');
const uuid = conversation.getUuid();
if (uuid) {
contactRecord.serviceUuid = uuid;
contactRecord.serviceUuid = uuid.toString();
}
const e164 = conversation.get('e164');
if (e164) {
@ -120,15 +118,8 @@ export async function toContactRecord(
contactRecord.profileKey = Bytes.fromBase64(String(profileKey));
}
let maybeUuid: UUID | undefined;
try {
maybeUuid = uuid ? new UUID(uuid) : undefined;
} catch (error) {
log.warn(`Invalid uuid in contact record: ${Errors.toLogFormat(error)}`);
}
const identityKey = maybeUuid
? await window.textsecure.storage.protocol.loadIdentityKey(maybeUuid)
const identityKey = uuid
? await window.textsecure.storage.protocol.loadIdentityKey(uuid)
: undefined;
if (identityKey) {
contactRecord.identityKey = identityKey;

View File

@ -33,6 +33,7 @@ import { assert, strictAssert } from '../util/assert';
import { cleanDataForIpc } from './cleanDataForIpc';
import type { ReactionType } from '../types/Reactions';
import type { ConversationColorType, CustomColorType } from '../types/Colors';
import type { UUIDStringType } from '../types/UUID';
import type { ProcessGroupCallRingRequestResult } from '../types/Calling';
import type { RemoveAllConfiguration } from '../types/RemoveAllConfiguration';
import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
@ -204,7 +205,7 @@ const dataInterface: ClientInterface = {
getAllConversations,
getAllConversationIds,
getAllPrivateConversations,
getAllGroupsInvolvingId,
getAllGroupsInvolvingUuid,
searchConversations,
searchMessages,
@ -1044,15 +1045,15 @@ async function getAllPrivateConversations({
return collection;
}
async function getAllGroupsInvolvingId(
id: string,
async function getAllGroupsInvolvingUuid(
uuid: UUIDStringType,
{
ConversationCollection,
}: {
ConversationCollection: typeof ConversationModelCollectionType;
}
) {
const conversations = await channels.getAllGroupsInvolvingId(id);
const conversations = await channels.getAllGroupsInvolvingUuid(uuid);
const collection = new ConversationCollection();
collection.add(conversations);
@ -1325,11 +1326,11 @@ async function getNewerMessagesByConversation(
}
async function getLastConversationMessages({
conversationId,
ourConversationId,
ourUuid,
Message,
}: {
conversationId: string;
ourConversationId: string;
ourUuid: UUIDStringType;
Message: typeof MessageModel;
}): Promise<LastConversationMessagesType> {
const {
@ -1338,7 +1339,7 @@ async function getLastConversationMessages({
hasUserInitiatedMessages,
} = await channels.getLastConversationMessages({
conversationId,
ourConversationId,
ourUuid,
});
return {

View File

@ -63,7 +63,7 @@ export type EmojiType = {
export type IdentityKeyType = {
firstUse: boolean;
id: UUIDStringType | `conversation:${UUIDStringType}`;
id: UUIDStringType | `conversation:${string}`;
nonblockingApproval: boolean;
publicKey: Uint8Array;
timestamp: number;
@ -501,7 +501,9 @@ export type DataInterface = {
export type ServerInterface = DataInterface & {
getAllConversations: () => Promise<Array<ConversationType>>;
getAllGroupsInvolvingId: (id: string) => Promise<Array<ConversationType>>;
getAllGroupsInvolvingUuid: (
id: UUIDStringType
) => Promise<Array<ConversationType>>;
getAllPrivateConversations: () => Promise<Array<ConversationType>>;
getConversationById: (id: string) => Promise<ConversationType | undefined>;
getExpiredMessages: () => Promise<Array<MessageType>>;
@ -528,7 +530,7 @@ export type ServerInterface = DataInterface & {
) => Promise<Array<MessageTypeUnhydrated>>;
getLastConversationMessages: (options: {
conversationId: string;
ourConversationId: string;
ourUuid: UUIDStringType;
}) => Promise<LastConversationMessagesServerType>;
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
removeConversation: (id: Array<string> | string) => Promise<void>;
@ -576,8 +578,8 @@ export type ClientInterface = DataInterface & {
getAllConversations: (options: {
ConversationCollection: typeof ConversationModelCollectionType;
}) => Promise<ConversationModelCollectionType>;
getAllGroupsInvolvingId: (
id: string,
getAllGroupsInvolvingUuid: (
id: UUIDStringType,
options: {
ConversationCollection: typeof ConversationModelCollectionType;
}
@ -630,7 +632,7 @@ export type ClientInterface = DataInterface & {
) => Promise<MessageModelCollectionType>;
getLastConversationMessages: (options: {
conversationId: string;
ourConversationId: string;
ourUuid: UUIDStringType;
Message: typeof MessageModel;
}) => Promise<LastConversationMessagesType>;
getTapToViewMessagesNeedingErase: (options: {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,448 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import type { LoggerType } from '../../types/Logging';
import { isValidUuid } from '../../types/UUID';
import { assertSync } from '../../util/assert';
import Helpers from '../../textsecure/Helpers';
import { createOrUpdate, getById, removeById } from '../util';
import type { EmptyQuery, Query } from '../util';
import type { ItemKeyType } from '../Interface';
function getOurUuid(db: Database): string | undefined {
const UUID_ID: ItemKeyType = 'uuid_id';
const row: { json: string } | undefined = db
.prepare<Query>('SELECT json FROM items WHERE id = $id;')
.get({ id: UUID_ID });
if (!row) {
return undefined;
}
const { value } = JSON.parse(row.json);
const [ourUuid] = Helpers.unencodeNumber(String(value).toLowerCase());
return ourUuid;
}
export default function updateToSchemaVersion41(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 41) {
return;
}
const getConversationUuid = db
.prepare<Query>(
`
SELECT uuid
FROM
conversations
WHERE
id = $conversationId
`
)
.pluck();
const getConversationStats = db.prepare<Query>(
`
SELECT uuid, e164, active_at
FROM
conversations
WHERE
id = $conversationId
`
);
const compareConvoRecency = (a: string, b: string): number => {
const aStats = getConversationStats.get({ conversationId: a });
const bStats = getConversationStats.get({ conversationId: b });
const isAComplete = Boolean(aStats?.uuid && aStats?.e164);
const isBComplete = Boolean(bStats?.uuid && bStats?.e164);
if (!isAComplete && !isBComplete) {
return 0;
}
if (!isAComplete) {
return -1;
}
if (!isBComplete) {
return 1;
}
return aStats.active_at - bStats.active_at;
};
const clearSessionsAndKeys = () => {
// ts/background.ts will ask user to relink so all that matters here is
// to maintain an invariant:
//
// After this migration all sessions and keys are prefixed by
// "uuid:".
db.exec(
`
DELETE FROM senderKeys;
DELETE FROM sessions;
DELETE FROM signedPreKeys;
DELETE FROM preKeys;
`
);
assertSync(removeById<string>(db, 'items', 'identityKey'));
assertSync(removeById<string>(db, 'items', 'registrationId'));
};
const moveIdentityKeyToMap = (ourUuid: string) => {
type IdentityKeyType = {
privKey: string;
publicKey: string;
};
const identityKey = assertSync(
getById<string, { value: IdentityKeyType }>(db, 'items', 'identityKey')
);
type RegistrationId = number;
const registrationId = assertSync(
getById<string, { value: RegistrationId }>(db, 'items', 'registrationId')
);
if (identityKey) {
assertSync(
createOrUpdate<ItemKeyType>(db, 'items', {
id: 'identityKeyMap',
value: {
[ourUuid]: identityKey.value,
},
})
);
}
if (registrationId) {
assertSync(
createOrUpdate<ItemKeyType>(db, 'items', {
id: 'registrationIdMap',
value: {
[ourUuid]: registrationId.value,
},
})
);
}
db.exec(
`
DELETE FROM items WHERE id = "identityKey" OR id = "registrationId";
`
);
};
const prefixKeys = (ourUuid: string) => {
for (const table of ['signedPreKeys', 'preKeys']) {
// Update id to include suffix, add `ourUuid` and `keyId` fields.
db.prepare<Query>(
`
UPDATE ${table}
SET
id = $ourUuid || ':' || id,
json = json_set(
json,
'$.id',
$ourUuid || ':' || json_extract(json, '$.id'),
'$.keyId',
json_extract(json, '$.id'),
'$.ourUuid',
$ourUuid
)
`
).run({ ourUuid });
}
};
const updateSenderKeys = (ourUuid: string) => {
const senderKeys: ReadonlyArray<{
id: string;
senderId: string;
lastUpdatedDate: number;
}> = db
.prepare<EmptyQuery>(
'SELECT id, senderId, lastUpdatedDate FROM senderKeys'
)
.all();
logger.info(`Updating ${senderKeys.length} sender keys`);
const updateSenderKey = db.prepare<Query>(
`
UPDATE senderKeys
SET
id = $newId,
senderId = $newSenderId
WHERE
id = $id
`
);
const deleteSenderKey = db.prepare<Query>(
'DELETE FROM senderKeys WHERE id = $id'
);
const pastKeys = new Map<
string,
{
conversationId: string;
lastUpdatedDate: number;
}
>();
let updated = 0;
let deleted = 0;
let skipped = 0;
for (const { id, senderId, lastUpdatedDate } of senderKeys) {
const [conversationId] = Helpers.unencodeNumber(senderId);
const uuid = getConversationUuid.get({ conversationId });
if (!uuid) {
deleted += 1;
deleteSenderKey.run({ id });
continue;
}
const newId = `${ourUuid}:${id.replace(conversationId, uuid)}`;
const existing = pastKeys.get(newId);
// We are going to delete on of the keys anyway
if (existing) {
skipped += 1;
} else {
updated += 1;
}
const isOlder =
existing &&
(lastUpdatedDate < existing.lastUpdatedDate ||
compareConvoRecency(conversationId, existing.conversationId) < 0);
if (isOlder) {
deleteSenderKey.run({ id });
continue;
} else if (existing) {
deleteSenderKey.run({ id: newId });
}
pastKeys.set(newId, { conversationId, lastUpdatedDate });
updateSenderKey.run({
id,
newId,
newSenderId: `${senderId.replace(conversationId, uuid)}`,
});
}
logger.info(
`Updated ${senderKeys.length} sender keys: ` +
`updated: ${updated}, deleted: ${deleted}, skipped: ${skipped}`
);
};
const updateSessions = (ourUuid: string) => {
// Use uuid instead of conversation id in existing sesions and prefix id
// with ourUuid.
//
// Set ourUuid column and field in json
const allSessions = db
.prepare<EmptyQuery>('SELECT id, conversationId FROM SESSIONS')
.all();
logger.info(`Updating ${allSessions.length} sessions`);
const updateSession = db.prepare<Query>(
`
UPDATE sessions
SET
id = $newId,
ourUuid = $ourUuid,
uuid = $uuid,
json = json_set(
sessions.json,
'$.id',
$newId,
'$.uuid',
$uuid,
'$.ourUuid',
$ourUuid
)
WHERE
id = $id
`
);
const deleteSession = db.prepare<Query>(
'DELETE FROM sessions WHERE id = $id'
);
const pastSessions = new Map<
string,
{
conversationId: string;
}
>();
let updated = 0;
let deleted = 0;
let skipped = 0;
for (const { id, conversationId } of allSessions) {
const uuid = getConversationUuid.get({ conversationId });
if (!uuid) {
deleted += 1;
deleteSession.run({ id });
continue;
}
const newId = `${ourUuid}:${id.replace(conversationId, uuid)}`;
const existing = pastSessions.get(newId);
// We are going to delete on of the keys anyway
if (existing) {
skipped += 1;
} else {
updated += 1;
}
const isOlder =
existing &&
compareConvoRecency(conversationId, existing.conversationId) < 0;
if (isOlder) {
deleteSession.run({ id });
continue;
} else if (existing) {
deleteSession.run({ id: newId });
}
pastSessions.set(newId, { conversationId });
updateSession.run({
id,
newId,
uuid,
ourUuid,
});
}
logger.info(
`Updated ${allSessions.length} sessions: ` +
`updated: ${updated}, deleted: ${deleted}, skipped: ${skipped}`
);
};
const updateIdentityKeys = () => {
const identityKeys: ReadonlyArray<{
id: string;
}> = db.prepare<EmptyQuery>('SELECT id FROM identityKeys').all();
logger.info(`Updating ${identityKeys.length} identity keys`);
const updateIdentityKey = db.prepare<Query>(
`
UPDATE identityKeys
SET
id = $newId,
json = json_set(
identityKeys.json,
'$.id',
$newId
)
WHERE
id = $id
`
);
let migrated = 0;
for (const { id } of identityKeys) {
const uuid = getConversationUuid.get({ conversationId: id });
let newId: string;
if (uuid) {
migrated += 1;
newId = uuid;
} else {
newId = `conversation:${id}`;
}
updateIdentityKey.run({ id, newId });
}
logger.info(`Migrated ${migrated} identity keys`);
};
db.transaction(() => {
db.exec(
`
-- Change type of 'id' column from INTEGER to STRING
ALTER TABLE preKeys
RENAME TO old_preKeys;
ALTER TABLE signedPreKeys
RENAME TO old_signedPreKeys;
CREATE TABLE preKeys(
id STRING PRIMARY KEY ASC,
json TEXT
);
CREATE TABLE signedPreKeys(
id STRING PRIMARY KEY ASC,
json TEXT
);
-- sqlite handles the type conversion
INSERT INTO preKeys SELECT * FROM old_preKeys;
INSERT INTO signedPreKeys SELECT * FROM old_signedPreKeys;
DROP TABLE old_preKeys;
DROP TABLE old_signedPreKeys;
-- Alter sessions
ALTER TABLE sessions
ADD COLUMN ourUuid STRING;
ALTER TABLE sessions
ADD COLUMN uuid STRING;
`
);
const ourUuid = getOurUuid(db);
if (!isValidUuid(ourUuid)) {
logger.error(
'updateToSchemaVersion41: no uuid is available clearing sessions'
);
clearSessionsAndKeys();
db.pragma('user_version = 41');
return;
}
prefixKeys(ourUuid);
updateSenderKeys(ourUuid);
updateSessions(ourUuid);
moveIdentityKeyToMap(ourUuid);
updateIdentityKeys();
db.pragma('user_version = 41');
})();
logger.info('updateToSchemaVersion41: success!');
}

View File

@ -0,0 +1,77 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import { batchMultiVarQuery } from '../util';
import type { ArrayQuery } from '../util';
import type { LoggerType } from '../../types/Logging';
export default function updateToSchemaVersion42(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 42) {
return;
}
db.transaction(() => {
// First, recreate messages table delete trigger with reaction support
db.exec(`
DROP TRIGGER messages_on_delete;
CREATE TRIGGER messages_on_delete AFTER DELETE ON messages BEGIN
DELETE FROM messages_fts WHERE rowid = old.rowid;
DELETE FROM sendLogPayloads WHERE id IN (
SELECT payloadId FROM sendLogMessageIds
WHERE messageId = old.id
);
DELETE FROM reactions WHERE rowid IN (
SELECT rowid FROM reactions
WHERE messageId = old.id
);
END;
`);
// Then, delete previously-orphaned reactions
// Note: we use `pluck` here to fetch only the first column of
// returned row.
const messageIdList: Array<string> = db
.prepare('SELECT id FROM messages ORDER BY id ASC;')
.pluck()
.all();
const allReactions: Array<{
rowid: number;
messageId: string;
}> = db.prepare('SELECT rowid, messageId FROM reactions;').all();
const messageIds = new Set(messageIdList);
const reactionsToDelete: Array<number> = [];
allReactions.forEach(reaction => {
if (!messageIds.has(reaction.messageId)) {
reactionsToDelete.push(reaction.rowid);
}
});
function deleteReactions(rowids: Array<number>) {
db.prepare<ArrayQuery>(
`
DELETE FROM reactions
WHERE rowid IN ( ${rowids.map(() => '?').join(', ')} );
`
).run(rowids);
}
if (reactionsToDelete.length > 0) {
logger.info(`Deleting ${reactionsToDelete.length} orphaned reactions`);
batchMultiVarQuery(db, reactionsToDelete, deleteReactions);
}
db.pragma('user_version = 42');
})();
logger.info('updateToSchemaVersion42: success!');
}

View File

@ -0,0 +1,417 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import { omit } from 'lodash';
import type { LoggerType } from '../../types/Logging';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import { isNotNil } from '../../util/isNotNil';
import { assert } from '../../util/assert';
import {
TableIterator,
getCountFromTable,
jsonToObject,
objectToJSON,
} from '../util';
import type { EmptyQuery, Query } from '../util';
import type { MessageType, ConversationType } from '../Interface';
export default function updateToSchemaVersion43(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 43) {
return;
}
type LegacyPendingMemberType = {
addedByUserId?: string;
conversationId: string;
};
type LegacyAdminApprovalType = {
conversationId: string;
};
type LegacyConversationType = {
id: string;
membersV2?: Array<{
conversationId: string;
}>;
pendingMembersV2?: Array<LegacyPendingMemberType>;
pendingAdminApprovalV2?: Array<LegacyAdminApprovalType>;
};
const getConversationUuid = db
.prepare<Query>(
`
SELECT uuid
FROM
conversations
WHERE
id = $conversationId
`
)
.pluck();
const updateConversationStmt = db.prepare(
`
UPDATE conversations SET
json = $json,
members = $members
WHERE id = $id;
`
);
const updateMessageStmt = db.prepare(
`
UPDATE messages SET
json = $json,
sourceUuid = $sourceUuid
WHERE id = $id;
`
);
const upgradeConversation = (convo: ConversationType) => {
const legacy = (convo as unknown) as LegacyConversationType;
let result = convo;
const memberKeys: Array<keyof LegacyConversationType> = [
'membersV2',
'pendingMembersV2',
'pendingAdminApprovalV2',
];
for (const key of memberKeys) {
const oldValue = legacy[key];
if (!Array.isArray(oldValue)) {
continue;
}
let addedByCount = 0;
const newValue = oldValue
.map(member => {
const uuid: UUIDStringType = getConversationUuid.get({
conversationId: member.conversationId,
});
if (!uuid) {
logger.warn(
`updateToSchemaVersion43: ${legacy.id}.${key} UUID not found ` +
`for ${member.conversationId}`
);
return undefined;
}
const updated = {
...omit(member, 'conversationId'),
uuid,
};
// We previously stored our conversation
if (!('addedByUserId' in member) || !member.addedByUserId) {
return updated;
}
const addedByUserId:
| UUIDStringType
| undefined = getConversationUuid.get({
conversationId: member.addedByUserId,
});
if (!addedByUserId) {
return updated;
}
addedByCount += 1;
return {
...updated,
addedByUserId,
};
})
.filter(isNotNil);
result = {
...result,
[key]: newValue,
};
if (oldValue.length !== 0) {
logger.info(
`updateToSchemaVersion43: migrated ${oldValue.length} ${key} ` +
`entries to ${newValue.length} for ${legacy.id}`
);
}
if (addedByCount > 0) {
logger.info(
`updateToSchemaVersion43: migrated ${addedByCount} addedByUserId ` +
`in ${key} for ${legacy.id}`
);
}
}
if (result === convo) {
return;
}
let dbMembers: string | null;
if (result.membersV2) {
dbMembers = result.membersV2.map(item => item.uuid).join(' ');
} else if (result.members) {
dbMembers = result.members.join(' ');
} else {
dbMembers = null;
}
updateConversationStmt.run({
id: result.id,
json: objectToJSON(result),
members: dbMembers,
});
};
type LegacyMessageType = {
id: string;
groupV2Change?: {
from: string;
details: Array<
(
| {
type:
| 'member-add'
| 'member-add-from-invite'
| 'member-add-from-link'
| 'member-add-from-admin-approval'
| 'member-privilege'
| 'member-remove'
| 'pending-add-one'
| 'pending-remove-one'
| 'admin-approval-add-one'
| 'admin-approval-remove-one';
conversationId: string;
}
| {
type: unknown;
conversationId?: undefined;
}
) &
(
| {
type:
| 'member-add-from-invite'
| 'pending-remove-one'
| 'pending-remove-many'
| 'admin-approval-remove-one';
inviter: string;
}
| {
inviter?: undefined;
}
)
>;
};
sourceUuid: string;
invitedGV2Members?: Array<LegacyPendingMemberType>;
};
const upgradeMessage = (message: MessageType): boolean => {
const {
id,
groupV2Change,
sourceUuid,
invitedGV2Members,
} = (message as unknown) as LegacyMessageType;
let result = message;
if (groupV2Change) {
assert(result.groupV2Change, 'Pacify typescript');
const from: UUIDStringType | undefined = getConversationUuid.get({
conversationId: groupV2Change.from,
});
if (from) {
result = {
...result,
groupV2Change: {
...result.groupV2Change,
from,
},
};
} else {
result = {
...result,
groupV2Change: omit(result.groupV2Change, ['from']),
};
}
let changedDetails = false;
const details = groupV2Change.details
.map((legacyDetail, i) => {
const oldDetail = result.groupV2Change?.details[i];
assert(oldDetail, 'Pacify typescript');
let newDetail = oldDetail;
for (const key of ['conversationId' as const, 'inviter' as const]) {
const oldValue = legacyDetail[key];
const newKey = key === 'conversationId' ? 'uuid' : key;
if (oldValue === undefined) {
continue;
}
changedDetails = true;
let newValue: UUIDStringType | null = getConversationUuid.get({
conversationId: oldValue,
});
if (key === 'inviter') {
newValue = newValue ?? UUID.cast(oldValue);
}
if (!newValue) {
logger.warn(
`updateToSchemaVersion43: ${id}.groupV2Change.details.${key} ` +
`UUID not found for ${oldValue}`
);
return undefined;
}
assert(newDetail.type === legacyDetail.type, 'Pacify typescript');
newDetail = {
...omit(newDetail, key),
[newKey]: newValue,
};
}
return newDetail;
})
.filter(isNotNil);
if (changedDetails) {
result = {
...result,
groupV2Change: {
...result.groupV2Change,
details,
},
};
}
}
if (sourceUuid) {
const newValue: UUIDStringType =
getConversationUuid.get({
conversationId: sourceUuid,
}) ?? UUID.cast(sourceUuid);
if (newValue !== sourceUuid) {
result = {
...result,
sourceUuid: newValue,
};
}
}
if (invitedGV2Members) {
const newMembers = invitedGV2Members
.map(({ addedByUserId, conversationId }, i) => {
const uuid: UUIDStringType | null = getConversationUuid.get({
conversationId,
});
const oldMember =
result.invitedGV2Members && result.invitedGV2Members[i];
assert(oldMember !== undefined, 'Pacify typescript');
if (!uuid) {
logger.warn(
`updateToSchemaVersion43: ${id}.invitedGV2Members UUID ` +
`not found for ${conversationId}`
);
return undefined;
}
const newMember = {
...omit(oldMember, ['conversationId']),
uuid,
};
if (!addedByUserId) {
return newMember;
}
const newAddedBy: UUIDStringType | null = getConversationUuid.get({
conversationId: addedByUserId,
});
if (!newAddedBy) {
return newMember;
}
return {
...newMember,
addedByUserId: newAddedBy,
};
})
.filter(isNotNil);
result = {
...result,
invitedGV2Members: newMembers,
};
}
if (result === message) {
return false;
}
updateMessageStmt.run({
id: result.id,
json: JSON.stringify(result),
sourceUuid: result.sourceUuid ?? null,
});
return true;
};
db.transaction(() => {
const allConversations = db
.prepare<EmptyQuery>(
`
SELECT json, profileLastFetchedAt
FROM conversations
ORDER BY id ASC;
`
)
.all()
.map(({ json }) => jsonToObject<ConversationType>(json));
logger.info(
'updateToSchemaVersion43: About to iterate through ' +
`${allConversations.length} conversations`
);
for (const convo of allConversations) {
upgradeConversation(convo);
}
const messageCount = getCountFromTable(db, 'messages');
logger.info(
'updateToSchemaVersion43: About to iterate through ' +
`${messageCount} messages`
);
let updatedCount = 0;
for (const message of new TableIterator<MessageType>(db, 'messages')) {
if (upgradeMessage(message)) {
updatedCount += 1;
}
}
logger.info(`updateToSchemaVersion43: Updated ${updatedCount} messages`);
db.pragma('user_version = 43');
})();
logger.info('updateToSchemaVersion43: success!');
}

1934
ts/sql/migrations/index.ts Normal file

File diff suppressed because it is too large Load Diff

260
ts/sql/util.ts Normal file
View File

@ -0,0 +1,260 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import { isNumber, last } from 'lodash';
export type EmptyQuery = [];
export type ArrayQuery = Array<Array<null | number | bigint | string>>;
export type Query = { [key: string]: null | number | bigint | string | Buffer };
export type JSONRows = Array<{ readonly json: string }>;
export type TableType =
| 'attachment_downloads'
| 'conversations'
| 'identityKeys'
| 'items'
| 'messages'
| 'preKeys'
| 'senderKeys'
| 'sessions'
| 'signedPreKeys'
| 'stickers'
| 'unprocessed';
// This value needs to be below SQLITE_MAX_VARIABLE_NUMBER.
const MAX_VARIABLE_COUNT = 100;
export function objectToJSON<T>(data: T): string {
return JSON.stringify(data);
}
export function jsonToObject<T>(json: string): T {
return JSON.parse(json);
}
//
// Database helpers
//
export function getSQLiteVersion(db: Database): string {
const { sqlite_version: version } = db
.prepare<EmptyQuery>('select sqlite_version() AS sqlite_version')
.get();
return version;
}
export function getSchemaVersion(db: Database): number {
return db.pragma('schema_version', { simple: true });
}
export function setUserVersion(db: Database, version: number): void {
if (!isNumber(version)) {
throw new Error(`setUserVersion: version ${version} is not a number`);
}
db.pragma(`user_version = ${version}`);
}
export function getUserVersion(db: Database): number {
return db.pragma('user_version', { simple: true });
}
export function getSQLCipherVersion(db: Database): string | undefined {
return db.pragma('cipher_version', { simple: true });
}
//
// Various table helpers
//
export function batchMultiVarQuery<ValueT>(
db: Database,
values: Array<ValueT>,
query: (batch: Array<ValueT>) => void
): [];
export function batchMultiVarQuery<ValueT, ResultT>(
db: Database,
values: Array<ValueT>,
query: (batch: Array<ValueT>) => Array<ResultT>
): Array<ResultT>;
export function batchMultiVarQuery<ValueT, ResultT>(
db: Database,
values: Array<ValueT>,
query:
| ((batch: Array<ValueT>) => void)
| ((batch: Array<ValueT>) => Array<ResultT>)
): Array<ResultT> {
if (values.length > MAX_VARIABLE_COUNT) {
const result: Array<ResultT> = [];
db.transaction(() => {
for (let i = 0; i < values.length; i += MAX_VARIABLE_COUNT) {
const batch = values.slice(i, i + MAX_VARIABLE_COUNT);
const batchResult = query(batch);
if (Array.isArray(batchResult)) {
result.push(...batchResult);
}
}
})();
return result;
}
const result = query(values);
return Array.isArray(result) ? result : [];
}
export function createOrUpdate<Key extends string | number>(
db: Database,
table: TableType,
data: Record<string, unknown> & { id: Key }
): void {
const { id } = data;
if (!id) {
throw new Error('createOrUpdate: Provided data did not have a truthy id');
}
db.prepare<Query>(
`
INSERT OR REPLACE INTO ${table} (
id,
json
) values (
$id,
$json
)
`
).run({
id,
json: objectToJSON(data),
});
}
export function bulkAdd(
db: Database,
table: TableType,
array: Array<Record<string, unknown> & { id: string | number }>
): void {
db.transaction(() => {
for (const data of array) {
createOrUpdate(db, table, data);
}
})();
}
export function getById<Key extends string | number, Result = unknown>(
db: Database,
table: TableType,
id: Key
): Result | undefined {
const row = db
.prepare<Query>(
`
SELECT *
FROM ${table}
WHERE id = $id;
`
)
.get({
id,
});
if (!row) {
return undefined;
}
return jsonToObject(row.json);
}
export function removeById<Key extends string | number>(
db: Database,
table: TableType,
id: Key | Array<Key>
): void {
if (!Array.isArray(id)) {
db.prepare<Query>(
`
DELETE FROM ${table}
WHERE id = $id;
`
).run({ id });
return;
}
if (!id.length) {
throw new Error('removeById: No ids to delete!');
}
const removeByIdsSync = (ids: Array<string | number>): void => {
db.prepare<ArrayQuery>(
`
DELETE FROM ${table}
WHERE id IN ( ${id.map(() => '?').join(', ')} );
`
).run(ids);
};
batchMultiVarQuery(db, id, removeByIdsSync);
}
export function removeAllFromTable(db: Database, table: TableType): void {
db.prepare<EmptyQuery>(`DELETE FROM ${table};`).run();
}
export function getAllFromTable<T>(db: Database, table: TableType): Array<T> {
const rows: JSONRows = db
.prepare<EmptyQuery>(`SELECT json FROM ${table};`)
.all();
return rows.map(row => jsonToObject(row.json));
}
export function getCountFromTable(db: Database, table: TableType): number {
const result: null | number = db
.prepare<EmptyQuery>(`SELECT count(*) from ${table};`)
.pluck(true)
.get();
if (isNumber(result)) {
return result;
}
throw new Error(`getCountFromTable: Unable to get count from table ${table}`);
}
export class TableIterator<ObjectType extends { id: string }> {
constructor(
private readonly db: Database,
private readonly table: TableType,
private readonly pageSize = 500
) {}
*[Symbol.iterator](): Iterator<ObjectType> {
const fetchObject = this.db.prepare<Query>(
`
SELECT json FROM ${this.table}
WHERE id > $id
ORDER BY id ASC
LIMIT $pageSize;
`
);
let complete = false;
let id = '';
while (!complete) {
const rows: JSONRows = fetchObject.all({
id,
pageSize: this.pageSize,
});
const messages: Array<ObjectType> = rows.map(row =>
jsonToObject(row.json)
);
yield* messages;
const lastMessage: ObjectType | undefined = last(messages);
if (lastMessage) {
({ id } = lastMessage);
}
complete = messages.length < this.pageSize;
}
}
}

View File

@ -35,21 +35,22 @@ import { requestCameraPermissions } from '../../util/callingPermissions';
import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled';
import { sleep } from '../../util/sleep';
import { LatestQueue } from '../../util/LatestQueue';
import type { UUIDStringType } from '../../types/UUID';
import type { ConversationChangedActionType } from './conversations';
import * as log from '../../logging/log';
// State
export type GroupCallPeekInfoType = {
uuids: Array<string>;
creatorUuid?: string;
uuids: Array<UUIDStringType>;
creatorUuid?: UUIDStringType;
eraId?: string;
maxDevices: number;
deviceCount: number;
};
export type GroupCallParticipantInfoType = {
uuid: string;
uuid: UUIDStringType;
demuxId: number;
hasRemoteAudio: boolean;
hasRemoteVideo: boolean;
@ -77,7 +78,7 @@ type GroupCallRingStateType =
}
| {
ringId: bigint;
ringerUuid: string;
ringerUuid: UUIDStringType;
};
export type GroupCallStateType = {
@ -99,7 +100,7 @@ export type ActiveCallStateType = {
pip: boolean;
presentingSource?: PresentedSource;
presentingSourcesAvailable?: Array<PresentableSource>;
safetyNumberChangedUuids: Array<string>;
safetyNumberChangedUuids: Array<UUIDStringType>;
settingsDialogOpen: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean;
@ -153,7 +154,7 @@ type GroupCallStateChangeArgumentType = {
};
type GroupCallStateChangeActionPayloadType = GroupCallStateChangeArgumentType & {
ourUuid: string;
ourUuid: UUIDStringType;
};
export type HangUpType = {
@ -161,7 +162,7 @@ export type HangUpType = {
};
type KeyChangedType = {
uuid: string;
uuid: UUIDStringType;
};
export type KeyChangeOkType = {
@ -176,7 +177,7 @@ export type IncomingDirectCallType = {
type IncomingGroupCallType = {
conversationId: string;
ringId: bigint;
ringerUuid: string;
ringerUuid: UUIDStringType;
};
type PeekNotConnectedGroupCallType = {
@ -262,7 +263,7 @@ export const getActiveCall = ({
// support it for direct calls.
export const getIncomingCall = (
callsByConversation: Readonly<CallsByConversationType>,
ourUuid: string
ourUuid: UUIDStringType
): undefined | DirectCallStateType | GroupCallStateType =>
Object.values(callsByConversation).find(call => {
switch (call.callMode) {
@ -281,7 +282,7 @@ export const getIncomingCall = (
export const isAnybodyElseInGroupCall = (
{ uuids }: Readonly<GroupCallPeekInfoType>,
ourUuid: string
ourUuid: UUIDStringType
): boolean => uuids.some(id => id !== ourUuid);
const getGroupCallRingState = (
@ -390,7 +391,7 @@ type IncomingGroupCallActionType = {
type KeyChangedActionType = {
type: 'calling/MARK_CALL_UNTRUSTED';
payload: {
safetyNumberChangedUuids: Array<string>;
safetyNumberChangedUuids: Array<UUIDStringType>;
};
};
@ -409,7 +410,7 @@ export type PeekNotConnectedGroupCallFulfilledActionType = {
payload: {
conversationId: string;
peekInfo: GroupCallPeekInfoType;
ourConversationId: string;
ourUuid: UUIDStringType;
};
};
@ -895,6 +896,8 @@ function peekNotConnectedGroupCall(
return;
}
const { ourUuid } = state.user;
await calling.updateCallHistoryForGroupCall(conversationId, peekInfo);
const formattedPeekInfo = calling.formatGroupCallPeekInfoForRedux(
@ -906,7 +909,7 @@ function peekNotConnectedGroupCall(
payload: {
conversationId,
peekInfo: formattedPeekInfo,
ourConversationId: state.user.ourConversationId,
ourUuid,
},
});
});
@ -1661,7 +1664,7 @@ export function reducer(
}
if (action.type === PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED) {
const { conversationId, peekInfo, ourConversationId } = action.payload;
const { conversationId, peekInfo, ourUuid } = action.payload;
const existingCall: GroupCallStateType = getGroupCall(
conversationId,
@ -1693,7 +1696,7 @@ export function reducer(
}
if (
!isAnybodyElseInGroupCall(peekInfo, ourConversationId) &&
!isAnybodyElseInGroupCall(peekInfo, ourUuid) &&
!existingCall.ringerUuid
) {
return removeConversationFromState(state, conversationId);

View File

@ -39,6 +39,7 @@ import type {
import type { BodyRangeType } from '../../types/Util';
import { CallMode } from '../../types/Calling';
import type { MediaItemType } from '../../types/MediaItem';
import type { UUIDStringType } from '../../types/UUID';
import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
@ -82,7 +83,7 @@ export type ConversationTypeType = typeof ConversationTypes[number];
export type ConversationType = {
id: string;
uuid?: string;
uuid?: UUIDStringType;
e164?: string;
name?: string;
familyName?: string;
@ -134,15 +135,15 @@ export type ConversationType = {
announcementsOnlyReady?: boolean;
expireTimer?: number;
memberships?: Array<{
conversationId: string;
uuid: UUIDStringType;
isAdmin: boolean;
}>;
pendingMemberships?: Array<{
conversationId: string;
addedByUserId?: string;
uuid: UUIDStringType;
addedByUserId?: UUIDStringType;
}>;
pendingApprovalMemberships?: Array<{
conversationId: string;
uuid: UUIDStringType;
}>;
muteExpiresAt?: number;
dontNotifyForMentionsIfMuted?: boolean;
@ -294,7 +295,7 @@ type ContactSpoofingReviewStateType =
export type ConversationsStateType = {
preJoinConversation?: PreJoinConversationType;
invitedConversationIdsForNewlyCreatedGroup?: Array<string>;
invitedUuidsForNewlyCreatedGroup?: Array<string>;
conversationLookup: ConversationLookupType;
conversationsByE164: ConversationLookupType;
conversationsByUuid: ConversationLookupType;
@ -373,8 +374,8 @@ type CantAddContactToGroupActionType = {
};
};
type ClearGroupCreationErrorActionType = { type: 'CLEAR_GROUP_CREATION_ERROR' };
type ClearInvitedConversationsForNewlyCreatedGroupActionType = {
type: 'CLEAR_INVITED_CONVERSATIONS_FOR_NEWLY_CREATED_GROUP';
type ClearInvitedUuidsForNewlyCreatedGroupActionType = {
type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP';
};
type CloseCantAddContactToGroupModalActionType = {
type: 'CLOSE_CANT_ADD_CONTACT_TO_GROUP_MODAL';
@ -470,7 +471,7 @@ type CreateGroupPendingActionType = {
type CreateGroupFulfilledActionType = {
type: 'CREATE_GROUP_FULFILLED';
payload: {
invitedConversationIds: Array<string>;
invitedUuids: Array<UUIDStringType>;
};
};
type CreateGroupRejectedActionType = {
@ -689,7 +690,7 @@ export type ConversationActionType =
| CantAddContactToGroupActionType
| ClearChangedMessagesActionType
| ClearGroupCreationErrorActionType
| ClearInvitedConversationsForNewlyCreatedGroupActionType
| ClearInvitedUuidsForNewlyCreatedGroupActionType
| ClearSelectedMessageActionType
| ClearUnreadMetricsActionType
| CloseCantAddContactToGroupModalActionType
@ -751,7 +752,7 @@ export const actions = {
cantAddContactToGroup,
clearChangedMessages,
clearGroupCreationError,
clearInvitedConversationsForNewlyCreatedGroup,
clearInvitedUuidsForNewlyCreatedGroup,
clearSelectedMessage,
clearUnreadMetrics,
closeCantAddContactToGroupModal,
@ -1320,9 +1321,9 @@ function createGroup(): ThunkAction<
dispatch({
type: 'CREATE_GROUP_FULFILLED',
payload: {
invitedConversationIds: (
conversation.get('pendingMembersV2') || []
).map(member => member.conversationId),
invitedUuids: (conversation.get('pendingMembersV2') || []).map(
member => member.uuid
),
},
});
openConversationInternal({
@ -1551,8 +1552,8 @@ function clearChangedMessages(
},
};
}
function clearInvitedConversationsForNewlyCreatedGroup(): ClearInvitedConversationsForNewlyCreatedGroupActionType {
return { type: 'CLEAR_INVITED_CONVERSATIONS_FOR_NEWLY_CREATED_GROUP' };
function clearInvitedUuidsForNewlyCreatedGroup(): ClearInvitedUuidsForNewlyCreatedGroupActionType {
return { type: 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP' };
}
function clearGroupCreationError(): ClearGroupCreationErrorActionType {
return { type: 'CLEAR_GROUP_CREATION_ERROR' };
@ -1979,8 +1980,8 @@ export function reducer(
};
}
if (action.type === 'CLEAR_INVITED_CONVERSATIONS_FOR_NEWLY_CREATED_GROUP') {
return omit(state, 'invitedConversationIdsForNewlyCreatedGroup');
if (action.type === 'CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP') {
return omit(state, 'invitedUuidsForNewlyCreatedGroup');
}
if (action.type === 'CLEAR_GROUP_CREATION_ERROR') {
@ -2169,8 +2170,7 @@ export function reducer(
// the work.
return {
...state,
invitedConversationIdsForNewlyCreatedGroup:
action.payload.invitedConversationIds,
invitedUuidsForNewlyCreatedGroup: action.payload.invitedUuids,
};
}
if (action.type === 'CREATE_GROUP_REJECTED') {

View File

@ -6,6 +6,7 @@ import { trigger } from '../../shims/events';
import type { NoopActionType } from './noop';
import type { LocalizerType } from '../../types/Util';
import { ThemeType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID';
// State
@ -15,7 +16,7 @@ export type UserStateType = {
tempPath: string;
ourConversationId: string;
ourDeviceId: number;
ourUuid: string;
ourUuid: UUIDStringType;
ourNumber: string;
platform: string;
regionCode: string;
@ -31,7 +32,7 @@ type UserChangedActionType = {
payload: {
ourConversationId?: string;
ourDeviceId?: number;
ourUuid?: string;
ourUuid?: UUIDStringType;
ourNumber?: string;
regionCode?: string;
interactionMode?: 'mouse' | 'keyboard';
@ -53,7 +54,7 @@ function userChanged(attributes: {
ourConversationId?: string;
ourDeviceId?: number;
ourNumber?: string;
ourUuid?: string;
ourUuid?: UUIDStringType;
regionCode?: string;
theme?: ThemeType;
}): UserChangedActionType {
@ -81,7 +82,7 @@ export function getEmptyState(): UserStateType {
tempPath: 'missing',
ourConversationId: 'missing',
ourDeviceId: 0,
ourUuid: 'missing',
ourUuid: '00000000-0000-4000-8000-000000000000',
ourNumber: 'missing',
regionCode: 'missing',
platform: 'missing',

View File

@ -13,6 +13,7 @@ import type {
import { getIncomingCall as getIncomingCallHelper } from '../ducks/calling';
import { getUserUuid } from './user';
import { getOwn } from '../../util/getOwn';
import type { UUIDStringType } from '../../types/UUID';
export type CallStateType = DirectCallStateType | GroupCallStateType;
@ -61,7 +62,7 @@ export const getIncomingCall = createSelector(
getUserUuid,
(
callsByConversation: CallsByConversationType,
ourUuid: string
ourUuid: UUIDStringType
): undefined | DirectCallStateType | GroupCallStateType =>
getIncomingCallHelper(callsByConversation, ourUuid)
);

View File

@ -27,6 +27,7 @@ import { filterAndSortConversationsByTitle } from '../../util/filterAndSortConve
import type { ContactNameColorType } from '../../types/Colors';
import { ContactNameColors } from '../../types/Colors';
import type { AvatarDataType } from '../../types/Avatar';
import type { UUIDStringType } from '../../types/UUID';
import { isInSystemContacts } from '../../util/isInSystemContacts';
import { sortByTitle } from '../../util/sortByTitle';
import {
@ -668,6 +669,12 @@ export const getConversationByIdSelector = createSelector(
getOwn(conversationLookup, id)
);
export const getConversationByUuidSelector = createSelector(
getConversationsByUuid,
conversationsByUuid => (uuid: UUIDStringType): undefined | ConversationType =>
getOwn(conversationsByUuid, uuid)
);
// A little optimization to reset our selector cache whenever high-level application data
// changes: regionCode and userNumber.
export const getCachedSelectorForMessage = createSelector(
@ -766,7 +773,7 @@ export const getMessageSelector = createSelector(
conversationSelector: GetConversationByIdType,
regionCode: string,
ourNumber: string,
ourUuid: string,
ourUuid: UUIDStringType,
ourConversationId: string,
callSelector: CallSelectorType,
activeCall: undefined | CallStateType,
@ -903,16 +910,13 @@ export const getConversationMessagesSelector = createSelector(
);
export const getInvitedContactsForNewlyCreatedGroup = createSelector(
getConversationLookup,
getConversationsByUuid,
getConversations,
(
conversationLookup,
{ invitedConversationIdsForNewlyCreatedGroup = [] }
{ invitedUuidsForNewlyCreatedGroup = [] }
): Array<ConversationType> =>
deconstructLookup(
conversationLookup,
invitedConversationIdsForNewlyCreatedGroup
)
deconstructLookup(conversationLookup, invitedUuidsForNewlyCreatedGroup)
);
export const getConversationsWithCustomColorSelector = createSelector(
@ -962,7 +966,7 @@ export const getGroupAdminsSelector = createSelector(
const admins: Array<ConversationType> = [];
memberships.forEach(membership => {
if (membership.isAdmin) {
const admin = conversationSelector(membership.conversationId);
const admin = conversationSelector(membership.uuid);
admins.push(admin);
}
});

View File

@ -37,6 +37,7 @@ import type { PropsType as ProfileChangeNotificationPropsType } from '../../comp
import type { QuotedAttachmentType } from '../../components/conversation/Quote';
import { getDomain, isStickerPack } from '../../types/LinkPreview';
import type { UUIDStringType } from '../../types/UUID';
import type { EmbeddedContactType } from '../../types/EmbeddedContact';
import { embeddedContactSelector } from '../../types/EmbeddedContact';
@ -99,7 +100,7 @@ export type GetPropsForBubbleOptions = Readonly<{
conversationSelector: GetConversationByIdType;
ourConversationId: string;
ourNumber?: string;
ourUuid?: string;
ourUuid: UUIDStringType;
selectedMessageId?: string;
selectedMessageCounter?: number;
regionCode: string;
@ -772,7 +773,7 @@ export function isGroupV2Change(message: MessageAttributesType): boolean {
function getPropsForGroupV2Change(
message: MessageAttributesType,
{ conversationSelector, ourConversationId }: GetPropsForBubbleOptions
{ conversationSelector, ourUuid }: GetPropsForBubbleOptions
): GroupsV2Props {
const change = message.groupV2Change;
@ -784,7 +785,7 @@ function getPropsForGroupV2Change(
return {
groupName: conversation?.type === 'group' ? conversation?.name : undefined,
ourConversationId,
ourUuid,
change,
};
}
@ -806,7 +807,7 @@ function getPropsForGroupV1Migration(
const droppedGV2MemberIds = message.droppedGV2MemberIds || [];
const invitedMembers = invitedGV2Members.map(item =>
conversationSelector(item.conversationId)
conversationSelector(item.uuid)
);
const droppedMembers = droppedGV2MemberIds.map(conversationId =>
conversationSelector(conversationId)
@ -825,7 +826,7 @@ function getPropsForGroupV1Migration(
invitedMembers: rawInvitedMembers,
} = migration;
const invitedMembers = rawInvitedMembers.map(item =>
conversationSelector(item.conversationId)
conversationSelector(item.uuid)
);
const droppedMembers = droppedMemberIds.map(conversationId =>
conversationSelector(conversationId)

View File

@ -4,6 +4,7 @@
import { createSelector } from 'reselect';
import type { LocalizerType, ThemeType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID';
import type { StateType } from '../reducer';
import type { UserStateType } from '../ducks/user';
@ -32,7 +33,7 @@ export const getUserConversationId = createSelector(
export const getUserUuid = createSelector(
getUser,
(state: UserStateType): string => state.ourUuid
(state: UserStateType): UUIDStringType => state.ourUuid
);
export const getIntl = createSelector(

View File

@ -17,6 +17,7 @@ import type {
ActiveCallType,
GroupCallRemoteParticipantType,
} from '../../types/Calling';
import type { UUIDStringType } from '../../types/UUID';
import { CallMode, CallState } from '../../types/Calling';
import type { StateType } from '../reducer';
import { missingCaseError } from '../../util/missingCaseError';
@ -117,7 +118,7 @@ const mapStateToActiveCallProp = (
}
const conversationSelectorByUuid = memoize<
(uuid: string) => undefined | ConversationType
(uuid: UUIDStringType) => undefined | ConversationType
>(uuid => {
const conversationId = window.ConversationController.ensureContactIds({
uuid,
@ -175,9 +176,9 @@ const mapStateToActiveCallProp = (
const { memberships = [] } = conversation;
for (let i = 0; i < memberships.length; i += 1) {
const { conversationId } = memberships[i];
const member = conversationSelectorByUuid(conversationId);
const { uuid } = memberships[i];
const member = conversationSelector(uuid);
if (!member) {
log.error('Group member has no corresponding conversation');
continue;

View File

@ -26,7 +26,7 @@ const mapStateToProps = (state: StateType): PropsDataType => {
let isAdmin = false;
if (contact && currentConversation && currentConversation.memberships) {
currentConversation.memberships.forEach(membership => {
if (membership.conversationId === contact.id) {
if (membership.uuid === contact.uuid) {
isMember = true;
isAdmin = membership.isAdmin;
}

View File

@ -10,6 +10,7 @@ import { ConversationDetails } from '../../components/conversation/conversation-
import {
getCandidateContactsForNewGroup,
getConversationByIdSelector,
getConversationByUuidSelector,
} from '../selectors/conversations';
import { getGroupMemberships } from '../../util/getGroupMemberships';
import { getIntl } from '../selectors/user';
@ -67,6 +68,7 @@ const mapStateToProps = (
Boolean(conversation.groupLink) &&
conversation.accessControlAddFromInviteLink !== ACCESS_ENUM.UNSATISFIABLE;
const conversationByUuidSelector = getConversationByUuidSelector(state);
return {
...props,
canEditGroupInfo,
@ -74,7 +76,7 @@ const mapStateToProps = (
conversation,
i18n: getIntl(state),
isAdmin,
...getGroupMemberships(conversation, conversationSelector),
...getGroupMemberships(conversation, conversationByUuidSelector),
userAvatarData: conversation.avatars || [],
hasGroupLink,
isGroup: conversation.type === 'group',

View File

@ -8,7 +8,10 @@ import { PendingInvites } from '../../components/conversation/conversation-detai
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { getConversationByIdSelector } from '../selectors/conversations';
import {
getConversationByIdSelector,
getConversationByUuidSelector,
} from '../selectors/conversations';
import { getGroupMemberships } from '../../util/getGroupMemberships';
import { assert } from '../../util/assert';
@ -24,6 +27,7 @@ const mapStateToProps = (
props: SmartPendingInvitesProps
): PropsType => {
const conversationSelector = getConversationByIdSelector(state);
const conversationByUuidSelector = getConversationByUuidSelector(state);
const conversation = conversationSelector(props.conversationId);
assert(
@ -33,7 +37,7 @@ const mapStateToProps = (
return {
...props,
...getGroupMemberships(conversation, conversationSelector),
...getGroupMemberships(conversation, conversationByUuidSelector),
conversation,
i18n: getIntl(state),
};

View File

@ -20,7 +20,7 @@ import type { ConversationType } from '../ducks/conversations';
import { getIntl } from '../selectors/user';
import {
getConversationByIdSelector,
getConversationByUuidSelector,
getConversationMessagesSelector,
getConversationSelector,
getConversationsByTitleSelector,
@ -208,11 +208,11 @@ const getWarning = (
return undefined;
}
const getConversationById = getConversationByIdSelector(state);
const getConversationByUuid = getConversationByUuidSelector(state);
const { memberships } = getGroupMemberships(
conversation,
getConversationById
getConversationByUuid
);
const groupNameCollisions = getCollisionsFromMemberships(memberships);
const hasGroupMembersWithSameName = !isEmpty(groupNameCollisions);
@ -244,7 +244,7 @@ const getContactSpoofingReview = (
}
const conversationSelector = getConversationSelector(state);
const getConversationById = getConversationByIdSelector(state);
const getConversationByUuid = getConversationByUuidSelector(state);
const currentConversation = conversationSelector(selectedConversationId);
@ -260,7 +260,7 @@ const getContactSpoofingReview = (
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
const { memberships } = getGroupMemberships(
currentConversation,
getConversationById
getConversationByUuid
);
const groupNameCollisions = getCollisionsFromMemberships(memberships);

View File

@ -4,8 +4,8 @@
import { assert } from 'chai';
import * as sinon from 'sinon';
import { times } from 'lodash';
import { v4 as uuid } from 'uuid';
import * as remoteConfig from '../../RemoteConfig';
import { UUID } from '../../types/UUID';
import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
@ -23,7 +23,7 @@ describe('isConversationTooBigToRing', () => {
});
const fakeMemberships = (count: number) =>
times(count, () => ({ conversationId: uuid(), isAdmin: false }));
times(count, () => ({ uuid: UUID.generate().toString(), isAdmin: false }));
afterEach(() => {
sinonSandbox.restore();

View File

@ -4,6 +4,8 @@
import { v4 as generateUuid } from 'uuid';
import { sample } from 'lodash';
import type { ConversationType } from '../../state/ducks/conversations';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import { getRandomColor } from './getRandomColor';
const FIRST_NAMES = [
@ -334,7 +336,17 @@ export function getDefaultConversation(
sharedGroupNames: [],
title: `${firstName} ${lastName}`,
type: 'direct' as const,
uuid: generateUuid(),
uuid: UUID.generate().toString(),
...overrideProps,
};
}
export function getDefaultConversationWithUuid(
overrideProps: Partial<ConversationType> = {},
uuid: UUIDStringType = UUID.generate().toString()
): ConversationType & { uuid: UUIDStringType } {
return {
...getDefaultConversation(overrideProps),
uuid,
};
}

View File

@ -48,8 +48,13 @@ import { noopAction } from '../../../state/ducks/noop';
import type { StateType } from '../../../state/reducer';
import { reducer as rootReducer } from '../../../state/reducer';
import { setupI18n } from '../../../util/setupI18n';
import { UUID } from '../../../types/UUID';
import type { UUIDStringType } from '../../../types/UUID';
import enMessages from '../../../../_locales/en/messages.json';
import { getDefaultConversation } from '../../helpers/getDefaultConversation';
import {
getDefaultConversation,
getDefaultConversationWithUuid,
} from '../../helpers/getDefaultConversation';
import {
defaultStartDirectConversationComposerState,
defaultChooseGroupMembersComposerState,
@ -69,6 +74,19 @@ describe('both/state/selectors/conversations', () => {
});
}
function makeConversationWithUuid(
id: string
): ConversationType & { uuid: UUIDStringType } {
return getDefaultConversationWithUuid(
{
id,
searchableTitle: `${id} title`,
title: `${id} title`,
},
UUID.fromPrefix(id).toString()
);
}
const i18n = setupI18n('en', enMessages);
describe('#getConversationByIdSelector', () => {
@ -374,15 +392,17 @@ describe('both/state/selectors/conversations', () => {
});
it('returns "hydrated" invited contacts', () => {
const abc = makeConversationWithUuid('abc');
const def = makeConversationWithUuid('def');
const state = {
...getEmptyRootState(),
conversations: {
...getEmptyState(),
conversationLookup: {
abc: makeConversation('abc'),
def: makeConversation('def'),
conversationsByUuid: {
[abc.uuid]: abc,
[def.uuid]: def,
},
invitedConversationIdsForNewlyCreatedGroup: ['def', 'abc'],
invitedUuidsForNewlyCreatedGroup: [def.uuid, abc.uuid],
},
};
const result = getInvitedContactsForNewlyCreatedGroup(state);
@ -1826,23 +1846,17 @@ describe('both/state/selectors/conversations', () => {
});
describe('#getContactNameColorSelector', () => {
function makeConversationWithUuid(id: string): ConversationType {
const convo = makeConversation(id);
convo.uuid = id;
return convo;
}
it('returns the right color order sorted by UUID ASC', () => {
const group = makeConversation('group');
group.type = 'group';
group.sortedGroupMembers = [
makeConversationWithUuid('zyx'),
makeConversationWithUuid('vut'),
makeConversationWithUuid('srq'),
makeConversationWithUuid('pon'),
makeConversationWithUuid('mlk'),
makeConversationWithUuid('jih'),
makeConversationWithUuid('gfe'),
makeConversationWithUuid('fff'),
makeConversationWithUuid('f00'),
makeConversationWithUuid('e00'),
makeConversationWithUuid('d00'),
makeConversationWithUuid('c00'),
makeConversationWithUuid('b00'),
makeConversationWithUuid('a00'),
];
const state = {
...getEmptyRootState(),
@ -1856,13 +1870,13 @@ describe('both/state/selectors/conversations', () => {
const contactNameColorSelector = getContactNameColorSelector(state);
assert.equal(contactNameColorSelector('group', 'gfe'), '200');
assert.equal(contactNameColorSelector('group', 'jih'), '120');
assert.equal(contactNameColorSelector('group', 'mlk'), '300');
assert.equal(contactNameColorSelector('group', 'pon'), '010');
assert.equal(contactNameColorSelector('group', 'srq'), '210');
assert.equal(contactNameColorSelector('group', 'vut'), '330');
assert.equal(contactNameColorSelector('group', 'zyx'), '230');
assert.equal(contactNameColorSelector('group', 'a00'), '200');
assert.equal(contactNameColorSelector('group', 'b00'), '120');
assert.equal(contactNameColorSelector('group', 'c00'), '300');
assert.equal(contactNameColorSelector('group', 'd00'), '010');
assert.equal(contactNameColorSelector('group', 'e00'), '210');
assert.equal(contactNameColorSelector('group', 'f00'), '330');
assert.equal(contactNameColorSelector('group', 'fff'), '230');
});
it('returns the right colors for direct conversation', () => {

View File

@ -19,7 +19,11 @@ import {
getSearchResults,
} from '../../../state/selectors/search';
import { makeLookup } from '../../../util/makeLookup';
import { getDefaultConversation } from '../../helpers/getDefaultConversation';
import { UUID } from '../../../types/UUID';
import {
getDefaultConversation,
getDefaultConversationWithUuid,
} from '../../helpers/getDefaultConversation';
import { ReadStatus } from '../../../messages/MessageReadStatus';
import type { StateType } from '../../../state/reducer';
@ -52,7 +56,7 @@ describe('both/state/selectors/search', () => {
received_at: NOW,
sent_at: NOW,
source: 'source',
sourceUuid: 'sourceUuid',
sourceUuid: UUID.generate().toString(),
timestamp: NOW,
type: 'incoming' as const,
readStatus: ReadStatus.Read,
@ -125,10 +129,9 @@ describe('both/state/selectors/search', () => {
it('returns incoming message', () => {
const searchId = 'search-id';
const fromId = 'from-id';
const toId = 'to-id';
const from = getDefaultConversation({ id: fromId });
const from = getDefaultConversationWithUuid();
const to = getDefaultConversation({ id: toId });
const state = {
@ -136,9 +139,12 @@ describe('both/state/selectors/search', () => {
conversations: {
...getEmptyConversationState(),
conversationLookup: {
[fromId]: from,
[from.id]: from,
[toId]: to,
},
conversationsByUuid: {
[from.uuid]: from,
},
},
search: {
...getEmptySearchState(),
@ -146,7 +152,7 @@ describe('both/state/selectors/search', () => {
[searchId]: {
...getDefaultMessage(searchId),
type: 'incoming' as const,
sourceUuid: fromId,
sourceUuid: from.uuid,
conversationId: toId,
snippet: 'snippet',
body: 'snippet',
@ -178,11 +184,10 @@ describe('both/state/selectors/search', () => {
it('returns the correct "from" and "to" when sent to me', () => {
const searchId = 'search-id';
const fromId = 'from-id';
const toId = fromId;
const myId = 'my-id';
const from = getDefaultConversation({ id: fromId });
const from = getDefaultConversationWithUuid();
const toId = from.uuid;
const meAsRecipient = getDefaultConversation({ id: myId });
const state = {
@ -190,9 +195,12 @@ describe('both/state/selectors/search', () => {
conversations: {
...getEmptyConversationState(),
conversationLookup: {
[fromId]: from,
[from.id]: from,
[myId]: meAsRecipient,
},
conversationsByUuid: {
[from.uuid]: from,
},
},
ourConversationId: myId,
search: {
@ -201,7 +209,7 @@ describe('both/state/selectors/search', () => {
[searchId]: {
...getDefaultMessage(searchId),
type: 'incoming' as const,
sourceUuid: fromId,
sourceUuid: from.uuid,
conversationId: toId,
snippet: 'snippet',
body: 'snippet',
@ -223,24 +231,26 @@ describe('both/state/selectors/search', () => {
it('returns outgoing message and caches appropriately', () => {
const searchId = 'search-id';
const fromId = 'from-id';
const toId = 'to-id';
const from = getDefaultConversation({ id: fromId });
const from = getDefaultConversationWithUuid();
const to = getDefaultConversation({ id: toId });
const state = {
...getEmptyRootState(),
user: {
...getEmptyUserState(),
ourConversationId: fromId,
ourConversationId: from.id,
},
conversations: {
...getEmptyConversationState(),
conversationLookup: {
[fromId]: from,
[from.id]: from,
[toId]: to,
},
conversationsByUuid: {
[from.uuid]: from,
},
},
search: {
...getEmptySearchState(),
@ -293,9 +303,9 @@ describe('both/state/selectors/search', () => {
...state,
conversations: {
...state.conversations,
conversationLookup: {
...state.conversations.conversationLookup,
[fromId]: {
conversationsByUuid: {
...state.conversations.conversationsByUuid,
[from.uuid]: {
...from,
name: 'new-name',
},

View File

@ -0,0 +1,33 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { isValidUuid } from '../../types/UUID';
describe('isValidUuid', () => {
const LOWERCASE_V4_UUID = '9cb737ce-2bb3-4c21-9fe0-d286caa0ca68';
it('returns false for non-strings', () => {
assert.isFalse(isValidUuid(undefined));
assert.isFalse(isValidUuid(null));
assert.isFalse(isValidUuid(1234));
});
it('returns false for non-UUID strings', () => {
assert.isFalse(isValidUuid(''));
assert.isFalse(isValidUuid('hello world'));
assert.isFalse(isValidUuid(` ${LOWERCASE_V4_UUID}`));
assert.isFalse(isValidUuid(`${LOWERCASE_V4_UUID} `));
});
it("returns false for UUIDs that aren't version 4", () => {
assert.isFalse(isValidUuid('a200a6e0-d2d9-11eb-bda7-dd5936a30ddf'));
assert.isFalse(isValidUuid('2adb8b83-4f2c-55ca-a481-7f98b716e615'));
});
it('returns true for v4 UUIDs', () => {
assert.isTrue(isValidUuid(LOWERCASE_V4_UUID));
assert.isTrue(isValidUuid(LOWERCASE_V4_UUID.toUpperCase()));
});
});

View File

@ -3,30 +3,34 @@
import { assert } from 'chai';
import type { ConversationType } from '../../state/ducks/conversations';
import { getDefaultConversation } from '../helpers/getDefaultConversation';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID';
import { getDefaultConversationWithUuid } from '../helpers/getDefaultConversation';
import { getGroupMemberships } from '../../util/getGroupMemberships';
describe('getGroupMemberships', () => {
const normalConversation1 = getDefaultConversation();
const normalConversation2 = getDefaultConversation();
const unregisteredConversation = getDefaultConversation({
const normalConversation1 = getDefaultConversationWithUuid();
const normalConversation2 = getDefaultConversationWithUuid();
const unregisteredConversation = getDefaultConversationWithUuid({
discoveredUnregisteredAt: Date.now(),
});
function getConversationById(id: string): undefined | ConversationType {
function getConversationByUuid(
uuid: UUIDStringType
): undefined | ConversationType {
return [
normalConversation1,
normalConversation2,
unregisteredConversation,
].find(conversation => conversation.id === id);
].find(conversation => conversation.uuid === uuid);
}
describe('memberships', () => {
it('returns an empty array if passed undefined', () => {
const conversation = {};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.memberships;
assert.isEmpty(result);
@ -35,7 +39,7 @@ describe('getGroupMemberships', () => {
it('returns an empty array if passed an empty array', () => {
const conversation = { memberships: [] };
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.memberships;
assert.isEmpty(result);
@ -45,13 +49,13 @@ describe('getGroupMemberships', () => {
const conversation = {
memberships: [
{
conversationId: 'garbage',
uuid: UUID.generate().toString(),
isAdmin: true,
},
],
};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.memberships;
assert.isEmpty(result);
@ -61,13 +65,13 @@ describe('getGroupMemberships', () => {
const conversation = {
memberships: [
{
conversationId: unregisteredConversation.id,
uuid: unregisteredConversation.uuid,
isAdmin: true,
},
],
};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.memberships;
assert.lengthOf(result, 1);
@ -81,17 +85,17 @@ describe('getGroupMemberships', () => {
const conversation = {
memberships: [
{
conversationId: normalConversation2.id,
uuid: normalConversation2.uuid,
isAdmin: false,
},
{
conversationId: normalConversation1.id,
uuid: normalConversation1.uuid,
isAdmin: true,
},
],
};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.memberships;
assert.lengthOf(result, 2);
@ -110,7 +114,7 @@ describe('getGroupMemberships', () => {
it('returns an empty array if passed undefined', () => {
const conversation = {};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingApprovalMemberships;
assert.isEmpty(result);
@ -119,7 +123,7 @@ describe('getGroupMemberships', () => {
it('returns an empty array if passed an empty array', () => {
const conversation = { pendingApprovalMemberships: [] };
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingApprovalMemberships;
assert.isEmpty(result);
@ -127,10 +131,10 @@ describe('getGroupMemberships', () => {
it("filters out conversation IDs that don't exist", () => {
const conversation = {
pendingApprovalMemberships: [{ conversationId: 'garbage' }],
pendingApprovalMemberships: [{ uuid: UUID.generate().toString() }],
};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingApprovalMemberships;
assert.isEmpty(result);
@ -138,12 +142,10 @@ describe('getGroupMemberships', () => {
it('filters out unregistered conversations', () => {
const conversation = {
pendingApprovalMemberships: [
{ conversationId: unregisteredConversation.id },
],
pendingApprovalMemberships: [{ uuid: unregisteredConversation.uuid }],
};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingApprovalMemberships;
assert.isEmpty(result);
@ -152,12 +154,12 @@ describe('getGroupMemberships', () => {
it('hydrates pending-approval memberships', () => {
const conversation = {
pendingApprovalMemberships: [
{ conversationId: normalConversation2.id },
{ conversationId: normalConversation1.id },
{ uuid: normalConversation2.uuid },
{ uuid: normalConversation1.uuid },
],
};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingApprovalMemberships;
assert.lengthOf(result, 2);
@ -170,7 +172,7 @@ describe('getGroupMemberships', () => {
it('returns an empty array if passed undefined', () => {
const conversation = {};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingMemberships;
assert.isEmpty(result);
@ -179,7 +181,7 @@ describe('getGroupMemberships', () => {
it('returns an empty array if passed an empty array', () => {
const conversation = { pendingMemberships: [] };
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingMemberships;
assert.isEmpty(result);
@ -188,11 +190,14 @@ describe('getGroupMemberships', () => {
it("filters out conversation IDs that don't exist", () => {
const conversation = {
pendingMemberships: [
{ conversationId: 'garbage', addedByUserId: normalConversation1.id },
{
uuid: UUID.generate().toString(),
addedByUserId: normalConversation1.uuid,
},
],
};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingMemberships;
assert.isEmpty(result);
@ -202,37 +207,40 @@ describe('getGroupMemberships', () => {
const conversation = {
pendingMemberships: [
{
conversationId: unregisteredConversation.id,
addedByUserId: normalConversation1.id,
uuid: unregisteredConversation.uuid,
addedByUserId: normalConversation1.uuid,
},
],
};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingMemberships;
assert.isEmpty(result);
});
it('hydrates pending memberships', () => {
const abc = UUID.generate().toString();
const xyz = UUID.generate().toString();
const conversation = {
pendingMemberships: [
{ conversationId: normalConversation2.id, addedByUserId: 'abc' },
{ conversationId: normalConversation1.id, addedByUserId: 'xyz' },
{ uuid: normalConversation2.uuid, addedByUserId: abc },
{ uuid: normalConversation1.uuid, addedByUserId: xyz },
],
};
const result = getGroupMemberships(conversation, getConversationById)
const result = getGroupMemberships(conversation, getConversationByUuid)
.pendingMemberships;
assert.lengthOf(result, 2);
assert.deepEqual(result[0], {
member: normalConversation2,
metadata: { addedByUserId: 'abc' },
metadata: { addedByUserId: abc },
});
assert.deepEqual(result[1], {
member: normalConversation1,
metadata: { addedByUserId: 'xyz' },
metadata: { addedByUserId: xyz },
});
});
});

View File

@ -1,33 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { isValidGuid } from '../../util/isValidGuid';
describe('isValidGuid', () => {
const LOWERCASE_V4_UUID = '9cb737ce-2bb3-4c21-9fe0-d286caa0ca68';
it('returns false for non-strings', () => {
assert.isFalse(isValidGuid(undefined));
assert.isFalse(isValidGuid(null));
assert.isFalse(isValidGuid(1234));
});
it('returns false for non-UUID strings', () => {
assert.isFalse(isValidGuid(''));
assert.isFalse(isValidGuid('hello world'));
assert.isFalse(isValidGuid(` ${LOWERCASE_V4_UUID}`));
assert.isFalse(isValidGuid(`${LOWERCASE_V4_UUID} `));
});
it("returns false for UUIDs that aren't version 4", () => {
assert.isFalse(isValidGuid('a200a6e0-d2d9-11eb-bda7-dd5936a30ddf'));
assert.isFalse(isValidGuid('2adb8b83-4f2c-55ca-a481-7f98b716e615'));
});
it('returns true for v4 UUIDs', () => {
assert.isTrue(isValidGuid(LOWERCASE_V4_UUID));
assert.isTrue(isValidGuid(LOWERCASE_V4_UUID.toUpperCase()));
});
});

View File

@ -10,7 +10,6 @@ import {
SenderKeyRecord,
SessionRecord,
} from '@signalapp/signal-client';
import { v4 as getGuid } from 'uuid';
import { signal } from '../protobuf/compiled';
import { sessionStructureToBytes } from '../util/sessionTranslation';
@ -36,8 +35,8 @@ const {
} = signal.proto.storage;
describe('SignalProtocolStore', () => {
const ourUuid = new UUID(getGuid());
const theirUuid = new UUID(getGuid());
const ourUuid = UUID.generate();
const theirUuid = UUID.generate();
let store: SignalProtocolStore;
let identityKey: KeyPairType;
let testKey: KeyPairType;
@ -170,7 +169,7 @@ describe('SignalProtocolStore', () => {
describe('senderKeys', () => {
it('roundtrips in memory', async () => {
const distributionId = window.getGuid();
const distributionId = UUID.generate().toString();
const expected = getSenderKeyRecord();
const deviceId = 1;
@ -200,7 +199,7 @@ describe('SignalProtocolStore', () => {
});
it('roundtrips through database', async () => {
const distributionId = window.getGuid();
const distributionId = UUID.generate().toString();
const expected = getSenderKeyRecord();
const deviceId = 1;

View File

@ -3,6 +3,7 @@
import { assert } from 'chai';
import { SendStatus } from '../../messages/MessageSendState';
import { UUID } from '../../types/UUID';
describe('Conversations', () => {
async function resetConversationController(): Promise<void> {
@ -16,14 +17,14 @@ describe('Conversations', () => {
it('updates lastMessage even in race conditions with db', async () => {
const ourNumber = '+15550000000';
const ourUuid = window.getGuid();
const ourUuid = UUID.generate().toString();
// Creating a fake conversation
const conversation = new window.Whisper.Conversation({
avatars: [],
id: window.getGuid(),
id: UUID.generate().toString(),
e164: '+15551234567',
uuid: window.getGuid(),
uuid: UUID.generate().toString(),
type: 'private',
inbox_position: 0,
isPinned: false,
@ -56,7 +57,7 @@ describe('Conversations', () => {
hasAttachments: false,
hasFileAttachments: false,
hasVisualMediaAttachments: false,
id: window.getGuid(),
id: UUID.generate().toString(),
received_at: now,
sent_at: now,
timestamp: now,

View File

@ -10,6 +10,7 @@ import MessageSender from '../../textsecure/SendMessage';
import type { WebAPIType } from '../../textsecure/WebAPI';
import type { CallbackResultType } from '../../textsecure/Types.d';
import type { StorageAccessType } from '../../types/Storage.d';
import { UUID } from '../../types/UUID';
import { SignalService as Proto } from '../../protobuf';
describe('Message', () => {
@ -32,7 +33,7 @@ describe('Message', () => {
const source = '+1 415-555-5555';
const me = '+14155555556';
const ourUuid = window.getGuid();
const ourUuid = UUID.generate().toString();
function createMessage(attrs: { [key: string]: unknown }) {
const messages = new window.Whisper.MessageCollection();
@ -138,7 +139,7 @@ describe('Message', () => {
const fakeDataMessage = new Uint8Array(0);
const conversation1Uuid = conversation1.get('uuid');
const ignoredUuid = window.getGuid();
const ignoredUuid = UUID.generate().toString();
if (!conversation1Uuid) {
throw new Error('Test setup failed: conversation1 should have a UUID');

View File

@ -12,11 +12,10 @@ import type { MentionCompletionOptions } from '../../../quill/mentions/completio
import { MentionCompletion } from '../../../quill/mentions/completion';
import type { ConversationType } from '../../../state/ducks/conversations';
import { MemberRepository } from '../../../quill/memberRepository';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
import { getDefaultConversationWithUuid } from '../../../test-both/helpers/getDefaultConversation';
const me: ConversationType = getDefaultConversation({
const me: ConversationType = getDefaultConversationWithUuid({
id: '666777',
uuid: 'pqrstuv',
title: 'Fred Savage',
firstName: 'Fred',
profileName: 'Fred S.',
@ -28,9 +27,8 @@ const me: ConversationType = getDefaultConversation({
});
const members: Array<ConversationType> = [
getDefaultConversation({
getDefaultConversationWithUuid({
id: '555444',
uuid: 'abcdefg',
title: 'Mahershala Ali',
firstName: 'Mahershala',
profileName: 'Mahershala A.',
@ -39,9 +37,8 @@ const members: Array<ConversationType> = [
markedUnread: false,
areWeAdmin: false,
}),
getDefaultConversation({
getDefaultConversationWithUuid({
id: '333222',
uuid: 'hijklmno',
title: 'Shia LaBeouf',
firstName: 'Shia',
profileName: 'Shia L.',

View File

@ -2,10 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as sinon from 'sinon';
import { v4 as uuid } from 'uuid';
import { times } from 'lodash';
import { ConversationModel } from '../models/conversations';
import type { ConversationAttributesType } from '../model-types.d';
import { UUID } from '../types/UUID';
import { routineProfileRefresh } from '../routineProfileRefresh';
import * as getProfileStub from '../util/getProfile';
@ -26,12 +26,12 @@ describe('routineProfileRefresh', () => {
overrideAttributes: Partial<ConversationAttributesType> = {}
): ConversationModel {
const result = new ConversationModel({
accessKey: uuid(),
accessKey: UUID.generate().toString(),
active_at: Date.now(),
draftAttachments: [],
draftBodyRanges: [],
draftTimestamp: null,
id: uuid(),
id: UUID.generate().toString(),
inbox_position: 0,
isPinned: false,
lastMessageDeletedForEveryone: false,
@ -43,7 +43,7 @@ describe('routineProfileRefresh', () => {
messageRequestResponseType: 0,
muteExpiresAt: 0,
profileAvatar: undefined,
profileKeyCredential: uuid(),
profileKeyCredential: UUID.generate().toString(),
profileKeyVersion: '',
profileSharing: true,
quotedMessageId: null,
@ -52,7 +52,7 @@ describe('routineProfileRefresh', () => {
sharedGroupNames: [],
timestamp: Date.now(),
type: 'private',
uuid: uuid(),
uuid: UUID.generate().toString(),
version: 2,
...overrideAttributes,
});
@ -85,7 +85,7 @@ describe('routineProfileRefresh', () => {
await routineProfileRefresh({
allConversations: [conversation1, conversation2],
ourConversationId: uuid(),
ourConversationId: UUID.generate().toString(),
storage,
});
@ -99,7 +99,7 @@ describe('routineProfileRefresh', () => {
await routineProfileRefresh({
allConversations: [conversation1, conversation2],
ourConversationId: uuid(),
ourConversationId: UUID.generate().toString(),
storage: makeStorage(),
});
@ -124,7 +124,7 @@ describe('routineProfileRefresh', () => {
await routineProfileRefresh({
allConversations: [recentlyActive, inactive, neverActive],
ourConversationId: uuid(),
ourConversationId: UUID.generate().toString(),
storage: makeStorage(),
});
@ -176,7 +176,7 @@ describe('routineProfileRefresh', () => {
await routineProfileRefresh({
allConversations: [neverRefreshed, recentlyFetched],
ourConversationId: uuid(),
ourConversationId: UUID.generate().toString(),
storage: makeStorage(),
});
@ -218,7 +218,7 @@ describe('routineProfileRefresh', () => {
memberWhoHasRecentlyRefreshed,
groupConversation,
],
ourConversationId: uuid(),
ourConversationId: UUID.generate().toString(),
storage: makeStorage(),
});

View File

@ -23,6 +23,8 @@ import {
GroupCallConnectionState,
GroupCallJoinState,
} from '../../../types/Calling';
import { UUID } from '../../../types/UUID';
import type { UUIDStringType } from '../../../types/UUID';
describe('calling duck', () => {
const stateWithDirectCall: CallingStateType = {
@ -68,6 +70,11 @@ describe('calling duck', () => {
},
};
const creatorUuid = UUID.generate().toString();
const differentCreatorUuid = UUID.generate().toString();
const remoteUuid = UUID.generate().toString();
const ringerUuid = UUID.generate().toString();
const stateWithGroupCall = {
...getEmptyState(),
callsByConversation: {
@ -77,15 +84,15 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: ['456'],
creatorUuid: '456',
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
@ -107,7 +114,7 @@ describe('calling duck', () => {
'fake-group-call-conversation-id'
],
ringId: BigInt(123),
ringerUuid: '789',
ringerUuid: UUID.generate().toString(),
},
},
};
@ -127,7 +134,7 @@ describe('calling duck', () => {
},
};
const ourUuid = 'ebf5fd79-9344-4ec1-b5c9-af463572caf5';
const ourUuid = UUID.generate().toString();
const getEmptyRootState = () => {
const rootState = rootReducer(undefined, noopAction());
@ -607,7 +614,7 @@ describe('calling duck', () => {
],
connectionState: GroupCallConnectionState.NotConnected,
ringId: BigInt(123),
ringerUuid: '789',
ringerUuid: UUID.generate().toString(),
},
},
};
@ -899,15 +906,15 @@ describe('calling duck', () => {
hasLocalAudio: true,
hasLocalVideo: false,
peekInfo: {
uuids: ['456'],
creatorUuid: '456',
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
@ -927,15 +934,15 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.Joining,
peekInfo: {
uuids: ['456'],
creatorUuid: '456',
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
@ -1022,7 +1029,7 @@ describe('calling duck', () => {
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
@ -1048,7 +1055,7 @@ describe('calling duck', () => {
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
@ -1071,7 +1078,7 @@ describe('calling duck', () => {
'fake-group-call-conversation-id'
],
ringId: BigInt(456),
ringerUuid: '55addfd8-09ed-4f5b-b42e-01058898d13b',
ringerUuid,
},
},
};
@ -1090,7 +1097,7 @@ describe('calling duck', () => {
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
@ -1107,7 +1114,7 @@ describe('calling duck', () => {
{
callMode: CallMode.Group,
ringId: BigInt(456),
ringerUuid: '55addfd8-09ed-4f5b-b42e-01058898d13b',
ringerUuid,
}
);
});
@ -1122,7 +1129,7 @@ describe('calling duck', () => {
'fake-group-call-conversation-id'
],
ringId: BigInt(456),
ringerUuid: '55addfd8-09ed-4f5b-b42e-01058898d13b',
ringerUuid,
},
},
};
@ -1141,7 +1148,7 @@ describe('calling duck', () => {
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
@ -1179,7 +1186,7 @@ describe('calling duck', () => {
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
@ -1210,7 +1217,7 @@ describe('calling duck', () => {
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
@ -1251,7 +1258,7 @@ describe('calling duck', () => {
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 456,
hasRemoteAudio: false,
hasRemoteVideo: true,
@ -1423,7 +1430,7 @@ describe('calling duck', () => {
const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96',
ringerUuid,
});
const result = reducer(stateWithIncomingGroupCall, action);
@ -1446,7 +1453,7 @@ describe('calling duck', () => {
const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96',
ringerUuid,
});
const result = reducer(state, action);
@ -1457,7 +1464,7 @@ describe('calling duck', () => {
const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96',
ringerUuid,
});
const result = reducer(getEmptyState(), action);
@ -1475,7 +1482,7 @@ describe('calling duck', () => {
},
remoteParticipants: [],
ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96',
ringerUuid,
}
);
});
@ -1484,7 +1491,7 @@ describe('calling duck', () => {
const action = receiveIncomingGroupCall({
conversationId: 'fake-group-call-conversation-id',
ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96',
ringerUuid,
});
const result = reducer(stateWithGroupCall, action);
@ -1492,7 +1499,7 @@ describe('calling duck', () => {
result.callsByConversation['fake-group-call-conversation-id'],
{
ringId: BigInt(456),
ringerUuid: '208b8ce6-3a73-48ee-9c8a-32e6196f6e96',
ringerUuid,
}
);
});
@ -1644,15 +1651,15 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: ['456'],
creatorUuid: '456',
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
@ -1670,15 +1677,15 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: ['456'],
creatorUuid: '456',
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
@ -1734,7 +1741,7 @@ describe('calling duck', () => {
peekInfo: undefined,
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
@ -1749,8 +1756,8 @@ describe('calling duck', () => {
const call =
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: ['456'],
creatorUuid: '456',
uuids: [creatorUuid],
creatorUuid,
eraId: 'xyz',
maxDevices: 16,
deviceCount: 1,
@ -1769,15 +1776,15 @@ describe('calling duck', () => {
connectionState: GroupCallConnectionState.Connected,
joinState: GroupCallJoinState.NotJoined,
peekInfo: {
uuids: ['999'],
creatorUuid: '999',
uuids: [differentCreatorUuid],
creatorUuid: differentCreatorUuid,
eraId: 'abc',
maxDevices: 5,
deviceCount: 1,
},
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
@ -1792,8 +1799,8 @@ describe('calling duck', () => {
const call =
result.callsByConversation['fake-group-call-conversation-id'];
assert.deepEqual(call?.callMode === CallMode.Group && call.peekInfo, {
uuids: ['999'],
creatorUuid: '999',
uuids: [differentCreatorUuid],
creatorUuid: differentCreatorUuid,
eraId: 'abc',
maxDevices: 5,
deviceCount: 1,
@ -1810,7 +1817,7 @@ describe('calling duck', () => {
'fake-group-call-conversation-id'
],
ringId: BigInt(987),
ringerUuid: 'd59f05f7-3be8-4d44-a1e8-0d7cb5677ed8',
ringerUuid,
},
},
},
@ -1825,7 +1832,7 @@ describe('calling duck', () => {
peekInfo: undefined,
remoteParticipants: [
{
uuid: '123',
uuid: remoteUuid,
demuxId: 123,
hasRemoteAudio: true,
hasRemoteVideo: true,
@ -1844,10 +1851,7 @@ describe('calling duck', () => {
}
assert.strictEqual(call.ringId, BigInt(987));
assert.strictEqual(
call.ringerUuid,
'd59f05f7-3be8-4d44-a1e8-0d7cb5677ed8'
);
assert.strictEqual(call.ringerUuid, ringerUuid);
});
});
@ -2036,47 +2040,33 @@ describe('calling duck', () => {
});
describe('isAnybodyElseInGroupCall', () => {
const fakePeekInfo = (uuids: Array<string>) => ({
const fakePeekInfo = (uuids: Array<UUIDStringType>) => ({
uuids,
maxDevices: 5,
deviceCount: uuids.length,
});
it('returns false if the peek info has no participants', () => {
assert.isFalse(
isAnybodyElseInGroupCall(
fakePeekInfo([]),
'2cd7b14c-3433-4b3c-9685-1ef1e2d26db2'
)
);
assert.isFalse(isAnybodyElseInGroupCall(fakePeekInfo([]), remoteUuid));
});
it('returns false if the peek info has one participant, you', () => {
assert.isFalse(
isAnybodyElseInGroupCall(
fakePeekInfo(['2cd7b14c-3433-4b3c-9685-1ef1e2d26db2']),
'2cd7b14c-3433-4b3c-9685-1ef1e2d26db2'
)
isAnybodyElseInGroupCall(fakePeekInfo([creatorUuid]), creatorUuid)
);
});
it('returns true if the peek info has one participant, someone else', () => {
assert.isTrue(
isAnybodyElseInGroupCall(
fakePeekInfo(['ca0ae16c-2936-4c68-86b1-a6f82e8fe67f']),
'2cd7b14c-3433-4b3c-9685-1ef1e2d26db2'
)
isAnybodyElseInGroupCall(fakePeekInfo([creatorUuid]), remoteUuid)
);
});
it('returns true if the peek info has two participants, you and someone else', () => {
assert.isTrue(
isAnybodyElseInGroupCall(
fakePeekInfo([
'ca0ae16c-2936-4c68-86b1-a6f82e8fe67f',
'2cd7b14c-3433-4b3c-9685-1ef1e2d26db2',
]),
'2cd7b14c-3433-4b3c-9685-1ef1e2d26db2'
fakePeekInfo([creatorUuid, remoteUuid]),
remoteUuid
)
);
});

View File

@ -28,8 +28,12 @@ import {
import { ReadStatus } from '../../../messages/MessageReadStatus';
import { ContactSpoofingType } from '../../../util/contactSpoofing';
import { CallMode } from '../../../types/Calling';
import { UUID } from '../../../types/UUID';
import * as groups from '../../../groups';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
import {
getDefaultConversation,
getDefaultConversationWithUuid,
} from '../../../test-both/helpers/getDefaultConversation';
import { getDefaultAvatars } from '../../../types/Avatar';
import {
defaultStartDirectConversationComposerState,
@ -40,7 +44,7 @@ import {
const {
cantAddContactToGroup,
clearGroupCreationError,
clearInvitedConversationsForNewlyCreatedGroup,
clearInvitedUuidsForNewlyCreatedGroup,
closeCantAddContactToGroupModal,
closeContactSpoofingReview,
closeMaximumGroupSizeModal,
@ -225,26 +229,24 @@ describe('both/state/ducks/conversations', () => {
});
it('adds and removes uuid-only contact', () => {
const removed = getDefaultConversation({
const removed = getDefaultConversationWithUuid({
id: 'id-removed',
uuid: 'uuid-removed',
e164: undefined,
});
const state = {
...getEmptyState(),
conversationsByuuid: {
'uuid-removed': removed,
[removed.uuid]: removed,
},
};
const added = getDefaultConversation({
const added = getDefaultConversationWithUuid({
id: 'id-added',
uuid: 'uuid-added',
e164: undefined,
});
const expected = {
'uuid-added': added,
[added.uuid]: added,
};
const actual = updateConversationLookups(added, removed, state);
@ -307,6 +309,7 @@ describe('both/state/ducks/conversations', () => {
const messageId = 'message-guid-1';
const messageIdTwo = 'message-guid-2';
const messageIdThree = 'message-guid-3';
const sourceUuid = UUID.generate().toString();
function getDefaultMessage(id: string): MessageType {
return {
@ -316,7 +319,7 @@ describe('both/state/ducks/conversations', () => {
received_at: previousTime,
sent_at: previousTime,
source: 'source',
sourceUuid: 'sourceUuid',
sourceUuid,
timestamp: previousTime,
type: 'incoming' as const,
readStatus: ReadStatus.Read,
@ -491,16 +494,19 @@ describe('both/state/ducks/conversations', () => {
});
});
describe('CLEAR_INVITED_CONVERSATIONS_FOR_NEWLY_CREATED_GROUP', () => {
it('clears the list of invited conversation IDs', () => {
describe('CLEAR_INVITED_UUIDS_FOR_NEWLY_CREATED_GROUP', () => {
it('clears the list of invited conversation UUIDs', () => {
const state = {
...getEmptyState(),
invitedConversationIdsForNewlyCreatedGroup: ['abc123', 'def456'],
invitedUuidsForNewlyCreatedGroup: [
UUID.generate().toString(),
UUID.generate().toString(),
],
};
const action = clearInvitedConversationsForNewlyCreatedGroup();
const action = clearInvitedUuidsForNewlyCreatedGroup();
const result = reducer(state, action);
assert.isUndefined(result.invitedConversationIdsForNewlyCreatedGroup);
assert.isUndefined(result.invitedUuidsForNewlyCreatedGroup);
});
});
@ -775,13 +781,14 @@ describe('both/state/ducks/conversations', () => {
});
it('dispatches a CREATE_GROUP_FULFILLED event (which updates the newly-created conversation IDs), triggers a showConversation event and switches to the associated conversation on success', async () => {
const abc = UUID.fromPrefix('abc').toString();
createGroupStub.resolves({
id: '9876',
get: (key: string) => {
if (key !== 'pendingMembersV2') {
throw new Error('This getter is not set up for this test');
}
return [{ conversationId: 'xyz999' }];
return [{ uuid: abc }];
},
});
@ -805,14 +812,12 @@ describe('both/state/ducks/conversations', () => {
sinon.assert.calledWith(dispatch, {
type: 'CREATE_GROUP_FULFILLED',
payload: { invitedConversationIds: ['xyz999'] },
payload: { invitedUuids: [abc] },
});
const fulfilledAction = dispatch.getCall(1).args[0];
const result = reducer(conversationsState, fulfilledAction);
assert.deepEqual(result.invitedConversationIdsForNewlyCreatedGroup, [
'xyz999',
]);
assert.deepEqual(result.invitedUuidsForNewlyCreatedGroup, [abc]);
sinon.assert.calledWith(dispatch, {
type: 'SWITCH_TO_ASSOCIATED_VIEW',
@ -1765,14 +1770,12 @@ describe('both/state/ducks/conversations', () => {
});
describe('COLORS_CHANGED', () => {
const abc = getDefaultConversation({
const abc = getDefaultConversationWithUuid({
id: 'abc',
uuid: 'abc',
conversationColor: 'wintergreen',
});
const def = getDefaultConversation({
const def = getDefaultConversationWithUuid({
id: 'def',
uuid: 'def',
conversationColor: 'infrared',
});
const ghi = getDefaultConversation({
@ -1820,8 +1823,12 @@ describe('both/state/ducks/conversations', () => {
assert.isUndefined(nextState.conversationLookup.def.conversationColor);
assert.isUndefined(nextState.conversationLookup.ghi.conversationColor);
assert.isUndefined(nextState.conversationLookup.jkl.conversationColor);
assert.isUndefined(nextState.conversationsByUuid.abc.conversationColor);
assert.isUndefined(nextState.conversationsByUuid.def.conversationColor);
assert.isUndefined(
nextState.conversationsByUuid[abc.uuid].conversationColor
);
assert.isUndefined(
nextState.conversationsByUuid[def.uuid].conversationColor
);
assert.isUndefined(nextState.conversationsByE164.ghi.conversationColor);
assert.isUndefined(
nextState.conversationsByGroupId.jkl.conversationColor

View File

@ -5,10 +5,10 @@
import { assert } from 'chai';
import sinon from 'sinon';
import { v4 as uuid } from 'uuid';
import { ConversationModel } from '../models/conversations';
import type { ConversationAttributesType } from '../model-types.d';
import type SendMessage from '../textsecure/SendMessage';
import { UUID } from '../types/UUID';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
@ -41,7 +41,7 @@ describe('updateConversationsWithUuidLookup', () => {
'FakeConversationController is not set up for this case (E164 must be provided)'
);
assert(
uuid,
uuidFromServer,
'FakeConversationController is not set up for this case (UUID must be provided)'
);
assert(
@ -81,7 +81,7 @@ describe('updateConversationsWithUuidLookup', () => {
attributes: Readonly<Partial<ConversationAttributesType>> = {}
): ConversationModel {
return new ConversationModel({
id: uuid(),
id: UUID.generate().toString(),
inbox_position: 0,
isPinned: false,
lastMessageDeletedForEveryone: false,
@ -128,7 +128,7 @@ describe('updateConversationsWithUuidLookup', () => {
conversationController: new FakeConversationController(),
conversations: [
createConversation(),
createConversation({ uuid: uuid() }),
createConversation({ uuid: UUID.generate().toString() }),
],
messaging: fakeMessaging,
});
@ -140,11 +140,11 @@ describe('updateConversationsWithUuidLookup', () => {
const conversation1 = createConversation({ e164: '+13215559876' });
const conversation2 = createConversation({
e164: '+16545559876',
uuid: 'should be overwritten',
uuid: UUID.generate().toString(), // should be overwritten
});
const uuid1 = uuid();
const uuid2 = uuid();
const uuid1 = UUID.generate().toString();
const uuid2 = UUID.generate().toString();
fakeGetUuidsForE164s.resolves({
'+13215559876': uuid1,
@ -187,7 +187,7 @@ describe('updateConversationsWithUuidLookup', () => {
});
it("doesn't mark conversations unregistered if we already had a UUID for them, even if the server doesn't return one", async () => {
const existingUuid = uuid();
const existingUuid = UUID.generate().toString();
const conversation = createConversation({
e164: '+13215559876',
uuid: existingUuid,

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { v4 as uuid } from 'uuid';
import * as Bytes from '../../Bytes';
import {
@ -11,6 +10,7 @@ import {
decryptProfileName,
decryptProfile,
} from '../../Crypto';
import { UUID } from '../../types/UUID';
import { encryptProfileData } from '../../util/encryptProfileData';
describe('encryptProfileData', () => {
@ -22,7 +22,7 @@ describe('encryptProfileData', () => {
familyName: 'Kid',
firstName: 'Zombie',
profileKey: Bytes.toBase64(keyBuffer),
uuid: uuid(),
uuid: UUID.generate().toString(),
// To satisfy TS
acceptedMessageRequest: true,

View File

@ -5,11 +5,10 @@ import { assert } from 'chai';
import type { ConversationType } from '../../state/ducks/conversations';
import { MemberRepository } from '../../quill/memberRepository';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { getDefaultConversationWithUuid } from '../../test-both/helpers/getDefaultConversation';
const memberMahershala: ConversationType = getDefaultConversation({
const memberMahershala: ConversationType = getDefaultConversationWithUuid({
id: '555444',
uuid: 'abcdefg',
title: 'Pal',
firstName: 'Mahershala',
profileName: 'Mr Ali',
@ -20,9 +19,8 @@ const memberMahershala: ConversationType = getDefaultConversation({
areWeAdmin: false,
});
const memberShia: ConversationType = getDefaultConversation({
const memberShia: ConversationType = getDefaultConversationWithUuid({
id: '333222',
uuid: 'hijklmno',
title: 'Buddy',
firstName: 'Shia',
profileName: 'Sr LaBeouf',
@ -35,9 +33,8 @@ const memberShia: ConversationType = getDefaultConversation({
const members: Array<ConversationType> = [memberMahershala, memberShia];
const singleMember: ConversationType = getDefaultConversation({
const singleMember: ConversationType = getDefaultConversationWithUuid({
id: '666777',
uuid: 'pqrstuv',
title: 'The Guy',
firstName: 'Jeff',
profileName: 'Jr Klaus',
@ -85,7 +82,7 @@ describe('MemberRepository', () => {
it('returns a matched member', () => {
const memberRepository = new MemberRepository(members);
assert.isDefined(memberRepository.getMemberByUuid('abcdefg'));
assert.isDefined(memberRepository.getMemberByUuid(memberMahershala.uuid));
});
it('returns undefined when it does not have the member', () => {

View File

@ -8,7 +8,7 @@ import Delta from 'quill-delta';
import { matchMention } from '../../../quill/mentions/matchers';
import { MemberRepository } from '../../../quill/memberRepository';
import type { ConversationType } from '../../../state/ducks/conversations';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
import { getDefaultConversationWithUuid } from '../../../test-both/helpers/getDefaultConversation';
class FakeTokenList<T> extends Array<T> {
constructor(elements: Array<T>) {
@ -38,9 +38,8 @@ const createMockMentionBlotElement = (
dataset: Record<string, string>
): HTMLElement => createMockElement('mention-blot', dataset);
const memberMahershala: ConversationType = getDefaultConversation({
const memberMahershala: ConversationType = getDefaultConversationWithUuid({
id: '555444',
uuid: 'abcdefg',
title: 'Mahershala Ali',
firstName: 'Mahershala',
profileName: 'Mahershala A.',
@ -50,9 +49,8 @@ const memberMahershala: ConversationType = getDefaultConversation({
areWeAdmin: false,
});
const memberShia: ConversationType = getDefaultConversation({
const memberShia: ConversationType = getDefaultConversationWithUuid({
id: '333222',
uuid: 'hijklmno',
title: 'Shia LaBeouf',
firstName: 'Shia',
profileName: 'Shia L.',

View File

@ -6,7 +6,8 @@ import type { Database } from 'better-sqlite3';
import SQL from 'better-sqlite3';
import { v4 as generateGuid } from 'uuid';
import { SCHEMA_VERSIONS } from '../sql/Server';
import { SCHEMA_VERSIONS } from '../sql/migrations';
import { consoleLogger } from '../util/consoleLogger';
const OUR_UUID = generateGuid();
@ -17,7 +18,7 @@ describe('SQL migrations test', () => {
const startVersion = db.pragma('user_version', { simple: true });
for (const run of SCHEMA_VERSIONS) {
run(startVersion, db);
run(startVersion, db, consoleLogger);
const currentVersion = db.pragma('user_version', { simple: true });
@ -616,4 +617,157 @@ describe('SQL migrations test', () => {
assert.sameDeepMembers(reactionMessageIds, [MESSAGE_ID_2, MESSAGE_ID_3]);
});
});
describe('updateToSchemaVersion43', () => {
it('remaps conversation ids to UUIDs in groups and messages', () => {
updateToVersion(42);
const UUID_A = generateGuid();
const UUID_B = generateGuid();
const UUID_C = generateGuid();
const rawConvoC = {
id: 'c',
uuid: UUID_C,
membersV2: [
{ conversationId: 'a', joinedAtVersion: 1 },
{ conversationId: 'b', joinedAtVersion: 2 },
{ conversationId: 'z', joinedAtVersion: 3 },
],
pendingMembersV2: [
{ conversationId: 'a', addedByUserId: 'b', timestamp: 4 },
{ conversationId: 'b', addedByUserId: UUID_A, timestamp: 5 },
{ conversationId: 'z', timestamp: 6 },
],
pendingAdminApprovalV2: [
{ conversationId: 'a', timestamp: 6 },
{ conversationId: 'b', timestamp: 7 },
{ conversationId: 'z', timestamp: 8 },
],
};
const CHANGE_TYPES = [
'member-add',
'member-add-from-link',
'member-add-from-admin-approval',
'member-privilege',
'member-remove',
'pending-add-one',
'admin-approval-add-one',
];
const CHANGE_TYPES_WITH_INVITER = [
'member-add-from-invite',
'pending-remove-one',
'pending-remove-many',
'admin-approval-remove-one',
];
db.exec(
`
INSERT INTO conversations
(id, uuid, json)
VALUES
('a', '${UUID_A}', '${JSON.stringify({ id: 'a', uuid: UUID_A })}'),
('b', '${UUID_B}', '${JSON.stringify({ id: 'b', uuid: UUID_B })}'),
('c', '${UUID_C}', '${JSON.stringify(rawConvoC)}');
INSERT INTO messages
(id, json)
VALUES
('m', '${JSON.stringify({
id: 'm',
groupV2Change: {
from: 'a',
details: [
...CHANGE_TYPES.map(type => ({ type, conversationId: 'b' })),
...CHANGE_TYPES_WITH_INVITER.map(type => {
return { type, conversationId: 'c', inviter: 'a' };
}),
],
},
sourceUuid: 'a',
invitedGV2Members: [
{
conversationId: 'b',
addedByUserId: 'c',
},
],
})}'),
('n', '${JSON.stringify({
id: 'n',
groupV2Change: {
from: 'not-found',
details: [],
},
sourceUuid: 'a',
})}');
`
);
updateToVersion(43);
const { members, json: convoJSON } = db
.prepare('SELECT members, json FROM conversations WHERE id = "c"')
.get();
assert.strictEqual(members, `${UUID_A} ${UUID_B}`);
assert.deepStrictEqual(JSON.parse(convoJSON), {
id: 'c',
uuid: UUID_C,
membersV2: [
{ uuid: UUID_A, joinedAtVersion: 1 },
{ uuid: UUID_B, joinedAtVersion: 2 },
],
pendingMembersV2: [
{ uuid: UUID_A, addedByUserId: UUID_B, timestamp: 4 },
{ uuid: UUID_B, addedByUserId: UUID_A, timestamp: 5 },
],
pendingAdminApprovalV2: [
{ uuid: UUID_A, timestamp: 6 },
{ uuid: UUID_B, timestamp: 7 },
],
});
const { json: messageMJSON } = db
.prepare('SELECT json FROM messages WHERE id = "m"')
.get();
assert.deepStrictEqual(JSON.parse(messageMJSON), {
id: 'm',
groupV2Change: {
from: UUID_A,
details: [
...CHANGE_TYPES.map(type => ({ type, uuid: UUID_B })),
...CHANGE_TYPES_WITH_INVITER.map(type => {
return {
type,
uuid: UUID_C,
inviter: UUID_A,
};
}),
],
},
sourceUuid: UUID_A,
invitedGV2Members: [
{
uuid: UUID_B,
addedByUserId: UUID_C,
},
],
});
const { json: messageNJSON } = db
.prepare('SELECT json FROM messages WHERE id = "n"')
.get();
assert.deepStrictEqual(JSON.parse(messageNJSON), {
id: 'n',
groupV2Change: {
details: [],
},
sourceUuid: UUID_A,
});
});
});
});

View File

@ -14,8 +14,8 @@ export function init(signalProtocolStore: SignalProtocolStore): void {
);
conversation.addKeyChange(uuid);
const groups = await window.ConversationController.getAllGroupsInvolvingId(
conversation.id
const groups = await window.ConversationController.getAllGroupsInvolvingUuid(
uuid
);
for (const group of groups) {
group.addKeyChange(uuid);

View File

@ -37,7 +37,7 @@ import type { CallbackResultType, CustomError } from './Types.d';
import { isValidNumber } from '../types/PhoneNumber';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { UUID, isValidUuid } from '../types/UUID';
import { Sessions, IdentityKeys } from '../LibSignalStores';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { getKeysForIdentifier } from './getKeysForIdentifier';
@ -656,7 +656,7 @@ export default class OutgoingMessage {
async sendToIdentifier(providedIdentifier: string): Promise<void> {
let identifier = providedIdentifier;
try {
if (window.isValidGuid(identifier)) {
if (isValidUuid(identifier)) {
// We're good!
} else if (isValidNumber(identifier)) {
if (!window.textsecure.messaging) {

View File

@ -1,14 +1,21 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { v4 as generateUUID } from 'uuid';
import { strictAssert } from '../util/assert';
import { isValidGuid } from '../util/isValidGuid';
export type UUIDStringType = `${string}-${string}-${string}-${string}-${string}`;
export const isValidUuid = (value: unknown): value is UUIDStringType =>
typeof value === 'string' &&
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
value
);
export class UUID {
constructor(protected readonly value: string) {
strictAssert(isValidGuid(value), `Invalid UUID: ${value}`);
strictAssert(isValidUuid(value), `Invalid UUID: ${value}`);
}
public toString(): UUIDStringType {
@ -41,4 +48,24 @@ export class UUID {
);
return uuid;
}
public static generate(): UUID {
return new UUID(generateUUID());
}
public static cast(value: UUIDStringType): never;
public static cast(value: string): UUIDStringType;
public static cast(value: string): UUIDStringType {
return new UUID(value.toLowerCase()).toString();
}
// For testing
public static fromPrefix(value: string): UUID {
let padded = value;
while (padded.length < 8) {
padded += '0';
}
return new UUID(`${padded}-0000-4000-8000-${'0'.repeat(12)}`);
}
}

View File

@ -31,3 +31,11 @@ export function strictAssert(
throw new Error(message);
}
}
/**
* Asserts that the type of value is not a promise.
* (Useful for database modules)
*/
export function assertSync<T, X>(value: T extends Promise<X> ? never : T): T {
return value;
}

View File

@ -3,6 +3,7 @@
import { compact } from 'lodash';
import type { ConversationAttributesType } from '../model-types.d';
import type { UUIDStringType } from '../types/UUID';
import { isDirectConversation } from './whatTypeOfConversation';
export function getConversationMembers(
@ -15,7 +16,7 @@ export function getConversationMembers(
if (conversationAttrs.membersV2) {
const { includePendingMembers } = options;
const members: Array<{ conversationId: string }> = includePendingMembers
const members: Array<{ uuid: UUIDStringType }> = includePendingMembers
? [
...(conversationAttrs.membersV2 || []),
...(conversationAttrs.pendingMembersV2 || []),
@ -24,9 +25,7 @@ export function getConversationMembers(
return compact(
members.map(member => {
const conversation = window.ConversationController.get(
member.conversationId
);
const conversation = window.ConversationController.get(member.uuid);
// In groups we won't sent to contacts we believe are unregistered
if (conversation && conversation.isUnregistered()) {

View File

@ -7,6 +7,7 @@ import type {
GroupV2RequestingMembership,
} from '../components/conversation/conversation-details/PendingInvites';
import type { ConversationType } from '../state/ducks/conversations';
import type { UUIDStringType } from '../types/UUID';
import { isConversationUnregistered } from './isConversationUnregistered';
export const getGroupMemberships = (
@ -20,7 +21,7 @@ export const getGroupMemberships = (
'memberships' | 'pendingApprovalMemberships' | 'pendingMemberships'
>
>,
getConversationById: (conversationId: string) => undefined | ConversationType
getConversationByUuid: (uuid: UUIDStringType) => undefined | ConversationType
): {
memberships: Array<GroupV2Membership>;
pendingApprovalMemberships: Array<GroupV2RequestingMembership>;
@ -28,7 +29,7 @@ export const getGroupMemberships = (
} => ({
memberships: memberships.reduce(
(result: Array<GroupV2Membership>, membership) => {
const member = getConversationById(membership.conversationId);
const member = getConversationByUuid(membership.uuid);
if (!member) {
return result;
}
@ -38,7 +39,7 @@ export const getGroupMemberships = (
),
pendingApprovalMemberships: pendingApprovalMemberships.reduce(
(result: Array<GroupV2RequestingMembership>, membership) => {
const member = getConversationById(membership.conversationId);
const member = getConversationByUuid(membership.uuid);
if (!member || isConversationUnregistered(member)) {
return result;
}
@ -48,7 +49,7 @@ export const getGroupMemberships = (
),
pendingMemberships: pendingMemberships.reduce(
(result: Array<GroupV2PendingMembership>, membership) => {
const member = getConversationById(membership.conversationId);
const member = getConversationByUuid(membership.uuid);
if (!member || isConversationUnregistered(member)) {
return result;
}

View File

@ -1,8 +0,0 @@
// Copyright 2017-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export const isValidGuid = (value: unknown): value is string =>
typeof value === 'string' &&
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
value
);

View File

@ -1,12 +1,12 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isValidUuid } from '../types/UUID';
import { assert } from './assert';
import { isValidGuid } from './isValidGuid';
export function normalizeUuid(uuid: string, context: string): string {
assert(
isValidGuid(uuid),
isValidUuid(uuid),
`Normalizing invalid uuid: ${uuid} in context "${context}"`
);

View File

@ -273,7 +273,7 @@ export async function sendToGroupViaSenderKey(options: {
conversation.set({
senderKeyInfo: {
createdAtDate: Date.now(),
distributionId: window.getGuid(),
distributionId: UUID.generate().toString(),
memberDevices: [],
},
});

View File

@ -22,6 +22,8 @@ import {
UuidCiphertext,
} from 'zkgroup';
import * as Bytes from '../Bytes';
import { UUID } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
export * from 'zkgroup';
@ -63,7 +65,7 @@ export function decryptGroupBlob(
export function decryptProfileKeyCredentialPresentation(
clientZkGroupCipher: ClientZkGroupCipher,
presentationBuffer: Uint8Array
): { profileKey: Uint8Array; uuid: string } {
): { profileKey: Uint8Array; uuid: UUIDStringType } {
const presentation = new ProfileKeyCredentialPresentation(
uint8ArrayToCompatArray(presentationBuffer)
);
@ -79,14 +81,14 @@ export function decryptProfileKeyCredentialPresentation(
return {
profileKey: compatArrayToUint8Array(profileKey.serialize()),
uuid,
uuid: UUID.cast(uuid),
};
}
export function decryptProfileKey(
clientZkGroupCipher: ClientZkGroupCipher,
profileKeyCiphertextBuffer: Uint8Array,
uuid: string
uuid: UUIDStringType
): Uint8Array {
const profileKeyCiphertext = new ProfileKeyCiphertext(
uint8ArrayToCompatArray(profileKeyCiphertextBuffer)
@ -113,7 +115,7 @@ export function decryptUuid(
export function deriveProfileKeyVersion(
profileKeyBase64: string,
uuid: string
uuid: UUIDStringType
): string {
const profileKeyArray = base64ToCompatArray(profileKeyBase64);
const profileKey = new ProfileKey(profileKeyArray);
@ -167,7 +169,7 @@ export function encryptGroupBlob(
export function encryptUuid(
clientZkGroupCipher: ClientZkGroupCipher,
uuidPlaintext: string
uuidPlaintext: UUIDStringType
): Uint8Array {
const uuidCiphertext = clientZkGroupCipher.encryptUuid(uuidPlaintext);
@ -176,7 +178,7 @@ export function encryptUuid(
export function generateProfileKeyCredentialRequest(
clientZkProfileCipher: ClientZkProfileOperations,
uuid: string,
uuid: UUIDStringType,
profileKeyBase64: string
): { context: ProfileKeyCredentialRequestContext; requestHex: string } {
const profileKeyArray = base64ToCompatArray(profileKeyBase64);
@ -287,7 +289,7 @@ export function handleProfileKeyCredential(
export function deriveProfileKeyCommitment(
profileKeyBase64: string,
uuid: string
uuid: UUIDStringType
): string {
const profileKeyArray = base64ToCompatArray(profileKeyBase64);
const profileKey = new ProfileKey(profileKeyArray);

View File

@ -1258,7 +1258,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
});
const invitedMemberIds = pendingMembersV2.map(
(item: GroupV2PendingMemberType) => item.conversationId
(item: GroupV2PendingMemberType) => item.uuid
);
this.migrationDialog = new Whisper.ReactWrapperView({

3
ts/window.d.ts vendored
View File

@ -102,7 +102,6 @@ import { SocketStatus } from './types/SocketStatus';
import SyncRequest from './textsecure/SyncRequest';
import { ConversationColorType, CustomColorType } from './types/Colors';
import { MessageController } from './util/MessageController';
import { isValidGuid } from './util/isValidGuid';
import { StateType } from './state/reducer';
import { SystemTraySetting } from './types/SystemTraySetting';
import { UUID } from './types/UUID';
@ -199,7 +198,6 @@ declare global {
getBuildCreation: () => number;
getEnvironment: typeof getEnvironment;
getExpiration: () => string;
getGuid: () => string;
getHostName: () => string;
getInboxCollection: () => ConversationModelCollectionType;
getInteractionMode: () => 'mouse' | 'keyboard';
@ -219,7 +217,6 @@ declare global {
isAfterVersion: (version: string, anotherVersion: string) => boolean;
isBeforeVersion: (version: string, anotherVersion: string) => boolean;
isFullScreen: () => boolean;
isValidGuid: typeof isValidGuid;
libphonenumber: {
util: {
getRegionCodeForNumber: (number: string) => string;