diff --git a/lib/actions/thread-actions.js b/lib/actions/thread-actions.js index de6f021bd..52b5bb35e 100644 --- a/lib/actions/thread-actions.js +++ b/lib/actions/thread-actions.js @@ -1,570 +1,575 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import uuid from 'uuid'; import genesis from '../facts/genesis.js'; import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { useKeyserverCall } from '../keyserver-conn/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import { type OutboundDMOperationSpecification, dmOperationSpecificationTypes, getCreateThickRawThreadInfoInputFromThreadInfo, } from '../shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from '../shared/dm-ops/process-dm-ops.js'; import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; import type { DMChangeThreadSettingsOperation, DMThreadSettingsChanges, DMJoinThreadOperation, } from '../types/dm-ops.js'; import type { ThreadInfo, RawThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import { thickThreadTypes } from '../types/thread-types-enum.js'; import type { ChangeThreadSettingsPayload, LeaveThreadPayload, UpdateThreadRequest, ClientNewThinThreadRequest, NewThreadResult, ClientThreadJoinRequest, ThreadJoinPayload, ThreadFetchMediaRequest, ThreadFetchMediaResult, RoleModificationRequest, RoleModificationPayload, RoleDeletionRequest, RoleDeletionPayload, UpdateThickThreadRequest, } from '../types/thread-types.js'; import { values } from '../utils/objects.js'; import { useSelector } from '../utils/redux-utils.js'; export type DeleteThreadInput = { +threadID: string, }; const deleteThreadActionTypes = Object.freeze({ started: 'DELETE_THREAD_STARTED', success: 'DELETE_THREAD_SUCCESS', failed: 'DELETE_THREAD_FAILED', }); const deleteThreadEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const deleteThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: DeleteThreadInput) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'delete_thread', requests, deleteThreadEndpointOptions, ); const response = responses[keyserverID]; return { updatesResult: response.updatesResult, }; }; function useDeleteThread(): ( input: DeleteThreadInput, ) => Promise { return useKeyserverCall(deleteThread); } const changeThreadSettingsActionTypes = Object.freeze({ started: 'CHANGE_THREAD_SETTINGS_STARTED', success: 'CHANGE_THREAD_SETTINGS_SUCCESS', failed: 'CHANGE_THREAD_SETTINGS_FAILED', }); const changeThreadSettingsEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const changeThreadSettings = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: UpdateThreadRequest) => Promise) => async input => { invariant( Object.keys(input.changes).length > 0, 'No changes provided to changeThreadSettings!', ); const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'update_thread', requests, changeThreadSettingsEndpointOptions, ); const response = responses[keyserverID]; return { threadID: input.threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; export type UseChangeThreadSettingsInput = $ReadOnly< | { ...UpdateThreadRequest, +thick: false, } | { ...UpdateThickThreadRequest, +thick: true, +threadInfo: ThreadInfo, }, >; function useChangeThreadSettings(): ( input: UseChangeThreadSettingsInput, ) => Promise { const processAndSendDMOperation = useProcessAndSendDMOperation(); const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const keyserverCall = useKeyserverCall(changeThreadSettings); return React.useCallback( async (input: UseChangeThreadSettingsInput) => { if (!input.thick) { const { thick, ...rest } = input; return await keyserverCall({ ...rest }); } invariant(viewerID, 'viewerID should be set'); 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 === 'remove' + ) { + changes.avatar = null; } // To support `image` and `encrypted_image` avatars we first, need stop // sending multimedia metadata to keyserver. // ENG-8708 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 processAndSendDMOperation(opSpecification); return ({ threadID: threadInfo.id, updatesResult: { newUpdates: [] }, newMessageInfos: [], }: ChangeThreadSettingsPayload); }, [keyserverCall, processAndSendDMOperation, viewerID], ); } export type RemoveUsersFromThreadInput = { +threadID: string, +memberIDs: $ReadOnlyArray, }; const removeUsersFromThreadActionTypes = Object.freeze({ started: 'REMOVE_USERS_FROM_THREAD_STARTED', success: 'REMOVE_USERS_FROM_THREAD_SUCCESS', failed: 'REMOVE_USERS_FROM_THREAD_FAILED', }); const removeMembersFromThreadEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const removeUsersFromThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: RemoveUsersFromThreadInput, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'remove_members', requests, removeMembersFromThreadEndpointOptions, ); const response = responses[keyserverID]; return { threadID: input.threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; function useRemoveUsersFromThread(): ( input: RemoveUsersFromThreadInput, ) => Promise { return useKeyserverCall(removeUsersFromThread); } export type ChangeThreadMemberRolesInput = { +threadID: string, +memberIDs: $ReadOnlyArray, +newRole: string, }; const changeThreadMemberRolesActionTypes = Object.freeze({ started: 'CHANGE_THREAD_MEMBER_ROLES_STARTED', success: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS', failed: 'CHANGE_THREAD_MEMBER_ROLES_FAILED', }); const changeThreadMemberRoleEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const changeThreadMemberRoles = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: ChangeThreadMemberRolesInput, ) => Promise) => async input => { const { threadID, memberIDs, newRole } = input; const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: { threadID, memberIDs, role: newRole, }, }; const responses = await callKeyserverEndpoint( 'update_role', requests, changeThreadMemberRoleEndpointOptions, ); const response = responses[keyserverID]; return { threadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, }; }; function useChangeThreadMemberRoles(): ( input: ChangeThreadMemberRolesInput, ) => Promise { return useKeyserverCall(changeThreadMemberRoles); } const newThreadActionTypes = Object.freeze({ started: 'NEW_THREAD_STARTED', success: 'NEW_THREAD_SUCCESS', failed: 'NEW_THREAD_FAILED', }); const newThinThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ClientNewThinThreadRequest) => Promise) => async input => { const parentThreadID = input.parentThreadID ?? genesis().id; const keyserverID = extractKeyserverIDFromID(parentThreadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint('create_thread', requests); const response = responses[keyserverID]; return { newThreadID: response.newThreadID, updatesResult: response.updatesResult, newMessageInfos: response.newMessageInfos, userInfos: response.userInfos, }; }; function useNewThinThread(): ( input: ClientNewThinThreadRequest, ) => Promise { return useKeyserverCall(newThinThread); } const joinThreadActionTypes = Object.freeze({ started: 'JOIN_THREAD_STARTED', success: 'JOIN_THREAD_SUCCESS', failed: 'JOIN_THREAD_FAILED', }); const joinThreadOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const joinThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ClientThreadJoinRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'join_thread', requests, joinThreadOptions, ); const response = responses[keyserverID]; const userInfos = values(response.userInfos); return { updatesResult: response.updatesResult, rawMessageInfos: response.rawMessageInfos, truncationStatuses: response.truncationStatuses, userInfos, keyserverID, }; }; export type UseJoinThreadInput = $ReadOnly< | { +thick: false, ...ClientThreadJoinRequest, } | { +thick: true, +rawThreadInfo: RawThreadInfo, }, >; function useJoinThread(): ( input: UseJoinThreadInput, ) => Promise { const processAndSendDMOperation = useProcessAndSendDMOperation(); const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const keyserverCall = useKeyserverCall(joinThread); return React.useCallback( async (input: UseJoinThreadInput) => { if (!input.thick) { const { thick, ...rest } = input; return await keyserverCall({ ...rest }); } const { rawThreadInfo } = input; invariant(viewerID, 'viewerID should be set'); invariant(rawThreadInfo.thick, 'thread must be thick'); const existingThreadDetails = getCreateThickRawThreadInfoInputFromThreadInfo(rawThreadInfo); const op: DMJoinThreadOperation = { type: 'join_thread', joinerID: viewerID, time: Date.now(), messageID: uuid.v4(), existingThreadDetails, }; const opSpecification: OutboundDMOperationSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op, recipients: { type: 'all_thread_members', threadID: rawThreadInfo.type === thickThreadTypes.THICK_SIDEBAR && rawThreadInfo.parentThreadID ? rawThreadInfo.parentThreadID : rawThreadInfo.id, }, }; await processAndSendDMOperation(opSpecification); return ({ updatesResult: { newUpdates: [] }, rawMessageInfos: [], truncationStatuses: {}, userInfos: [], }: ThreadJoinPayload); }, [keyserverCall, processAndSendDMOperation, viewerID], ); } export type LeaveThreadInput = { +threadID: string, }; const leaveThreadActionTypes = Object.freeze({ started: 'LEAVE_THREAD_STARTED', success: 'LEAVE_THREAD_SUCCESS', failed: 'LEAVE_THREAD_FAILED', }); const leaveThreadEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const leaveThread = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: LeaveThreadInput) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'leave_thread', requests, leaveThreadEndpointOptions, ); const response = responses[keyserverID]; return { updatesResult: response.updatesResult, }; }; function useLeaveThread(): ( input: LeaveThreadInput, ) => Promise { return useKeyserverCall(leaveThread); } const fetchThreadMedia = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: ThreadFetchMediaRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'fetch_thread_media', requests, ); const response = responses[keyserverID]; return { media: response.media }; }; function useFetchThreadMedia(): ( input: ThreadFetchMediaRequest, ) => Promise { return useKeyserverCall(fetchThreadMedia); } const modifyCommunityRoleActionTypes = Object.freeze({ started: 'MODIFY_COMMUNITY_ROLE_STARTED', success: 'MODIFY_COMMUNITY_ROLE_SUCCESS', failed: 'MODIFY_COMMUNITY_ROLE_FAILED', }); const modifyCommunityRole = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: RoleModificationRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.community); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'modify_community_role', requests, ); const response = responses[keyserverID]; return { threadInfo: response.threadInfo, updatesResult: response.updatesResult, }; }; function useModifyCommunityRole(): ( input: RoleModificationRequest, ) => Promise { return useKeyserverCall(modifyCommunityRole); } const deleteCommunityRoleActionTypes = Object.freeze({ started: 'DELETE_COMMUNITY_ROLE_STARTED', success: 'DELETE_COMMUNITY_ROLE_SUCCESS', failed: 'DELETE_COMMUNITY_ROLE_FAILED', }); const deleteCommunityRole = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: RoleDeletionRequest) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.community); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'delete_community_role', requests, ); const response = responses[keyserverID]; return { threadInfo: response.threadInfo, updatesResult: response.updatesResult, }; }; function useDeleteCommunityRole(): ( input: RoleDeletionRequest, ) => Promise { return useKeyserverCall(deleteCommunityRole); } export { deleteThreadActionTypes, useDeleteThread, changeThreadSettingsActionTypes, useChangeThreadSettings, removeUsersFromThreadActionTypes, useRemoveUsersFromThread, changeThreadMemberRolesActionTypes, useChangeThreadMemberRoles, newThreadActionTypes, useNewThinThread, joinThreadActionTypes, useJoinThread, leaveThreadActionTypes, useLeaveThread, useFetchThreadMedia, modifyCommunityRoleActionTypes, useModifyCommunityRole, deleteCommunityRoleActionTypes, useDeleteCommunityRole, }; diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js index 872718e50..6d27a9179 100644 --- a/lib/shared/dm-ops/change-thread-settings-spec.js +++ b/lib/shared/dm-ops/change-thread-settings-spec.js @@ -1,174 +1,183 @@ // @flow import invariant from 'invariant'; import uuid from 'uuid'; import type { DMOperationSpec, ProcessDMOperationUtilities, } from './dm-op-spec.js'; import type { DMChangeThreadSettingsOperation, DMThreadSettingsChanges, } from '../../types/dm-ops.js'; import type { MessageData, RawMessageInfo } from '../../types/message-types'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ChangeSettingsMessageData } from '../../types/messages/change-settings.js'; import type { RawThreadInfo, ThickRawThreadInfo, } from '../../types/minimally-encoded-thread-permissions-types.js'; import { updateTypes } from '../../types/update-types-enum.js'; import type { ClientUpdateInfo } from '../../types/update-types.js'; import { values } from '../../utils/objects.js'; import { rawMessageInfoFromMessageData } from '../message-utils.js'; function getThreadIDFromChangeThreadSettingsDMOp( dmOperation: DMChangeThreadSettingsOperation, ): string { return dmOperation.type === 'change_thread_settings' ? dmOperation.threadID : dmOperation.existingThreadDetails.threadID; } function createChangeSettingsMessageDatasAndUpdate( dmOperation: DMChangeThreadSettingsOperation, ): { +fieldNameToMessageData: { +[fieldName: string]: ChangeSettingsMessageData }, +threadInfoUpdate: DMThreadSettingsChanges, } { const { changes, editorID, time } = dmOperation; const { name, description, color, avatar } = changes; const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); const threadInfoUpdate: { ...DMThreadSettingsChanges } = {}; if (name !== undefined && name !== null) { threadInfoUpdate.name = name; } if (description !== undefined && description !== null) { threadInfoUpdate.description = description; } if (color) { threadInfoUpdate.color = color; } - if (avatar) { + if (avatar || avatar === null) { threadInfoUpdate.avatar = avatar; } const fieldNameToMessageData: { [fieldName: string]: ChangeSettingsMessageData, } = {}; const { avatar: avatarObject, ...rest } = threadInfoUpdate; - const normalizedThreadInfoUpdate = avatarObject - ? { ...rest, avatar: JSON.stringify(avatarObject) } - : { ...rest }; + let normalizedThreadInfoUpdate; + if (avatarObject) { + normalizedThreadInfoUpdate = { + ...rest, + avatar: JSON.stringify(avatarObject), + }; + } else if (avatarObject === null) { + // clear thread avatar + normalizedThreadInfoUpdate = { ...rest, avatar: '' }; + } else { + normalizedThreadInfoUpdate = { ...rest }; + } for (const fieldName in normalizedThreadInfoUpdate) { const value = normalizedThreadInfoUpdate[fieldName]; fieldNameToMessageData[fieldName] = { type: messageTypes.CHANGE_SETTINGS, threadID, creatorID: editorID, time, field: fieldName, value: value, }; } return { fieldNameToMessageData, threadInfoUpdate }; } const changeThreadSettingsSpec: DMOperationSpec = Object.freeze({ notificationsCreationData: async ( dmOperation: DMChangeThreadSettingsOperation, ) => { const messageDatas: Array = []; const { fieldNameToMessageData } = createChangeSettingsMessageDatasAndUpdate(dmOperation); messageDatas.push(...values(fieldNameToMessageData)); return { messageDatas }; }, processDMOperation: async ( dmOperation: DMChangeThreadSettingsOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) => { const { time, messageIDsPrefix } = dmOperation; const threadID = getThreadIDFromChangeThreadSettingsDMOp(dmOperation); const threadInfo: ?RawThreadInfo = utilities.threadInfos[threadID]; const updateInfos: Array = []; const rawMessageInfos: Array = []; const { fieldNameToMessageData, threadInfoUpdate } = createChangeSettingsMessageDatasAndUpdate(dmOperation); const fieldNameToMessageDataPairs = Object.entries( fieldNameToMessageData, ); rawMessageInfos.push( ...fieldNameToMessageDataPairs.map(([fieldName, messageData]) => rawMessageInfoFromMessageData( messageData, `${messageIDsPrefix}/${fieldName}`, ), ), ); invariant(threadInfo?.thick, 'Thread should be thick'); let threadInfoToUpdate: ThickRawThreadInfo = threadInfo; for (const fieldName in threadInfoUpdate) { const timestamp = threadInfoToUpdate.timestamps[fieldName]; if (timestamp < time) { threadInfoToUpdate = { ...threadInfoToUpdate, [fieldName]: threadInfoUpdate[fieldName], timestamps: { ...threadInfoToUpdate.timestamps, [fieldName]: time, }, }; } } if (fieldNameToMessageDataPairs.length > 0) { updateInfos.push({ type: updateTypes.UPDATE_THREAD, id: uuid.v4(), time, threadInfo: threadInfoToUpdate, }); } return { rawMessageInfos, updateInfos, }; }, canBeProcessed( dmOperation: DMChangeThreadSettingsOperation, viewerID: string, utilities: ProcessDMOperationUtilities, ) { if (utilities.threadInfos[dmOperation.threadID]) { return { isProcessingPossible: true }; } return { isProcessingPossible: false, reason: { type: 'missing_thread', threadID: dmOperation.threadID, }, }; }, supportsAutoRetry: true, }); export { changeThreadSettingsSpec, createChangeSettingsMessageDatasAndUpdate }; diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js index 8296fd758..d9c034b86 100644 --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -1,438 +1,438 @@ // @flow import t, { type TInterface, type TUnion } from 'tcomb'; import { clientAvatarValidator, type ClientAvatar } from './avatar-types.js'; import type { RawMessageInfo } from './message-types.js'; import type { NotificationsCreationData } from './notif-types.js'; import type { OutboundP2PMessage } from './sqlite-types.js'; import { type ThreadSubscription, threadSubscriptionValidator, } from './subscription-types.js'; import { type NonSidebarThickThreadType, nonSidebarThickThreadTypes, type ThickThreadType, thickThreadTypeValidator, } from './thread-types-enum.js'; import { threadTimestampsValidator, type ThreadTimestamps, } from './thread-types.js'; import type { ClientUpdateInfo } from './update-types.js'; import { values } from '../utils/objects.js'; import { tColor, tShape, tString, tUserID } from '../utils/validation-utils.js'; export const dmOperationTypes = Object.freeze({ CREATE_THREAD: 'create_thread', CREATE_SIDEBAR: 'create_sidebar', SEND_TEXT_MESSAGE: 'send_text_message', SEND_REACTION_MESSAGE: 'send_reaction_message', SEND_EDIT_MESSAGE: 'send_edit_message', ADD_MEMBERS: 'add_members', ADD_VIEWER_TO_THREAD_MEMBERS: 'add_viewer_to_thread_members', JOIN_THREAD: 'join_thread', LEAVE_THREAD: 'leave_thread', REMOVE_MEMBERS: 'remove_members', CHANGE_THREAD_SETTINGS: 'change_thread_settings', CHANGE_THREAD_SUBSCRIPTION: 'change_thread_subscription', CHANGE_THREAD_READ_STATUS: 'change_thread_read_status', }); export type DMOperationType = $Values; type MemberIDWithSubscription = { +id: string, +subscription: ThreadSubscription, }; export const memberIDWithSubscriptionValidator: TInterface = tShape({ id: tUserID, subscription: threadSubscriptionValidator, }); export type CreateThickRawThreadInfoInput = { +threadID: string, +threadType: ThickThreadType, +creationTime: number, +parentThreadID?: ?string, +allMemberIDsWithSubscriptions: $ReadOnlyArray, +roleID: string, +unread: boolean, +timestamps: ThreadTimestamps, +name?: ?string, +avatar?: ?ClientAvatar, +description?: ?string, +color?: ?string, +containingThreadID?: ?string, +sourceMessageID?: ?string, +repliesCount?: ?number, +pinnedCount?: ?number, }; export const createThickRawThreadInfoInputValidator: TInterface = tShape({ threadID: t.String, threadType: thickThreadTypeValidator, creationTime: t.Number, parentThreadID: t.maybe(t.String), allMemberIDsWithSubscriptions: t.list(memberIDWithSubscriptionValidator), roleID: t.String, unread: t.Boolean, timestamps: threadTimestampsValidator, name: t.maybe(t.String), avatar: t.maybe(clientAvatarValidator), description: t.maybe(t.String), color: t.maybe(t.String), containingThreadID: t.maybe(t.String), sourceMessageID: t.maybe(t.String), repliesCount: t.maybe(t.Number), pinnedCount: t.maybe(t.Number), }); export type DMCreateThreadOperation = { +type: 'create_thread', +threadID: string, +creatorID: string, +time: number, +threadType: NonSidebarThickThreadType, +memberIDs: $ReadOnlyArray, +roleID: string, +newMessageID: string, }; export const dmCreateThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_THREAD), threadID: t.String, creatorID: tUserID, time: t.Number, threadType: t.enums.of(values(nonSidebarThickThreadTypes)), memberIDs: t.list(tUserID), roleID: t.String, newMessageID: t.String, }); export type DMCreateSidebarOperation = { +type: 'create_sidebar', +threadID: string, +creatorID: string, +time: number, +parentThreadID: string, +memberIDs: $ReadOnlyArray, +sourceMessageID: string, +roleID: string, +newSidebarSourceMessageID: string, +newCreateSidebarMessageID: string, }; export const dmCreateSidebarOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CREATE_SIDEBAR), threadID: t.String, creatorID: tUserID, time: t.Number, parentThreadID: t.String, memberIDs: t.list(tUserID), sourceMessageID: t.String, roleID: t.String, newSidebarSourceMessageID: t.String, newCreateSidebarMessageID: t.String, }); export type DMSendTextMessageOperation = { +type: 'send_text_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +text: string, }; export const dmSendTextMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_TEXT_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, text: t.String, }); export type DMSendReactionMessageOperation = { +type: 'send_reaction_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +targetMessageID: string, +reaction: string, +action: 'add_reaction' | 'remove_reaction', }; export const dmSendReactionMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_REACTION_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, targetMessageID: t.String, reaction: t.String, action: t.enums.of(['add_reaction', 'remove_reaction']), }); export type DMSendEditMessageOperation = { +type: 'send_edit_message', +threadID: string, +creatorID: string, +time: number, +messageID: string, +targetMessageID: string, +text: string, }; export const dmSendEditMessageOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.SEND_EDIT_MESSAGE), threadID: t.String, creatorID: tUserID, time: t.Number, messageID: t.String, targetMessageID: t.String, text: t.String, }); type DMAddMembersBase = { +editorID: string, +time: number, +addedUserIDs: $ReadOnlyArray, }; const dmAddMembersBaseValidatorShape = { editorID: tUserID, time: t.Number, addedUserIDs: t.list(tUserID), }; export type DMAddMembersOperation = $ReadOnly<{ +type: 'add_members', +threadID: string, +messageID: string, ...DMAddMembersBase, }>; export const dmAddMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_MEMBERS), threadID: t.String, messageID: t.String, ...dmAddMembersBaseValidatorShape, }); export type DMAddViewerToThreadMembersOperation = $ReadOnly<{ +type: 'add_viewer_to_thread_members', +messageID: ?string, +existingThreadDetails: CreateThickRawThreadInfoInput, ...DMAddMembersBase, }>; export const dmAddViewerToThreadMembersValidator: TInterface = tShape({ type: tString(dmOperationTypes.ADD_VIEWER_TO_THREAD_MEMBERS), messageID: t.maybe(t.String), existingThreadDetails: createThickRawThreadInfoInputValidator, ...dmAddMembersBaseValidatorShape, }); export type DMJoinThreadOperation = { +type: 'join_thread', +joinerID: string, +time: number, +messageID: string, +existingThreadDetails: CreateThickRawThreadInfoInput, }; export const dmJoinThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.JOIN_THREAD), joinerID: tUserID, time: t.Number, messageID: t.String, existingThreadDetails: createThickRawThreadInfoInputValidator, }); export type DMLeaveThreadOperation = { +type: 'leave_thread', +editorID: string, +time: number, +messageID: string, +threadID: string, }; export const dmLeaveThreadOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.LEAVE_THREAD), editorID: tUserID, time: t.Number, messageID: t.String, threadID: t.String, }); export type DMRemoveMembersOperation = { +type: 'remove_members', +editorID: string, +time: number, +messageID: string, +threadID: string, +removedUserIDs: $ReadOnlyArray, }; export const dmRemoveMembersOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.REMOVE_MEMBERS), editorID: tUserID, time: t.Number, messageID: t.String, threadID: t.String, removedUserIDs: t.list(tUserID), }); export type DMThreadSettingsChanges = { +name?: string, +description?: string, +color?: string, - +avatar?: ClientAvatar, + +avatar?: ClientAvatar | null, }; export type DMChangeThreadSettingsOperation = $ReadOnly<{ +type: 'change_thread_settings', +threadID: string, +editorID: string, +time: number, +changes: DMThreadSettingsChanges, +messageIDsPrefix: string, }>; export const dmChangeThreadSettingsOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS), threadID: t.String, editorID: tUserID, time: t.Number, changes: tShape({ name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), avatar: t.maybe(clientAvatarValidator), }), messageIDsPrefix: t.String, }); export type DMChangeThreadSubscriptionOperation = { +type: 'change_thread_subscription', +time: number, +threadID: string, +creatorID: string, +subscription: ThreadSubscription, }; export const dmChangeThreadSubscriptionOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_SUBSCRIPTION), time: t.Number, threadID: t.String, creatorID: tUserID, subscription: threadSubscriptionValidator, }); export type DMChangeThreadReadStatusOperation = { +type: 'change_thread_read_status', +time: number, +threadID: string, +creatorID: string, +unread: boolean, }; export const dmChangeThreadReadStatusOperationValidator: TInterface = tShape({ type: tString(dmOperationTypes.CHANGE_THREAD_READ_STATUS), time: t.Number, threadID: t.String, creatorID: tUserID, unread: t.Boolean, }); export type DMOperation = | DMCreateThreadOperation | DMCreateSidebarOperation | DMSendTextMessageOperation | DMSendReactionMessageOperation | DMSendEditMessageOperation | DMAddMembersOperation | DMAddViewerToThreadMembersOperation | DMJoinThreadOperation | DMLeaveThreadOperation | DMRemoveMembersOperation | DMChangeThreadSettingsOperation | DMChangeThreadSubscriptionOperation | DMChangeThreadReadStatusOperation; export const dmOperationValidator: TUnion = t.union([ dmCreateThreadOperationValidator, dmCreateSidebarOperationValidator, dmSendTextMessageOperationValidator, dmSendReactionMessageOperationValidator, dmSendEditMessageOperationValidator, dmAddMembersOperationValidator, dmAddViewerToThreadMembersValidator, dmJoinThreadOperationValidator, dmLeaveThreadOperationValidator, dmRemoveMembersOperationValidator, dmChangeThreadSettingsOperationValidator, dmChangeThreadSubscriptionOperationValidator, dmChangeThreadReadStatusOperationValidator, ]); export type DMOperationResult = { rawMessageInfos: Array, updateInfos: Array, }; export const processDMOpsActionType = 'PROCESS_DM_OPS'; export type ProcessDMOpsPayload = { +rawMessageInfos: $ReadOnlyArray, +updateInfos: $ReadOnlyArray, +outboundP2PMessages: ?$ReadOnlyArray, // For messages that could be retried from UI, we need to bind DM `messageID` // with `outboundP2PMessages` to keep track of whether all P2P messages // were queued on Tunnelbroker. +messageIDWithoutAutoRetry: ?string, +notificationsCreationData: ?NotificationsCreationData, }; export const queueDMOpsActionType = 'QUEUE_DM_OPS'; export type QueueDMOpsPayload = { +operation: DMOperation, +threadID: string, +timestamp: number, }; export const pruneDMOpsQueueActionType = 'PRUNE_DM_OPS_QUEUE'; export type PruneDMOpsQueuePayload = { +pruneMaxTimestamp: number, }; export const clearQueuedThreadDMOpsActionType = 'CLEAR_QUEUED_THREAD_DM_OPS'; export type ClearQueuedThreadDMOpsPayload = { +threadID: string, }; export type QueuedDMOperations = { +operations: { +[threadID: string]: $ReadOnlyArray<{ +operation: DMOperation, +timestamp: number, }>, }, }; export type SendDMStartedPayload = { +messageID: string, }; export type SendDMOpsSuccessPayload = { +messageID: string, +outboundP2PMessageIDs: $ReadOnlyArray, }; export const sendDMActionTypes = Object.freeze({ started: 'SEND_DM_STARTED', success: 'SEND_DM_SUCCESS', failed: 'SEND_DM_FAILED', });