diff --git a/lib/shared/threads/community-announcement-root-spec.js b/lib/shared/threads/community-announcement-root-spec.js --- a/lib/shared/threads/community-announcement-root-spec.js +++ b/lib/shared/threads/community-announcement-root-spec.js @@ -2,11 +2,13 @@ import { keyserverThreadProtocol } from './protocols/keyserver-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; -const communityAnnouncementRootSpec: ThreadSpec = Object.freeze({ - traits: new Set(['community', 'announcement']), - protocol: keyserverThreadProtocol, - threadLabel: 'Community', -}); +const communityAnnouncementRootSpec: ThreadSpec = + Object.freeze({ + traits: new Set(['community', 'announcement']), + protocol: keyserverThreadProtocol, + threadLabel: 'Community', + }); export { communityAnnouncementRootSpec }; diff --git a/lib/shared/threads/community-open-announcement-subthread-spec.js b/lib/shared/threads/community-open-announcement-subthread-spec.js --- a/lib/shared/threads/community-open-announcement-subthread-spec.js +++ b/lib/shared/threads/community-open-announcement-subthread-spec.js @@ -2,11 +2,13 @@ import { keyserverThreadProtocol } from './protocols/keyserver-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; -const communityOpenAnnouncementSubthreadSpec: ThreadSpec = Object.freeze({ - traits: new Set(['communitySubthread', 'announcement']), - protocol: keyserverThreadProtocol, - threadLabel: 'Open', -}); +const communityOpenAnnouncementSubthreadSpec: ThreadSpec = + Object.freeze({ + traits: new Set(['communitySubthread', 'announcement']), + protocol: keyserverThreadProtocol, + threadLabel: 'Open', + }); export { communityOpenAnnouncementSubthreadSpec }; diff --git a/lib/shared/threads/community-open-subthread-spec.js b/lib/shared/threads/community-open-subthread-spec.js --- a/lib/shared/threads/community-open-subthread-spec.js +++ b/lib/shared/threads/community-open-subthread-spec.js @@ -2,11 +2,13 @@ import { keyserverThreadProtocol } from './protocols/keyserver-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; -const communityOpenSubthreadSpec: ThreadSpec = Object.freeze({ - traits: new Set(['communitySubthread']), - protocol: keyserverThreadProtocol, - threadLabel: 'Open', -}); +const communityOpenSubthreadSpec: ThreadSpec = + Object.freeze({ + traits: new Set(['communitySubthread']), + protocol: keyserverThreadProtocol, + threadLabel: 'Open', + }); export { communityOpenSubthreadSpec }; diff --git a/lib/shared/threads/community-root-spec.js b/lib/shared/threads/community-root-spec.js --- a/lib/shared/threads/community-root-spec.js +++ b/lib/shared/threads/community-root-spec.js @@ -2,8 +2,9 @@ import { keyserverThreadProtocol } from './protocols/keyserver-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; -const communityRootSpec: ThreadSpec = Object.freeze({ +const communityRootSpec: ThreadSpec = Object.freeze({ traits: new Set(['community']), protocol: keyserverThreadProtocol, threadLabel: 'Community', diff --git a/lib/shared/threads/community-secret-announcement-subthread-spec.js b/lib/shared/threads/community-secret-announcement-subthread-spec.js --- a/lib/shared/threads/community-secret-announcement-subthread-spec.js +++ b/lib/shared/threads/community-secret-announcement-subthread-spec.js @@ -2,11 +2,13 @@ import { keyserverThreadProtocol } from './protocols/keyserver-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; -const communitySecretAnnouncementSubthreadSpec: ThreadSpec = Object.freeze({ - traits: new Set(['communitySubthread', 'announcement']), - protocol: keyserverThreadProtocol, - threadLabel: 'Secret', -}); +const communitySecretAnnouncementSubthreadSpec: ThreadSpec = + Object.freeze({ + traits: new Set(['communitySubthread', 'announcement']), + protocol: keyserverThreadProtocol, + threadLabel: 'Secret', + }); export { communitySecretAnnouncementSubthreadSpec }; diff --git a/lib/shared/threads/community-secret-subthread-spec.js b/lib/shared/threads/community-secret-subthread-spec.js --- a/lib/shared/threads/community-secret-subthread-spec.js +++ b/lib/shared/threads/community-secret-subthread-spec.js @@ -2,11 +2,13 @@ import { keyserverThreadProtocol } from './protocols/keyserver-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; -const communitySecretSubthreadSpec: ThreadSpec = Object.freeze({ - traits: new Set(['communitySubthread']), - protocol: keyserverThreadProtocol, - threadLabel: 'Secret', -}); +const communitySecretSubthreadSpec: ThreadSpec = + Object.freeze({ + traits: new Set(['communitySubthread']), + protocol: keyserverThreadProtocol, + threadLabel: 'Secret', + }); export { communitySecretSubthreadSpec }; diff --git a/lib/shared/threads/genesis-personal-spec.js b/lib/shared/threads/genesis-personal-spec.js --- a/lib/shared/threads/genesis-personal-spec.js +++ b/lib/shared/threads/genesis-personal-spec.js @@ -2,11 +2,13 @@ import { keyserverThreadProtocol } from './protocols/keyserver-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; -const genesisPersonalSpec: ThreadSpec = Object.freeze({ - traits: new Set(['personal']), - protocol: keyserverThreadProtocol, - threadLabel: 'Personal', -}); +const genesisPersonalSpec: ThreadSpec = + Object.freeze({ + traits: new Set(['personal']), + protocol: keyserverThreadProtocol, + threadLabel: 'Personal', + }); export { genesisPersonalSpec }; diff --git a/lib/shared/threads/genesis-private-spec.js b/lib/shared/threads/genesis-private-spec.js --- a/lib/shared/threads/genesis-private-spec.js +++ b/lib/shared/threads/genesis-private-spec.js @@ -2,11 +2,14 @@ import { keyserverThreadProtocol } from './protocols/keyserver-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; -const genesisPrivateSpec: ThreadSpec = Object.freeze({ - traits: new Set(['private']), - protocol: keyserverThreadProtocol, - threadLabel: 'Private', -}); +const genesisPrivateSpec: ThreadSpec = Object.freeze( + { + traits: new Set(['private']), + protocol: keyserverThreadProtocol, + threadLabel: 'Private', + }, +); export { genesisPrivateSpec }; diff --git a/lib/shared/threads/genesis-spec.js b/lib/shared/threads/genesis-spec.js --- a/lib/shared/threads/genesis-spec.js +++ b/lib/shared/threads/genesis-spec.js @@ -2,8 +2,9 @@ import { keyserverThreadProtocol } from './protocols/keyserver-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; -const genesisSpec: ThreadSpec = Object.freeze({ +const genesisSpec: ThreadSpec = Object.freeze({ traits: new Set(['community', 'announcement']), protocol: keyserverThreadProtocol, threadLabel: 'Community', diff --git a/lib/shared/threads/local-spec.js b/lib/shared/threads/local-spec.js --- a/lib/shared/threads/local-spec.js +++ b/lib/shared/threads/local-spec.js @@ -2,8 +2,9 @@ import { dmThreadProtocol } from './protocols/dm-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MinimallyEncodedThickMemberInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; -const localSpec: ThreadSpec = Object.freeze({ +const localSpec: ThreadSpec = Object.freeze({ traits: new Set(), protocol: dmThreadProtocol, threadLabel: 'Local DM', diff --git a/lib/shared/threads/personal-spec.js b/lib/shared/threads/personal-spec.js --- a/lib/shared/threads/personal-spec.js +++ b/lib/shared/threads/personal-spec.js @@ -2,11 +2,14 @@ import { dmThreadProtocol } from './protocols/dm-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MinimallyEncodedThickMemberInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; -const personalSpec: ThreadSpec = Object.freeze({ - traits: new Set(['personal']), - protocol: dmThreadProtocol, - threadLabel: 'Local DM', -}); +const personalSpec: ThreadSpec = Object.freeze( + { + traits: new Set(['personal']), + protocol: dmThreadProtocol, + threadLabel: 'Local DM', + }, +); export { personalSpec }; diff --git a/lib/shared/threads/private-spec.js b/lib/shared/threads/private-spec.js --- a/lib/shared/threads/private-spec.js +++ b/lib/shared/threads/private-spec.js @@ -2,8 +2,9 @@ import { dmThreadProtocol } from './protocols/dm-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MinimallyEncodedThickMemberInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; -const privateSpec: ThreadSpec = Object.freeze({ +const privateSpec: ThreadSpec = Object.freeze({ traits: new Set(['private']), protocol: dmThreadProtocol, threadLabel: 'Local DM', diff --git a/lib/shared/threads/protocols/dm-thread-protocol.js b/lib/shared/threads/protocols/dm-thread-protocol.js --- a/lib/shared/threads/protocols/dm-thread-protocol.js +++ b/lib/shared/threads/protocols/dm-thread-protocol.js @@ -16,11 +16,27 @@ DMSendReactionMessageOperation, DMThreadSettingsChanges, } from '../../../types/dm-ops.js'; -import { thickThreadTypes } from '../../../types/thread-types-enum.js'; -import type { ChangeThreadSettingsPayload } from '../../../types/thread-types.js'; +import type { + RoleInfo, + ThreadCurrentUserInfo, + ThickRawThreadInfo, + MinimallyEncodedThickMemberInfo, +} from '../../../types/minimally-encoded-thread-permissions-types.js'; +import { + assertThickThreadType, + thickThreadTypes, +} from '../../../types/thread-types-enum.js'; +import type { + ChangeThreadSettingsPayload, + ClientDBThreadInfo, +} from '../../../types/thread-types.js'; +import { threadTimestampsValidator } from '../../../types/thread-types.js'; import { dateString as stringFromDate } from '../../../utils/date-utils.js'; import { SendMessageError } from '../../../utils/errors.js'; -import { pendingThickSidebarURLPrefix } from '../../../utils/validation-utils.js'; +import { + assertWithValidator, + pendingThickSidebarURLPrefix, +} from '../../../utils/validation-utils.js'; import { dmOperationSpecificationTypes, type OutboundDMOperationSpecification, @@ -54,533 +70,599 @@ ProtocolLeaveThreadInput, } from '../thread-spec.js'; -const dmThreadProtocol: ThreadProtocol = Object.freeze({ - sendTextMessage: async ( - message: ProtocolSendTextMessageInput, - utils: SendTextMessageUtils, - ) => { - const { messageInfo, threadInfo, parentThreadInfo } = message; - const { localID } = messageInfo; - invariant( - localID !== null && localID !== undefined, - 'localID should be set', - ); - - const messageID = getIDFromLocalID(localID); - const time = Date.now(); - - const recipients = - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && parentThreadInfo - ? parentThreadInfo.members - : threadInfo.members; - const recipientsIDs = recipients.map(recipient => recipient.id); - - const result = await utils.sendComposableDMOperation({ - type: dmOperationSpecificationTypes.OUTBOUND, - op: { - type: 'send_text_message', - threadID: threadInfo.id, - creatorID: messageInfo.creatorID, - time, - messageID, - text: messageInfo.text, - }, - // We need to use a different mechanism than `all_thread_members` - // because when creating a thread, the thread might not yet - // be in the store. - recipients: { - type: 'some_users', - userIDs: recipientsIDs, - }, - sendOnly: true, - composableMessageID: localID, - }); - - if (result.result === 'failure' && result.failedMessageIDs.length > 0) { - const error = new SendMessageError( - 'Failed to send message to all peers', - localID, - messageInfo.threadID, +const dmThreadProtocol: ThreadProtocol = + Object.freeze({ + sendTextMessage: async ( + message: ProtocolSendTextMessageInput, + utils: SendTextMessageUtils, + ) => { + const { messageInfo, threadInfo, parentThreadInfo } = message; + const { localID } = messageInfo; + invariant( + localID !== null && localID !== undefined, + 'localID should be set', ); - error.failedOutboundP2PMessageIDs = result.failedMessageIDs; - throw error; - } - return { - localID, - serverID: messageID, - threadID: messageInfo.threadID, - time, - }; - }, - - sendMultimediaMessage: async ( - message: ProtocolSendMultimediaMessageInput, - utils: SendMultimediaMessageUtils, - ) => { - const { messageInfo, threadInfo } = message; - const { localID } = messageInfo; - invariant( - localID !== null && localID !== undefined, - 'localID should be set', - ); - - const messageID = getIDFromLocalID(localID); - const time = Date.now(); - - const result = await utils.sendComposableDMOperation({ - type: dmOperationSpecificationTypes.OUTBOUND, - op: { - type: 'send_multimedia_message', - threadID: threadInfo.id, - creatorID: messageInfo.creatorID, - time: Date.now(), - messageID, - media: messageInfo.media, - }, - recipients: { - type: 'all_thread_members', - threadID: - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && - threadInfo.parentThreadID - ? threadInfo.parentThreadID - : threadInfo.id, - }, - sendOnly: true, - composableMessageID: localID, - }); - - if (result.result === 'failure' && result.failedMessageIDs.length > 0) { - const error = new SendMessageError( - 'Failed to send message to all peers', - localID, - messageInfo.threadID, - ); - error.failedOutboundP2PMessageIDs = result.failedMessageIDs; - throw error; - } - return { - result: { + + const messageID = getIDFromLocalID(localID); + const time = Date.now(); + + const recipients = + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && parentThreadInfo + ? parentThreadInfo.members + : threadInfo.members; + const recipientsIDs = recipients.map(recipient => recipient.id); + + const result = await utils.sendComposableDMOperation({ + type: dmOperationSpecificationTypes.OUTBOUND, + op: { + type: 'send_text_message', + threadID: threadInfo.id, + creatorID: messageInfo.creatorID, + time, + messageID, + text: messageInfo.text, + }, + // We need to use a different mechanism than `all_thread_members` + // because when creating a thread, the thread might not yet + // be in the store. + recipients: { + type: 'some_users', + userIDs: recipientsIDs, + }, + sendOnly: true, + composableMessageID: localID, + }); + + if (result.result === 'failure' && result.failedMessageIDs.length > 0) { + const error = new SendMessageError( + 'Failed to send message to all peers', + localID, + messageInfo.threadID, + ); + error.failedOutboundP2PMessageIDs = result.failedMessageIDs; + throw error; + } + return { localID, serverID: messageID, threadID: messageInfo.threadID, time, - }, - }; - }, - - editTextMessage: async ( - message: ProtocolEditTextMessageInput, - utils: EditTextMessageUtils, - ) => { - const { viewerID, threadInfo, messageID, newText } = message; - invariant(viewerID, 'viewerID should be set'); - const op: DMSendEditMessageOperation = { - type: 'send_edit_message', - threadID: threadInfo.id, - creatorID: viewerID, - time: Date.now(), - messageID: uuid.v4(), - targetMessageID: messageID, - text: newText, - }; - const opSpecification: OutboundDMOperationSpecification = { - type: dmOperationSpecificationTypes.OUTBOUND, - op, - recipients: { - type: 'all_thread_members', - threadID: - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && - threadInfo.parentThreadID - ? threadInfo.parentThreadID - : threadInfo.id, - }, - }; - await utils.processAndSendDMOperation(opSpecification); - }, - - changeThreadSettings: async ( - protocolInput: ProtocolChangeThreadSettingsInput, - utils: ChangeThreadSettingsUtils, - ) => { - const { viewerID, input } = protocolInput; - invariant(viewerID, 'viewerID should be set'); - invariant( - !input.changes.newMemberIDs, - "DM protocol doesn't support" + - ' adding new members when changing thread settings', - ); - - const changes: { ...DMThreadSettingsChanges } = {}; - if (input.changes.name) { - changes.name = input.changes.name; - } - if (input.changes.description) { - changes.description = input.changes.description; - } - if (input.changes.color) { - changes.color = input.changes.color; - } - if (input.changes.avatar && input.changes.avatar.type === 'emoji') { - changes.avatar = { - type: 'emoji', - emoji: input.changes.avatar.emoji, - color: input.changes.avatar.color, }; - } else if (input.changes.avatar && input.changes.avatar.type === 'ens') { - changes.avatar = { type: 'ens' }; - } else if ( - input.changes.avatar && - input.changes.avatar.type === 'non_keyserver_image' - ) { - changes.avatar = { - type: 'encrypted_image', - blobURI: input.changes.avatar.blobURI, - thumbHash: input.changes.avatar.thumbHash, - encryptionKey: input.changes.avatar.encryptionKey, + }, + + sendMultimediaMessage: async ( + message: ProtocolSendMultimediaMessageInput, + utils: SendMultimediaMessageUtils, + ) => { + const { messageInfo, threadInfo } = message; + const { localID } = messageInfo; + invariant( + localID !== null && localID !== undefined, + 'localID should be set', + ); + + const messageID = getIDFromLocalID(localID); + const time = Date.now(); + + const result = await utils.sendComposableDMOperation({ + type: dmOperationSpecificationTypes.OUTBOUND, + op: { + type: 'send_multimedia_message', + threadID: threadInfo.id, + creatorID: messageInfo.creatorID, + time: Date.now(), + messageID, + media: messageInfo.media, + }, + recipients: { + type: 'all_thread_members', + threadID: + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && + threadInfo.parentThreadID + ? threadInfo.parentThreadID + : threadInfo.id, + }, + sendOnly: true, + composableMessageID: localID, + }); + + if (result.result === 'failure' && result.failedMessageIDs.length > 0) { + const error = new SendMessageError( + 'Failed to send message to all peers', + localID, + messageInfo.threadID, + ); + error.failedOutboundP2PMessageIDs = result.failedMessageIDs; + throw error; + } + return { + result: { + localID, + serverID: messageID, + threadID: messageInfo.threadID, + time, + }, + }; + }, + + editTextMessage: async ( + message: ProtocolEditTextMessageInput, + utils: EditTextMessageUtils, + ) => { + const { viewerID, threadInfo, messageID, newText } = message; + invariant(viewerID, 'viewerID should be set'); + const op: DMSendEditMessageOperation = { + type: 'send_edit_message', + threadID: threadInfo.id, + creatorID: viewerID, + time: Date.now(), + messageID: uuid.v4(), + targetMessageID: messageID, + text: newText, + }; + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { + type: 'all_thread_members', + threadID: + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && + threadInfo.parentThreadID + ? threadInfo.parentThreadID + : threadInfo.id, + }, + }; + await utils.processAndSendDMOperation(opSpecification); + }, + + changeThreadSettings: async ( + protocolInput: ProtocolChangeThreadSettingsInput, + utils: ChangeThreadSettingsUtils, + ) => { + const { viewerID, input } = protocolInput; + invariant(viewerID, 'viewerID should be set'); + invariant( + !input.changes.newMemberIDs, + "DM protocol doesn't support" + + ' adding new members when changing thread settings', + ); + + const changes: { ...DMThreadSettingsChanges } = {}; + if (input.changes.name) { + changes.name = input.changes.name; + } + if (input.changes.description) { + changes.description = input.changes.description; + } + if (input.changes.color) { + changes.color = input.changes.color; + } + if (input.changes.avatar && input.changes.avatar.type === 'emoji') { + changes.avatar = { + type: 'emoji', + emoji: input.changes.avatar.emoji, + color: input.changes.avatar.color, + }; + } else if (input.changes.avatar && input.changes.avatar.type === 'ens') { + changes.avatar = { type: 'ens' }; + } else if ( + input.changes.avatar && + input.changes.avatar.type === 'non_keyserver_image' + ) { + changes.avatar = { + type: 'encrypted_image', + blobURI: input.changes.avatar.blobURI, + thumbHash: input.changes.avatar.thumbHash, + encryptionKey: input.changes.avatar.encryptionKey, + }; + } else if ( + input.changes.avatar && + input.changes.avatar.type === 'remove' + ) { + changes.avatar = null; + } + + const { threadInfo } = input; + const op: DMChangeThreadSettingsOperation = { + type: 'change_thread_settings', + threadID: threadInfo.id, + editorID: viewerID, + time: Date.now(), + changes, + messageIDsPrefix: uuid.v4(), + }; + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { + type: 'all_thread_members', + threadID: + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && + threadInfo.parentThreadID + ? threadInfo.parentThreadID + : threadInfo.id, + }, }; - } else if (input.changes.avatar && input.changes.avatar.type === 'remove') { - changes.avatar = null; - } - - const { threadInfo } = input; - const op: DMChangeThreadSettingsOperation = { - type: 'change_thread_settings', - threadID: threadInfo.id, - editorID: viewerID, - time: Date.now(), - changes, - messageIDsPrefix: uuid.v4(), - }; - const opSpecification: OutboundDMOperationSpecification = { - type: dmOperationSpecificationTypes.OUTBOUND, - op, - recipients: { - type: 'all_thread_members', - threadID: - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && - threadInfo.parentThreadID - ? threadInfo.parentThreadID - : threadInfo.id, - }, - }; - - await utils.processAndSendDMOperation(opSpecification); - return ({ - threadID: threadInfo.id, - updatesResult: { newUpdates: [] }, - newMessageInfos: [], - }: ChangeThreadSettingsPayload); - }, - - supportsCalendarHistory: false, - - calendarIsOnline: (tunnelbrokerSocketState: TunnelbrokerSocketState) => - !!tunnelbrokerSocketState.connected, - - createCalendarEntry: async ( - protocolInput: ProtocolCreateEntryInput, - utils: CreateEntryUtils, - ) => { - const { viewerID, input } = protocolInput; - - invariant(viewerID, 'viewerID must be set'); - const entryID = uuid.v4(); - - const { createEntryInfo, threadInfo } = input; - const op: DMCreateEntryOperation = { - type: 'create_entry', - threadID: threadInfo.id, - creatorID: viewerID, - time: createEntryInfo.timestamp, - entryID: uuid.v4(), - entryDate: createEntryInfo.date, - text: createEntryInfo.text, - messageID: uuid.v4(), - }; - const opSpecification: OutboundDMOperationSpecification = { - type: dmOperationSpecificationTypes.OUTBOUND, - op, - recipients: { - type: 'all_thread_members', - threadID: - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && - threadInfo.parentThreadID - ? threadInfo.parentThreadID - : threadInfo.id, - }, - }; - - await utils.processAndSendDMOperation(opSpecification); - - return { - entryID, - newMessageInfos: [], - threadID: createEntryInfo.threadID, - localID: createEntryInfo.localID, - updatesResult: { - viewerUpdates: [], - userInfos: [], - }, - }; - }, - - deleteCalendarEntry: async ( - protocolInput: ProtocolDeleteEntryInput, - utils: DeleteEntryUtils, - ) => { - const { viewerID, input, originalEntry: prevEntry } = protocolInput; - const { deleteEntryInfo, threadInfo } = input; - - invariant(viewerID, 'viewerID must be set'); - - const op: DMDeleteEntryOperation = { - type: 'delete_entry', - threadID: threadInfo.id, - creatorID: viewerID, - creationTime: prevEntry.creationTime, - time: Date.now(), - entryID: deleteEntryInfo.entryID, - entryDate: stringFromDate(prevEntry.year, prevEntry.month, prevEntry.day), - prevText: deleteEntryInfo.prevText, - messageID: uuid.v4(), - }; - - const opSpecification: OutboundDMOperationSpecification = { - type: dmOperationSpecificationTypes.OUTBOUND, - op, - recipients: { - type: 'all_thread_members', - threadID: - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && - threadInfo.parentThreadID - ? threadInfo.parentThreadID - : threadInfo.id, - }, - }; - - await utils.processAndSendDMOperation(opSpecification); - - return { - threadID: threadInfo.id, - newMessageInfos: [], - updatesResult: { - viewerUpdates: [], - userInfos: [], - }, - }; - }, - - editCalendarEntry: async ( - protocolInput: ProtocolEditEntryInput, - utils: EditEntryUtils, - ) => { - const { viewerID, input, originalEntry: prevEntry } = protocolInput; - const { saveEntryInfo, threadInfo } = input; - - invariant(viewerID, 'viewerID must be set'); - - const op: DMEditEntryOperation = { - type: 'edit_entry', - threadID: threadInfo.id, - creatorID: viewerID, - creationTime: prevEntry.creationTime, - time: saveEntryInfo.timestamp, - entryID: saveEntryInfo.entryID, - entryDate: stringFromDate(prevEntry.year, prevEntry.month, prevEntry.day), - text: saveEntryInfo.text, - messageID: uuid.v4(), - }; - const opSpecification: OutboundDMOperationSpecification = { - type: dmOperationSpecificationTypes.OUTBOUND, - op, - recipients: { - type: 'all_thread_members', - threadID: - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && - threadInfo.parentThreadID - ? threadInfo.parentThreadID - : threadInfo.id, - }, - }; - - await utils.processAndSendDMOperation(opSpecification); - - return { - entryID: saveEntryInfo.entryID, - newMessageInfos: [], - updatesResult: { - viewerUpdates: [], - userInfos: [], - }, - }; - }, - - setThreadUnreadStatus: async ( - input: ProtocolSetThreadUnreadStatusInput, - utils: SetThreadUnreadStatusUtils, - ) => { - const { - viewerID, - input: { threadInfo }, - } = input; - - invariant(viewerID, 'viewerID must be set'); - const op: DMChangeThreadReadStatusOperation = { - type: 'change_thread_read_status', - time: Date.now(), - threadID: threadInfo.id, - creatorID: viewerID, - unread: !threadInfo.currentUser.unread, - }; - - const opSpecification: OutboundDMOperationSpecification = { - type: dmOperationSpecificationTypes.OUTBOUND, - op, - recipients: { - type: 'self_devices', - }, - }; - - await utils.processAndSendDMOperation(opSpecification); - return { - resetToUnread: false, - threadID: threadInfo.id, - }; - }, - - sendReaction: async ( - input: ProtocolSendReactionInput, - utils: SendReactionUtils, - ) => { - const { threadInfo, viewerID, messageID, reaction, action } = input; - const threadID = threadInfo.id; - - const op: DMSendReactionMessageOperation = { - type: 'send_reaction_message', - threadID, - creatorID: viewerID, - time: Date.now(), - messageID: uuid.v4(), - targetMessageID: messageID, - reaction, - action, - }; - const opSpecification: OutboundDMOperationSpecification = { - type: dmOperationSpecificationTypes.OUTBOUND, - op, - recipients: { - type: 'all_thread_members', - threadID: - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && - threadInfo.parentThreadID - ? threadInfo.parentThreadID - : threadInfo.id, - }, - }; - await utils.processAndSendDMOperation(opSpecification); - }, - - addThreadMembers: ( - input: ProtocolAddThreadMembersInput, - utils: AddThreadMembersUtils, - ) => utils.dmAddThreadMembers(input.newMemberIDs, input.threadInfo), - - updateSubscription: async ( - protocolInput: ProtocolUpdateSubscriptionInput, - utils: UpdateSubscriptionUtils, - ) => { - const { viewerID, input } = protocolInput; - invariant(viewerID, 'viewerID must be set'); - - const { threadInfo, updatedFields } = input; - const subscription = { - ...threadInfo.currentUser.subscription, - ...updatedFields, - }; - - const op: DMChangeThreadSubscriptionOperation = { - type: 'change_thread_subscription', - time: Date.now(), - threadID: threadInfo.id, - creatorID: viewerID, - subscription, - }; - - const opSpecification: OutboundDMOperationSpecification = { - type: dmOperationSpecificationTypes.OUTBOUND, - op, - recipients: { - type: 'all_thread_members', - threadID: - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && - threadInfo.parentThreadID - ? threadInfo.parentThreadID - : threadInfo.id, - }, - }; - - await utils.processAndSendDMOperation(opSpecification); - return { threadID: threadInfo.id, subscription }; - }, - - leaveThread: async ( - input: ProtocolLeaveThreadInput, - utils: LeaveThreadUtils, - ) => { - const { threadInfo, viewerID } = input; - invariant(viewerID, 'viewerID should be set'); - const op: DMLeaveThreadOperation = { - type: 'leave_thread', - editorID: viewerID, - time: Date.now(), - messageID: uuid.v4(), - threadID: threadInfo.id, - }; - const opSpecification: OutboundDMOperationSpecification = { - type: dmOperationSpecificationTypes.OUTBOUND, - op, - recipients: { - type: 'all_thread_members', - threadID: - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && - threadInfo.parentThreadID - ? threadInfo.parentThreadID - : threadInfo.id, - }, - }; - await utils.processAndSendDMOperation(opSpecification); - return { - invalidatedThreads: [], - }; - }, - - allowsDeletingSidebarSource: false, - - presentationDetails: { - membershipChangesShownInThreadPreview: true, - usersWithoutDeviceListExcludedFromSearchResult: true, - supportsMediaGallery: false, - nativeChatThreadListIcon: 'lock', - webChatThreadListIcon: iconCreationFunction => iconCreationFunction('lock'), - threadAncestorLabel: () => 'Local DM', - }, - - uploadMultimediaMetadataToKeyserver: false, - - canActionsTargetPendingMessages: true, - - sidebarConfig: { - sidebarThreadType: thickThreadTypes.THICK_SIDEBAR, - pendingSidebarURLPrefix: pendingThickSidebarURLPrefix, - }, - - shouldPerformSideEffectsBeforeSendingMessage: false, - - messagesStoredOnServer: false, - - arePendingThreadsDescendantsOfGenesis: false, - - threadActivityUpdatedByDMActivityHandler: true, -}); + + await utils.processAndSendDMOperation(opSpecification); + return ({ + threadID: threadInfo.id, + updatesResult: { newUpdates: [] }, + newMessageInfos: [], + }: ChangeThreadSettingsPayload); + }, + + supportsCalendarHistory: false, + + calendarIsOnline: (tunnelbrokerSocketState: TunnelbrokerSocketState) => + !!tunnelbrokerSocketState.connected, + + createCalendarEntry: async ( + protocolInput: ProtocolCreateEntryInput, + utils: CreateEntryUtils, + ) => { + const { viewerID, input } = protocolInput; + + invariant(viewerID, 'viewerID must be set'); + const entryID = uuid.v4(); + + const { createEntryInfo, threadInfo } = input; + const op: DMCreateEntryOperation = { + type: 'create_entry', + threadID: threadInfo.id, + creatorID: viewerID, + time: createEntryInfo.timestamp, + entryID: uuid.v4(), + entryDate: createEntryInfo.date, + text: createEntryInfo.text, + messageID: uuid.v4(), + }; + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { + type: 'all_thread_members', + threadID: + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && + threadInfo.parentThreadID + ? threadInfo.parentThreadID + : threadInfo.id, + }, + }; + + await utils.processAndSendDMOperation(opSpecification); + + return { + entryID, + newMessageInfos: [], + threadID: createEntryInfo.threadID, + localID: createEntryInfo.localID, + updatesResult: { + viewerUpdates: [], + userInfos: [], + }, + }; + }, + + deleteCalendarEntry: async ( + protocolInput: ProtocolDeleteEntryInput, + utils: DeleteEntryUtils, + ) => { + const { viewerID, input, originalEntry: prevEntry } = protocolInput; + const { deleteEntryInfo, threadInfo } = input; + + invariant(viewerID, 'viewerID must be set'); + + const op: DMDeleteEntryOperation = { + type: 'delete_entry', + threadID: threadInfo.id, + creatorID: viewerID, + creationTime: prevEntry.creationTime, + time: Date.now(), + entryID: deleteEntryInfo.entryID, + entryDate: stringFromDate( + prevEntry.year, + prevEntry.month, + prevEntry.day, + ), + prevText: deleteEntryInfo.prevText, + messageID: uuid.v4(), + }; + + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { + type: 'all_thread_members', + threadID: + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && + threadInfo.parentThreadID + ? threadInfo.parentThreadID + : threadInfo.id, + }, + }; + + await utils.processAndSendDMOperation(opSpecification); + + return { + threadID: threadInfo.id, + newMessageInfos: [], + updatesResult: { + viewerUpdates: [], + userInfos: [], + }, + }; + }, + + editCalendarEntry: async ( + protocolInput: ProtocolEditEntryInput, + utils: EditEntryUtils, + ) => { + const { viewerID, input, originalEntry: prevEntry } = protocolInput; + const { saveEntryInfo, threadInfo } = input; + + invariant(viewerID, 'viewerID must be set'); + + const op: DMEditEntryOperation = { + type: 'edit_entry', + threadID: threadInfo.id, + creatorID: viewerID, + creationTime: prevEntry.creationTime, + time: saveEntryInfo.timestamp, + entryID: saveEntryInfo.entryID, + entryDate: stringFromDate( + prevEntry.year, + prevEntry.month, + prevEntry.day, + ), + text: saveEntryInfo.text, + messageID: uuid.v4(), + }; + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { + type: 'all_thread_members', + threadID: + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && + threadInfo.parentThreadID + ? threadInfo.parentThreadID + : threadInfo.id, + }, + }; + + await utils.processAndSendDMOperation(opSpecification); + + return { + entryID: saveEntryInfo.entryID, + newMessageInfos: [], + updatesResult: { + viewerUpdates: [], + userInfos: [], + }, + }; + }, + + setThreadUnreadStatus: async ( + input: ProtocolSetThreadUnreadStatusInput, + utils: SetThreadUnreadStatusUtils, + ) => { + const { + viewerID, + input: { threadInfo }, + } = input; + + invariant(viewerID, 'viewerID must be set'); + const op: DMChangeThreadReadStatusOperation = { + type: 'change_thread_read_status', + time: Date.now(), + threadID: threadInfo.id, + creatorID: viewerID, + unread: !threadInfo.currentUser.unread, + }; + + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { + type: 'self_devices', + }, + }; + + await utils.processAndSendDMOperation(opSpecification); + return { + resetToUnread: false, + threadID: threadInfo.id, + }; + }, + + sendReaction: async ( + input: ProtocolSendReactionInput, + utils: SendReactionUtils, + ) => { + const { threadInfo, viewerID, messageID, reaction, action } = input; + const threadID = threadInfo.id; + + const op: DMSendReactionMessageOperation = { + type: 'send_reaction_message', + threadID, + creatorID: viewerID, + time: Date.now(), + messageID: uuid.v4(), + targetMessageID: messageID, + reaction, + action, + }; + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { + type: 'all_thread_members', + threadID: + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && + threadInfo.parentThreadID + ? threadInfo.parentThreadID + : threadInfo.id, + }, + }; + await utils.processAndSendDMOperation(opSpecification); + }, + + addThreadMembers: ( + input: ProtocolAddThreadMembersInput, + utils: AddThreadMembersUtils, + ) => utils.dmAddThreadMembers(input.newMemberIDs, input.threadInfo), + + updateSubscription: async ( + protocolInput: ProtocolUpdateSubscriptionInput, + utils: UpdateSubscriptionUtils, + ) => { + const { viewerID, input } = protocolInput; + invariant(viewerID, 'viewerID must be set'); + + const { threadInfo, updatedFields } = input; + const subscription = { + ...threadInfo.currentUser.subscription, + ...updatedFields, + }; + + const op: DMChangeThreadSubscriptionOperation = { + type: 'change_thread_subscription', + time: Date.now(), + threadID: threadInfo.id, + creatorID: viewerID, + subscription, + }; + + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { + type: 'all_thread_members', + threadID: + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && + threadInfo.parentThreadID + ? threadInfo.parentThreadID + : threadInfo.id, + }, + }; + + await utils.processAndSendDMOperation(opSpecification); + return { threadID: threadInfo.id, subscription }; + }, + + leaveThread: async ( + input: ProtocolLeaveThreadInput, + utils: LeaveThreadUtils, + ) => { + const { threadInfo, viewerID } = input; + invariant(viewerID, 'viewerID should be set'); + const op: DMLeaveThreadOperation = { + type: 'leave_thread', + editorID: viewerID, + time: Date.now(), + messageID: uuid.v4(), + threadID: threadInfo.id, + }; + const opSpecification: OutboundDMOperationSpecification = { + type: dmOperationSpecificationTypes.OUTBOUND, + op, + recipients: { + type: 'all_thread_members', + threadID: + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && + threadInfo.parentThreadID + ? threadInfo.parentThreadID + : threadInfo.id, + }, + }; + await utils.processAndSendDMOperation(opSpecification); + return { + invalidatedThreads: [], + }; + }, + + convertClientDBThreadInfo: ( + clientDBThreadInfo: ClientDBThreadInfo, + members: $ReadOnlyArray, + roles: { +[id: string]: RoleInfo }, + currentUser: ThreadCurrentUserInfo, + ) => { + const thickThreadType = assertThickThreadType(clientDBThreadInfo.type); + + invariant( + clientDBThreadInfo.timestamps, + 'Thick thread info must contain the timestamps', + ); + const threadTimestamps = assertWithValidator( + JSON.parse(clientDBThreadInfo.timestamps), + threadTimestampsValidator, + ); + + let rawThreadInfo: ThickRawThreadInfo = { + minimallyEncoded: true, + thick: true, + id: clientDBThreadInfo.id, + type: thickThreadType, + name: clientDBThreadInfo.name, + description: clientDBThreadInfo.description, + color: clientDBThreadInfo.color, + creationTime: Number(clientDBThreadInfo.creationTime), + parentThreadID: clientDBThreadInfo.parentThreadID, + containingThreadID: clientDBThreadInfo.containingThreadID, + members, + roles, + currentUser, + repliesCount: clientDBThreadInfo.repliesCount, + pinnedCount: clientDBThreadInfo.pinnedCount, + timestamps: threadTimestamps, + }; + + if (clientDBThreadInfo.sourceMessageID) { + rawThreadInfo = { + ...rawThreadInfo, + sourceMessageID: clientDBThreadInfo.sourceMessageID, + }; + } + + if (clientDBThreadInfo.avatar) { + rawThreadInfo = { + ...rawThreadInfo, + avatar: JSON.parse(clientDBThreadInfo.avatar), + }; + } + + return rawThreadInfo; + }, + + allowsDeletingSidebarSource: false, + + presentationDetails: { + membershipChangesShownInThreadPreview: true, + usersWithoutDeviceListExcludedFromSearchResult: true, + supportsMediaGallery: false, + nativeChatThreadListIcon: 'lock', + webChatThreadListIcon: iconCreationFunction => + iconCreationFunction('lock'), + threadAncestorLabel: () => 'Local DM', + }, + + uploadMultimediaMetadataToKeyserver: false, + + canActionsTargetPendingMessages: true, + + sidebarConfig: { + sidebarThreadType: thickThreadTypes.THICK_SIDEBAR, + pendingSidebarURLPrefix: pendingThickSidebarURLPrefix, + }, + + shouldPerformSideEffectsBeforeSendingMessage: false, + + messagesStoredOnServer: false, + + arePendingThreadsDescendantsOfGenesis: false, + + threadActivityUpdatedByDMActivityHandler: true, + }); export { dmThreadProtocol }; diff --git a/lib/shared/threads/protocols/keyserver-thread-protocol.js b/lib/shared/threads/protocols/keyserver-thread-protocol.js --- a/lib/shared/threads/protocols/keyserver-thread-protocol.js +++ b/lib/shared/threads/protocols/keyserver-thread-protocol.js @@ -36,8 +36,18 @@ } from '../../../types/message-types.js'; import { getMediaMessageServerDBContentsFromMedia } from '../../../types/messages/media.js'; import type { RawReactionMessageInfo } from '../../../types/messages/reaction.js'; +import type { + RoleInfo, + ThreadCurrentUserInfo, + ThinRawThreadInfo, + MemberInfoSansPermissions, +} from '../../../types/minimally-encoded-thread-permissions-types.js'; import type { Dispatch } from '../../../types/redux-types.js'; -import { thinThreadTypes } from '../../../types/thread-types-enum.js'; +import { + assertThinThreadType, + thinThreadTypes, +} from '../../../types/thread-types-enum.js'; +import type { ClientDBThreadInfo } from '../../../types/thread-types.js'; import { blobHashFromBlobServiceURI, isBlobServiceURI, @@ -77,50 +87,79 @@ LeaveThreadUtils, } from '../thread-spec.js'; -const keyserverThreadProtocol: ThreadProtocol = Object.freeze({ - sendTextMessage: async ( - message: ProtocolSendTextMessageInput, - utils: SendTextMessageUtils, - ) => { - const { messageInfo, sidebarCreation } = message; - const { localID } = messageInfo; - invariant( - localID !== null && localID !== undefined, - 'localID should be set', - ); - const result = await utils.sendKeyserverTextMessage({ - threadID: messageInfo.threadID, - localID, - text: messageInfo.text, - sidebarCreation, - }); - return { - localID, - serverID: result.id, - threadID: messageInfo.threadID, - time: result.time, - }; - }, +const keyserverThreadProtocol: ThreadProtocol = + Object.freeze({ + sendTextMessage: async ( + message: ProtocolSendTextMessageInput, + utils: SendTextMessageUtils, + ) => { + const { messageInfo, sidebarCreation } = message; + const { localID } = messageInfo; + invariant( + localID !== null && localID !== undefined, + 'localID should be set', + ); + const result = await utils.sendKeyserverTextMessage({ + threadID: messageInfo.threadID, + localID, + text: messageInfo.text, + sidebarCreation, + }); + return { + localID, + serverID: result.id, + threadID: messageInfo.threadID, + time: result.time, + }; + }, - sendMultimediaMessage: async ( - message: ProtocolSendMultimediaMessageInput, - utils: SendMultimediaMessageUtils, - ) => { - const { messageInfo, isLegacy, sidebarCreation } = message; - const { localID } = messageInfo; - invariant( - localID !== null && localID !== undefined, - 'localID should be set', - ); + sendMultimediaMessage: async ( + message: ProtocolSendMultimediaMessageInput, + utils: SendMultimediaMessageUtils, + ) => { + const { messageInfo, isLegacy, sidebarCreation } = message; + const { localID } = messageInfo; + invariant( + localID !== null && localID !== undefined, + 'localID should be set', + ); + + const { + reassignThickThreadMedia, + dispatch, + processHolders, + legacyKeyserverSendMultimediaMessage, + sendKeyserverMultimediaMessage, + } = utils; + if (isLegacy) { + const { messageMedia, mediaIDUpdates } = + await migrateMessageMediaToKeyserver( + messageInfo, + reassignThickThreadMedia, + dispatch, + processHolders, + ); + const mediaIDs = []; + for (const { id } of messageMedia) { + mediaIDs.push(id); + } + const result = await legacyKeyserverSendMultimediaMessage({ + threadID: messageInfo.threadID, + localID, + mediaIDs, + sidebarCreation, + }); + return { + result: { + localID, + serverID: result.id, + threadID: messageInfo.threadID, + time: result.time, + }, + mediaIDUpdates, + }; + } - const { - reassignThickThreadMedia, - dispatch, - processHolders, - legacyKeyserverSendMultimediaMessage, - sendKeyserverMultimediaMessage, - } = utils; - if (isLegacy) { const { messageMedia, mediaIDUpdates } = await migrateMessageMediaToKeyserver( messageInfo, @@ -128,14 +167,12 @@ dispatch, processHolders, ); - const mediaIDs = []; - for (const { id } of messageMedia) { - mediaIDs.push(id); - } - const result = await legacyKeyserverSendMultimediaMessage({ + const mediaMessageContents = + getMediaMessageServerDBContentsFromMedia(messageMedia); + const result = await sendKeyserverMultimediaMessage({ threadID: messageInfo.threadID, localID, - mediaIDs, + mediaMessageContents, sidebarCreation, }); return { @@ -147,238 +184,255 @@ }, mediaIDUpdates, }; - } + }, - const { messageMedia, mediaIDUpdates } = - await migrateMessageMediaToKeyserver( - messageInfo, - reassignThickThreadMedia, - dispatch, - processHolders, + editTextMessage: async ( + message: ProtocolEditTextMessageInput, + utils: EditTextMessageUtils, + ) => { + const { messageID, newText } = message; + const editMessagePromise = (async () => { + const result = await utils.keyserverEditMessage({ + targetMessageID: messageID, + text: newText, + }); + + return ({ + newMessageInfos: result.newMessageInfos, + }: { +newMessageInfos: $ReadOnlyArray }); + })(); + + void utils.dispatchActionPromise( + sendEditMessageActionTypes, + editMessagePromise, ); - const mediaMessageContents = - getMediaMessageServerDBContentsFromMedia(messageMedia); - const result = await sendKeyserverMultimediaMessage({ - threadID: messageInfo.threadID, - localID, - mediaMessageContents, - sidebarCreation, - }); - return { - result: { - localID, - serverID: result.id, - threadID: messageInfo.threadID, - time: result.time, - }, - mediaIDUpdates, - }; - }, - editTextMessage: async ( - message: ProtocolEditTextMessageInput, - utils: EditTextMessageUtils, - ) => { - const { messageID, newText } = message; - const editMessagePromise = (async () => { - const result = await utils.keyserverEditMessage({ + await editMessagePromise; + }, + + changeThreadSettings: async ( + protocolInput: ProtocolChangeThreadSettingsInput, + utils: ChangeThreadSettingsUtils, + ) => { + const { threadInfo, ...rest } = protocolInput.input; + return await utils.keyserverChangeThreadSettings({ ...rest }); + }, + + supportsCalendarHistory: true, + + calendarIsOnline: ( + tunnelbrokerSocketState: TunnelbrokerSocketState, + isKeyserverConnected: boolean, + ) => isKeyserverConnected, + + createCalendarEntry: ( + protocolInput: ProtocolCreateEntryInput, + utils: CreateEntryUtils, + ) => utils.keyserverCreateEntry(protocolInput.input.createEntryInfo), + + deleteCalendarEntry: ( + protocolInput: ProtocolDeleteEntryInput, + utils: DeleteEntryUtils, + ) => utils.keyserverDeleteEntry(protocolInput.input.deleteEntryInfo), + + editCalendarEntry: ( + protocolInput: ProtocolEditEntryInput, + utils: EditEntryUtils, + ) => utils.keyserverEditEntry(protocolInput.input.saveEntryInfo), + + setThreadUnreadStatus: ( + input: ProtocolSetThreadUnreadStatusInput, + utils: SetThreadUnreadStatusUtils, + ) => { + const { + input: { threadInfo, ...rest }, + } = input; + return utils.keyserverSetThreadUnreadStatus(rest); + }, + + sendReaction: async ( + input: ProtocolSendReactionInput, + utils: SendReactionUtils, + ) => { + const { + threadInfo, + viewerID, + messageID, + reaction, + action, + showErrorAlert, + } = input; + const threadID = threadInfo.id; + const localID = getNextLocalID(); + + const reactionMessagePromise = (async () => { + try { + const result = await utils.keyserverSendReaction({ + threadID, + localID, + targetMessageID: messageID, + reaction, + action, + }); + return { + localID, + serverID: result.id, + threadID, + time: result.time, + }; + } catch (e) { + showErrorAlert(); + const exceptionMessage = getMessageForException(e) ?? ''; + throw new SendMessageError( + `Exception while sending reaction: ${exceptionMessage}`, + localID, + threadID, + ); + } + })(); + + const startingPayload: RawReactionMessageInfo = { + type: messageTypes.REACTION, + threadID, + localID, + creatorID: viewerID, + time: Date.now(), targetMessageID: messageID, - text: newText, - }); + reaction, + action, + }; - return ({ - newMessageInfos: result.newMessageInfos, - }: { +newMessageInfos: $ReadOnlyArray }); - })(); + void utils.dispatchActionPromise( + sendReactionMessageActionTypes, + reactionMessagePromise, + undefined, + startingPayload, + ); - void utils.dispatchActionPromise( - sendEditMessageActionTypes, - editMessagePromise, - ); + await reactionMessagePromise; + }, - await editMessagePromise; - }, + addThreadMembers: async ( + input: ProtocolAddThreadMembersInput, + utils: AddThreadMembersUtils, + ) => { + const { threadInfo, newMemberIDs } = input; - changeThreadSettings: async ( - protocolInput: ProtocolChangeThreadSettingsInput, - utils: ChangeThreadSettingsUtils, - ) => { - const { threadInfo, ...rest } = protocolInput.input; - return await utils.keyserverChangeThreadSettings({ ...rest }); - }, - - supportsCalendarHistory: true, - - calendarIsOnline: ( - tunnelbrokerSocketState: TunnelbrokerSocketState, - isKeyserverConnected: boolean, - ) => isKeyserverConnected, - - createCalendarEntry: ( - protocolInput: ProtocolCreateEntryInput, - utils: CreateEntryUtils, - ) => utils.keyserverCreateEntry(protocolInput.input.createEntryInfo), - - deleteCalendarEntry: ( - protocolInput: ProtocolDeleteEntryInput, - utils: DeleteEntryUtils, - ) => utils.keyserverDeleteEntry(protocolInput.input.deleteEntryInfo), - - editCalendarEntry: ( - protocolInput: ProtocolEditEntryInput, - utils: EditEntryUtils, - ) => utils.keyserverEditEntry(protocolInput.input.saveEntryInfo), - - setThreadUnreadStatus: ( - input: ProtocolSetThreadUnreadStatusInput, - utils: SetThreadUnreadStatusUtils, - ) => { - const { - input: { threadInfo, ...rest }, - } = input; - return utils.keyserverSetThreadUnreadStatus(rest); - }, - - sendReaction: async ( - input: ProtocolSendReactionInput, - utils: SendReactionUtils, - ) => { - const { - threadInfo, - viewerID, - messageID, - reaction, - action, - showErrorAlert, - } = input; - const threadID = threadInfo.id; - const localID = getNextLocalID(); - - const reactionMessagePromise = (async () => { - try { - const result = await utils.keyserverSendReaction({ - threadID, - localID, - targetMessageID: messageID, - reaction, - action, - }); - return { - localID, - serverID: result.id, - threadID, - time: result.time, - }; - } catch (e) { - showErrorAlert(); - const exceptionMessage = getMessageForException(e) ?? ''; - throw new SendMessageError( - `Exception while sending reaction: ${exceptionMessage}`, - localID, - threadID, - ); - } - })(); - - const startingPayload: RawReactionMessageInfo = { - type: messageTypes.REACTION, - threadID, - localID, - creatorID: viewerID, - time: Date.now(), - targetMessageID: messageID, - reaction, - action, - }; + const changeThreadSettingsInput = { + threadInfo, + threadID: threadInfo.id, + changes: { newMemberIDs }, + }; - void utils.dispatchActionPromise( - sendReactionMessageActionTypes, - reactionMessagePromise, - undefined, - startingPayload, - ); + const addMembersPromise = utils.changeThreadSettings( + changeThreadSettingsInput, + ); - await reactionMessagePromise; - }, + void utils.dispatchActionPromise( + changeThreadSettingsActionTypes, + addMembersPromise, + ); + await addMembersPromise; + }, - addThreadMembers: async ( - input: ProtocolAddThreadMembersInput, - utils: AddThreadMembersUtils, - ) => { - const { threadInfo, newMemberIDs } = input; + updateSubscription: ( + protocolInput: ProtocolUpdateSubscriptionInput, + utils: UpdateSubscriptionUtils, + ) => { + const { threadInfo, ...rest } = protocolInput.input; + return utils.keyserverUpdateSubscription(rest); + }, - const changeThreadSettingsInput = { - threadInfo, - threadID: threadInfo.id, - changes: { newMemberIDs }, - }; + leaveThread: async ( + input: ProtocolLeaveThreadInput, + utils: LeaveThreadUtils, + ) => { + const threadID = input.threadInfo.id; + const promise = utils.keyserverLeaveThread({ threadID }); + void utils.dispatchActionPromise(leaveThreadActionTypes, promise, { + customKeyName: `${leaveThreadActionTypes.started}:${threadID}`, + }); + const result = await promise; + const invalidated = identifyInvalidatedThreads( + result.updatesResult.newUpdates, + ); + return { + invalidatedThreads: [...invalidated], + }; + }, - const addMembersPromise = utils.changeThreadSettings( - changeThreadSettingsInput, - ); + convertClientDBThreadInfo: ( + clientDBThreadInfo: ClientDBThreadInfo, + members: $ReadOnlyArray, + roles: { +[id: string]: RoleInfo }, + currentUser: ThreadCurrentUserInfo, + ) => { + const thinThreadType = assertThinThreadType(clientDBThreadInfo.type); + + let rawThreadInfo: ThinRawThreadInfo = { + minimallyEncoded: true, + id: clientDBThreadInfo.id, + type: thinThreadType, + name: clientDBThreadInfo.name, + description: clientDBThreadInfo.description, + color: clientDBThreadInfo.color, + creationTime: Number(clientDBThreadInfo.creationTime), + parentThreadID: clientDBThreadInfo.parentThreadID, + containingThreadID: clientDBThreadInfo.containingThreadID, + community: clientDBThreadInfo.community, + members, + roles, + currentUser, + repliesCount: clientDBThreadInfo.repliesCount, + pinnedCount: clientDBThreadInfo.pinnedCount, + }; - void utils.dispatchActionPromise( - changeThreadSettingsActionTypes, - addMembersPromise, - ); - await addMembersPromise; - }, + if (clientDBThreadInfo.sourceMessageID) { + rawThreadInfo = { + ...rawThreadInfo, + sourceMessageID: clientDBThreadInfo.sourceMessageID, + }; + } - updateSubscription: ( - protocolInput: ProtocolUpdateSubscriptionInput, - utils: UpdateSubscriptionUtils, - ) => { - const { threadInfo, ...rest } = protocolInput.input; - return utils.keyserverUpdateSubscription(rest); - }, + if (clientDBThreadInfo.avatar) { + rawThreadInfo = { + ...rawThreadInfo, + avatar: JSON.parse(clientDBThreadInfo.avatar), + }; + } - leaveThread: async ( - input: ProtocolLeaveThreadInput, - utils: LeaveThreadUtils, - ) => { - const threadID = input.threadInfo.id; - const promise = utils.keyserverLeaveThread({ threadID }); - void utils.dispatchActionPromise(leaveThreadActionTypes, promise, { - customKeyName: `${leaveThreadActionTypes.started}:${threadID}`, - }); - const result = await promise; - const invalidated = identifyInvalidatedThreads( - result.updatesResult.newUpdates, - ); - return { - invalidatedThreads: [...invalidated], - }; - }, + return rawThreadInfo; + }, - allowsDeletingSidebarSource: true, + allowsDeletingSidebarSource: true, - presentationDetails: { - membershipChangesShownInThreadPreview: false, - usersWithoutDeviceListExcludedFromSearchResult: false, - supportsMediaGallery: true, - nativeChatThreadListIcon: 'server', - webChatThreadListIcon: iconCreationFunction => - iconCreationFunction('server'), - threadAncestorLabel: (ancestorPath: React.Node) => ancestorPath, - }, + presentationDetails: { + membershipChangesShownInThreadPreview: false, + usersWithoutDeviceListExcludedFromSearchResult: false, + supportsMediaGallery: true, + nativeChatThreadListIcon: 'server', + webChatThreadListIcon: iconCreationFunction => + iconCreationFunction('server'), + threadAncestorLabel: (ancestorPath: React.Node) => ancestorPath, + }, - uploadMultimediaMetadataToKeyserver: true, + uploadMultimediaMetadataToKeyserver: true, - canActionsTargetPendingMessages: false, + canActionsTargetPendingMessages: false, - sidebarConfig: { - sidebarThreadType: thinThreadTypes.SIDEBAR, - pendingSidebarURLPrefix: pendingSidebarURLPrefix, - }, + sidebarConfig: { + sidebarThreadType: thinThreadTypes.SIDEBAR, + pendingSidebarURLPrefix: pendingSidebarURLPrefix, + }, - shouldPerformSideEffectsBeforeSendingMessage: true, + shouldPerformSideEffectsBeforeSendingMessage: true, - messagesStoredOnServer: true, + messagesStoredOnServer: true, - arePendingThreadsDescendantsOfGenesis: true, + arePendingThreadsDescendantsOfGenesis: true, - threadActivityUpdatedByDMActivityHandler: false, -}); + threadActivityUpdatedByDMActivityHandler: false, + }); function mediaIDIsKeyserverID(mediaID: string): boolean { return mediaID.indexOf('|') !== -1; diff --git a/lib/shared/threads/sidebar-spec.js b/lib/shared/threads/sidebar-spec.js --- a/lib/shared/threads/sidebar-spec.js +++ b/lib/shared/threads/sidebar-spec.js @@ -2,8 +2,9 @@ import { keyserverThreadProtocol } from './protocols/keyserver-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; -const sidebarSpec: ThreadSpec = Object.freeze({ +const sidebarSpec: ThreadSpec = Object.freeze({ traits: new Set(['sidebar']), protocol: keyserverThreadProtocol, threadLabel: 'Thread', diff --git a/lib/shared/threads/thick-sidebar-spec.js b/lib/shared/threads/thick-sidebar-spec.js --- a/lib/shared/threads/thick-sidebar-spec.js +++ b/lib/shared/threads/thick-sidebar-spec.js @@ -2,11 +2,13 @@ import { dmThreadProtocol } from './protocols/dm-thread-protocol.js'; import type { ThreadSpec } from './thread-spec.js'; +import type { MinimallyEncodedThickMemberInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; -const thickSidebarSpec: ThreadSpec = Object.freeze({ - traits: new Set(['sidebar']), - protocol: dmThreadProtocol, - threadLabel: 'Thread', -}); +const thickSidebarSpec: ThreadSpec = + Object.freeze({ + traits: new Set(['sidebar']), + protocol: dmThreadProtocol, + threadLabel: 'Thread', + }); export { thickSidebarSpec }; diff --git a/lib/shared/threads/thread-spec.js b/lib/shared/threads/thread-spec.js --- a/lib/shared/threads/thread-spec.js +++ b/lib/shared/threads/thread-spec.js @@ -47,6 +47,10 @@ } from '../../types/message-types.js'; import type { RawTextMessageInfo } from '../../types/messages/text.js'; import type { + MemberInfoSansPermissions, + MinimallyEncodedThickMemberInfo, + RoleInfo, + ThreadCurrentUserInfo, RawThreadInfo, ThreadInfo, } from '../../types/minimally-encoded-thread-permissions-types.js'; @@ -58,6 +62,7 @@ import type { ThreadType } from '../../types/thread-types-enum.js'; import type { ChangeThreadSettingsPayload, + ClientDBThreadInfo, LeaveThreadPayload, UpdateThreadRequest, } from '../../types/thread-types.js'; @@ -206,7 +211,11 @@ +dispatchActionPromise: DispatchActionPromise, }; -export type ThreadProtocol = { +export type ThreadProtocol< + RawThreadMemberType: + | MemberInfoSansPermissions + | MinimallyEncodedThickMemberInfo, +> = { +sendTextMessage: ( message: ProtocolSendTextMessageInput, utils: SendTextMessageUtils, @@ -260,6 +269,12 @@ input: ProtocolLeaveThreadInput, utils: LeaveThreadUtils, ) => Promise, + +convertClientDBThreadInfo: ( + clientDBThreadInfo: ClientDBThreadInfo, + members: $ReadOnlyArray, + roles: { +[id: string]: RoleInfo }, + minimallyEncodedCurrentUser: ThreadCurrentUserInfo, + ) => RawThreadInfo, +allowsDeletingSidebarSource: boolean, +presentationDetails: { +membershipChangesShownInThreadPreview: boolean, @@ -285,8 +300,12 @@ +threadActivityUpdatedByDMActivityHandler: boolean, }; -export type ThreadSpec = { +export type ThreadSpec< + RawThreadMemberType: + | MemberInfoSansPermissions + | MinimallyEncodedThickMemberInfo, +> = { +traits: $ReadOnlySet, - +protocol: ThreadProtocol, + +protocol: ThreadProtocol, +threadLabel: string, }; diff --git a/lib/shared/threads/thread-specs.js b/lib/shared/threads/thread-specs.js --- a/lib/shared/threads/thread-specs.js +++ b/lib/shared/threads/thread-specs.js @@ -20,7 +20,7 @@ import { values } from '../../utils/objects.js'; export const threadSpecs: { - +[ThreadType]: ThreadSpec, + +[ThreadType]: ThreadSpec, } = Object.freeze({ [threadTypes.SIDEBAR]: sidebarSpec, [threadTypes.GENESIS_PERSONAL]: genesisPersonalSpec, diff --git a/lib/utils/thread-ops-utils.js b/lib/utils/thread-ops-utils.js --- a/lib/utils/thread-ops-utils.js +++ b/lib/utils/thread-ops-utils.js @@ -2,7 +2,6 @@ import invariant from 'invariant'; -import { assertWithValidator } from './validation-utils.js'; import { memberInfoWithPermissionsValidator, persistedRoleInfoValidator, @@ -10,6 +9,7 @@ minimallyEncodedThickMemberInfoValidator, memberInfoSansPermissionsValidator, } from '../permissions/minimally-encoded-raw-thread-info-validators.js'; +import { threadSpecs } from '../shared/threads/thread-specs.js'; import type { RawThreadInfo, RoleInfo, @@ -20,19 +20,13 @@ minimallyEncodeRoleInfo, minimallyEncodeThreadCurrentUserInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; -import { - assertThreadType, - threadTypeIsThick, - assertThinThreadType, - assertThickThreadType, -} from '../types/thread-types-enum.js'; +import { assertThreadType } from '../types/thread-types-enum.js'; import { type ClientDBThreadInfo, legacyMemberInfoValidator, type LegacyRawThreadInfo, clientLegacyRoleInfoValidator, legacyThreadCurrentUserInfoValidator, - threadTimestampsValidator, } from '../types/thread-types.js'; function convertRawThreadInfoToClientDBThreadInfo( @@ -99,72 +93,14 @@ ? rawCurrentUser : minimallyEncodeThreadCurrentUserInfo(rawCurrentUser); - let rawThreadInfo: RawThreadInfo; const threadType = assertThreadType(clientDBThreadInfo.type); - if (threadTypeIsThick(threadType)) { - const thickThreadType = assertThickThreadType(threadType); - invariant( - clientDBThreadInfo.timestamps, - 'Thick thread info must contain the timestamps', - ); - const threadTimestamps = assertWithValidator( - JSON.parse(clientDBThreadInfo.timestamps), - threadTimestampsValidator, - ); - rawThreadInfo = { - minimallyEncoded: true, - thick: true, - id: clientDBThreadInfo.id, - type: thickThreadType, - name: clientDBThreadInfo.name, - description: clientDBThreadInfo.description, - color: clientDBThreadInfo.color, - creationTime: Number(clientDBThreadInfo.creationTime), - parentThreadID: clientDBThreadInfo.parentThreadID, - containingThreadID: clientDBThreadInfo.containingThreadID, - members: minimallyEncodedMembers, - roles: minimallyEncodedRoles, - currentUser: minimallyEncodedCurrentUser, - repliesCount: clientDBThreadInfo.repliesCount, - pinnedCount: clientDBThreadInfo.pinnedCount, - timestamps: threadTimestamps, - }; - } else { - const thinThreadType = assertThinThreadType(threadType); - rawThreadInfo = { - minimallyEncoded: true, - id: clientDBThreadInfo.id, - type: thinThreadType, - name: clientDBThreadInfo.name, - description: clientDBThreadInfo.description, - color: clientDBThreadInfo.color, - creationTime: Number(clientDBThreadInfo.creationTime), - parentThreadID: clientDBThreadInfo.parentThreadID, - containingThreadID: clientDBThreadInfo.containingThreadID, - community: clientDBThreadInfo.community, - members: minimallyEncodedMembers, - roles: minimallyEncodedRoles, - currentUser: minimallyEncodedCurrentUser, - repliesCount: clientDBThreadInfo.repliesCount, - pinnedCount: clientDBThreadInfo.pinnedCount, - }; - } - if (clientDBThreadInfo.sourceMessageID) { - rawThreadInfo = { - ...rawThreadInfo, - sourceMessageID: clientDBThreadInfo.sourceMessageID, - }; - } - - if (clientDBThreadInfo.avatar) { - rawThreadInfo = { - ...rawThreadInfo, - avatar: JSON.parse(clientDBThreadInfo.avatar), - }; - } - - return rawThreadInfo; + return threadSpecs[threadType].protocol.convertClientDBThreadInfo( + clientDBThreadInfo, + minimallyEncodedMembers, + minimallyEncodedRoles, + minimallyEncodedCurrentUser, + ); } // WARNING: Do not consume or delete this function!