diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js index cb7983b3b..e1df84239 100644 --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -1,411 +1,412 @@ // @flow import invariant from 'invariant'; import PropTypes from 'prop-types'; import type { Shape } from './core'; import type { CalendarQuery, CalendarResult, RawEntryInfo, } from './entry-types'; import type { RawMessageInfo, MessageTruncationStatuses, } from './message-types'; import type { ClientThreadInconsistencyReportCreationRequest } from './report-types'; import type { ThreadSubscription } from './subscription-types'; import type { UpdateInfo } from './update-types'; import type { UserInfo, AccountUserInfo } from './user-types'; export const threadTypes = Object.freeze({ //OPEN: 0, (DEPRECATED) //CLOSED: 1, (DEPRECATED) //SECRET: 2, (DEPRECATED) CHAT_NESTED_OPEN: 3, CHAT_SECRET: 4, SIDEBAR: 5, PERSONAL: 6, PRIVATE: 7, }); export type ThreadType = $Values; export function assertThreadType(threadType: number): ThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6 || threadType === 7, 'number is not ThreadType enum', ); return threadType; } export const threadPermissions = Object.freeze({ KNOW_OF: 'know_of', VISIBLE: 'visible', VOICED: 'voiced', EDIT_ENTRIES: 'edit_entries', EDIT_THREAD: 'edit_thread', DELETE_THREAD: 'delete_thread', CREATE_SUBTHREADS: 'create_subthreads', CREATE_SIDEBARS: 'create_sidebars', JOIN_THREAD: 'join_thread', EDIT_PERMISSIONS: 'edit_permissions', ADD_MEMBERS: 'add_members', REMOVE_MEMBERS: 'remove_members', CHANGE_ROLE: 'change_role', LEAVE_THREAD: 'leave_thread', }); export type ThreadPermission = $Values; export function assertThreadPermissions( ourThreadPermissions: string, ): ThreadPermission { invariant( ourThreadPermissions === 'know_of' || ourThreadPermissions === 'visible' || ourThreadPermissions === 'voiced' || ourThreadPermissions === 'edit_entries' || ourThreadPermissions === 'edit_thread' || ourThreadPermissions === 'delete_thread' || ourThreadPermissions === 'create_subthreads' || ourThreadPermissions === 'create_sidebars' || ourThreadPermissions === 'join_thread' || ourThreadPermissions === 'edit_permissions' || ourThreadPermissions === 'add_members' || ourThreadPermissions === 'remove_members' || ourThreadPermissions === 'change_role' || ourThreadPermissions === 'leave_thread', 'string is not threadPermissions enum', ); return ourThreadPermissions; } export const threadPermissionPrefixes = Object.freeze({ DESCENDANT: 'descendant_', CHILD: 'child_', OPEN: 'open_', OPEN_DESCENDANT: 'descendant_open_', }); export type ThreadPermissionInfo = | {| value: true, source: string |} | {| value: false, source: null |}; export type ThreadPermissionsBlob = { [permission: string]: ThreadPermissionInfo, }; export type ThreadRolePermissionsBlob = { [permission: string]: boolean }; export type ThreadPermissionsInfo = { [permission: ThreadPermission]: ThreadPermissionInfo, }; export const threadPermissionsInfoPropType = PropTypes.objectOf( PropTypes.oneOfType([ PropTypes.shape({ value: PropTypes.oneOf([true]), source: PropTypes.string.isRequired, }), PropTypes.shape({ value: PropTypes.oneOf([false]), source: PropTypes.oneOf([null]), }), ]), ); export type MemberInfo = {| +id: string, +role: ?string, +permissions: ThreadPermissionsInfo, +isSender: boolean, |}; export const memberInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, role: PropTypes.string, permissions: threadPermissionsInfoPropType.isRequired, isSender: PropTypes.bool.isRequired, }); export type RelativeMemberInfo = {| ...MemberInfo, username: ?string, isViewer: boolean, |}; export const relativeMemberInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, role: PropTypes.string, permissions: threadPermissionsInfoPropType.isRequired, username: PropTypes.string, isViewer: PropTypes.bool.isRequired, isSender: PropTypes.bool.isRequired, }); export type RoleInfo = {| id: string, name: string, permissions: ThreadRolePermissionsBlob, isDefault: boolean, |}; export type ThreadCurrentUserInfo = {| role: ?string, permissions: ThreadPermissionsInfo, subscription: ThreadSubscription, unread: ?boolean, |}; export type RawThreadInfo = {| id: string, type: ThreadType, name: ?string, description: ?string, color: string, // hex, without "#" or "0x" creationTime: number, // millisecond timestamp parentThreadID: ?string, members: $ReadOnlyArray, roles: { [id: string]: RoleInfo }, currentUser: ThreadCurrentUserInfo, sourceMessageID?: string, repliesCount: number, |}; export type ThreadInfo = {| id: string, type: ThreadType, name: ?string, uiName: string, description: ?string, color: string, // hex, without "#" or "0x" creationTime: number, // millisecond timestamp parentThreadID: ?string, members: $ReadOnlyArray, roles: { [id: string]: RoleInfo }, currentUser: ThreadCurrentUserInfo, sourceMessageID?: string, repliesCount: number, |}; export const threadTypePropType = PropTypes.oneOf([ threadTypes.CHAT_NESTED_OPEN, threadTypes.CHAT_SECRET, threadTypes.SIDEBAR, threadTypes.PERSONAL, threadTypes.PRIVATE, ]); const rolePropType = PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, permissions: PropTypes.objectOf(PropTypes.bool).isRequired, isDefault: PropTypes.bool.isRequired, }); const currentUserPropType = PropTypes.shape({ role: PropTypes.string, permissions: threadPermissionsInfoPropType.isRequired, subscription: PropTypes.shape({ pushNotifs: PropTypes.bool.isRequired, home: PropTypes.bool.isRequired, }).isRequired, unread: PropTypes.bool, }); export const rawThreadInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, type: threadTypePropType.isRequired, name: PropTypes.string, description: PropTypes.string, color: PropTypes.string.isRequired, creationTime: PropTypes.number.isRequired, parentThreadID: PropTypes.string, members: PropTypes.arrayOf(memberInfoPropType).isRequired, roles: PropTypes.objectOf(rolePropType).isRequired, currentUser: currentUserPropType.isRequired, sourceMessageID: PropTypes.string, repliesCount: PropTypes.number.isRequired, }); export const threadInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, type: threadTypePropType.isRequired, name: PropTypes.string, uiName: PropTypes.string.isRequired, description: PropTypes.string, color: PropTypes.string.isRequired, creationTime: PropTypes.number.isRequired, parentThreadID: PropTypes.string, members: PropTypes.arrayOf(memberInfoPropType).isRequired, roles: PropTypes.objectOf(rolePropType).isRequired, currentUser: currentUserPropType.isRequired, sourceMessageID: PropTypes.string, repliesCount: PropTypes.number.isRequired, }); export type ServerMemberInfo = {| +id: string, +role: ?string, +permissions: ThreadPermissionsInfo, +subscription: ThreadSubscription, +unread: ?boolean, +isSender: boolean, |}; export type ServerThreadInfo = {| id: string, type: ThreadType, name: ?string, description: ?string, color: string, // hex, without "#" or "0x" creationTime: number, // millisecond timestamp parentThreadID: ?string, members: $ReadOnlyArray, roles: { [id: string]: RoleInfo }, sourceMessageID?: string, repliesCount: number, |}; export type ThreadStore = {| threadInfos: { [id: string]: RawThreadInfo }, inconsistencyReports: $ReadOnlyArray, |}; export type ThreadDeletionRequest = {| threadID: string, accountPassword: string, |}; export type RemoveMembersRequest = {| threadID: string, memberIDs: $ReadOnlyArray, |}; export type RoleChangeRequest = {| threadID: string, memberIDs: $ReadOnlyArray, role: string, |}; export type ChangeThreadSettingsResult = {| threadInfo?: RawThreadInfo, threadInfos?: { [id: string]: RawThreadInfo }, updatesResult: { newUpdates: $ReadOnlyArray, }, newMessageInfos: $ReadOnlyArray, |}; export type ChangeThreadSettingsPayload = {| threadID: string, updatesResult: { newUpdates: $ReadOnlyArray, }, newMessageInfos: $ReadOnlyArray, |}; export type LeaveThreadRequest = {| threadID: string, |}; export type LeaveThreadResult = {| threadInfos?: { [id: string]: RawThreadInfo }, updatesResult: { newUpdates: $ReadOnlyArray, }, |}; export type LeaveThreadPayload = {| updatesResult: { newUpdates: $ReadOnlyArray, }, |}; export type ThreadChanges = Shape<{| type: ThreadType, name: string, description: string, color: string, parentThreadID: string, newMemberIDs: $ReadOnlyArray, |}>; export type UpdateThreadRequest = {| threadID: string, changes: ThreadChanges, |}; export type BaseNewThreadRequest = {| +name?: ?string, +description?: ?string, +color?: ?string, +parentThreadID?: ?string, +initialMemberIDs?: ?$ReadOnlyArray, + +ghostMemberIDs?: ?$ReadOnlyArray, |}; export type NewThreadRequest = | {| +type: 3 | 4 | 6 | 7, ...BaseNewThreadRequest, |} | {| +type: 5, +sourceMessageID: string, ...BaseNewThreadRequest, |}; export type NewThreadResponse = {| +updatesResult: {| +newUpdates: $ReadOnlyArray, |}, +newMessageInfos: $ReadOnlyArray, +newThreadInfo?: RawThreadInfo, +userInfos: { [string]: AccountUserInfo }, +newThreadID?: string, |}; export type NewThreadResult = {| +updatesResult: {| +newUpdates: $ReadOnlyArray, |}, +newMessageInfos: $ReadOnlyArray, +userInfos: { [string]: AccountUserInfo }, +newThreadID: string, |}; export type ServerThreadJoinRequest = {| threadID: string, calendarQuery?: ?CalendarQuery, |}; export type ClientThreadJoinRequest = {| threadID: string, calendarQuery: CalendarQuery, |}; export type ThreadJoinResult = {| threadInfos?: { [id: string]: RawThreadInfo }, updatesResult: { newUpdates: $ReadOnlyArray, }, rawMessageInfos: $ReadOnlyArray, truncationStatuses: MessageTruncationStatuses, userInfos: { [string]: AccountUserInfo }, rawEntryInfos?: ?$ReadOnlyArray, |}; export type ThreadJoinPayload = {| updatesResult: { newUpdates: $ReadOnlyArray, }, rawMessageInfos: RawMessageInfo[], truncationStatuses: MessageTruncationStatuses, userInfos: $ReadOnlyArray, calendarResult: CalendarResult, |}; export type SidebarInfo = {| +threadInfo: ThreadInfo, +lastUpdatedTime: number, +mostRecentNonLocalMessage: ?string, |}; // We can show a max of 3 sidebars inline underneath their parent in the chat // tab. If there are more, we show a button that opens a modal to see the rest export const maxReadSidebars = 3; // We can show a max of 5 sidebars inline underneath their parent // in the chat tab if every one of the displayed sidebars is unread export const maxUnreadSidebars = 5; diff --git a/server/src/creators/thread-creator.js b/server/src/creators/thread-creator.js index 9c1dd3a0b..252a10f77 100644 --- a/server/src/creators/thread-creator.js +++ b/server/src/creators/thread-creator.js @@ -1,428 +1,459 @@ // @flow import invariant from 'invariant'; import { relationshipBlockedInEitherDirection } from 'lib/shared/relationship-utils'; import { generatePendingThreadColor, generateRandomColor, } from 'lib/shared/thread-utils'; import { hasMinCodeVersion } from 'lib/shared/version-utils'; import type { Shape } from 'lib/types/core'; import { messageTypes } from 'lib/types/message-types'; import { userRelationshipStatus } from 'lib/types/relationship-types'; import { type NewThreadRequest, type NewThreadResponse, threadTypes, threadPermissions, } from 'lib/types/thread-types'; import { ServerError } from 'lib/utils/errors'; import { promiseAll } from 'lib/utils/promises'; import { dbQuery, SQL } from '../database/database'; import { fetchMessageInfoByID } from '../fetchers/message-fetchers'; import { fetchThreadInfos } from '../fetchers/thread-fetchers'; import { checkThreadPermission } from '../fetchers/thread-permission-fetchers'; import { fetchKnownUserInfos } from '../fetchers/user-fetchers'; import type { Viewer } from '../session/viewer'; import { changeRole, recalculateAllPermissions, commitMembershipChangeset, setJoinsToUnread, getRelationshipRowsForUsers, getParentThreadRelationshipRowsForNewUsers, } from '../updaters/thread-permission-updaters'; import createIDs from './id-creator'; import createMessages from './message-creator'; import { createInitialRolesForNewThread } from './role-creator'; import type { UpdatesForCurrentSession } from './update-creator'; type CreateThreadOptions = Shape<{| +forceAddMembers: boolean, +updatesForCurrentSession: UpdatesForCurrentSession, |}>; // If forceAddMembers is set, we will allow the viewer to add random users who // they aren't friends with. We will only fail if the viewer is trying to add // somebody who they have blocked or has blocked them. On the other hand, if // forceAddMembers is not set, we will fail if the viewer tries to add somebody // who they aren't friends with and doesn't have a membership row with a // nonnegative role for the parent thread. async function createThread( viewer: Viewer, request: NewThreadRequest, options?: CreateThreadOptions, ): Promise { if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const forceAddMembers = options?.forceAddMembers ?? false; const updatesForCurrentSession = options?.updatesForCurrentSession ?? 'return'; const threadType = request.type; const shouldCreateRelationships = forceAddMembers || threadType === threadTypes.PERSONAL; const parentThreadID = request.parentThreadID ? request.parentThreadID : null; const initialMemberIDs = request.initialMemberIDs && request.initialMemberIDs.length > 0 ? request.initialMemberIDs : null; + const ghostMemberIDs = + request.ghostMemberIDs && request.ghostMemberIDs.length > 0 + ? request.ghostMemberIDs + : null; if ( threadType !== threadTypes.CHAT_SECRET && threadType !== threadTypes.PERSONAL && threadType !== threadTypes.PRIVATE && !parentThreadID ) { throw new ServerError('invalid_parameters'); } if ( threadType === threadTypes.PERSONAL && (request.initialMemberIDs?.length !== 1 || parentThreadID) ) { throw new ServerError('invalid_parameters'); } const checkPromises = {}; if (parentThreadID) { checkPromises.parentThreadFetch = fetchThreadInfos( viewer, SQL`t.id = ${parentThreadID}`, ); checkPromises.hasParentPermission = checkThreadPermission( viewer, parentThreadID, threadType === threadTypes.SIDEBAR ? threadPermissions.CREATE_SIDEBARS : threadPermissions.CREATE_SUBTHREADS, ); } + + const memberIDs = []; if (initialMemberIDs) { - checkPromises.fetchInitialMembers = fetchKnownUserInfos( - viewer, - initialMemberIDs, - ); + memberIDs.push(...initialMemberIDs); + } + if (ghostMemberIDs) { + memberIDs.push(...ghostMemberIDs); } + + if (initialMemberIDs || ghostMemberIDs) { + checkPromises.fetchMemberIDs = fetchKnownUserInfos(viewer, memberIDs); + } + const { parentThreadFetch, hasParentPermission, - fetchInitialMembers, + fetchMemberIDs, } = await promiseAll(checkPromises); let parentThreadMembers; if (parentThreadID) { invariant(parentThreadFetch, 'parentThreadFetch should be set'); const parentThreadInfo = parentThreadFetch.threadInfos[parentThreadID]; if (!hasParentPermission) { throw new ServerError('invalid_credentials'); } parentThreadMembers = parentThreadInfo.members.map( (userInfo) => userInfo.id, ); } const viewerNeedsRelationshipsWith = []; - if (fetchInitialMembers) { - invariant(initialMemberIDs, 'should be set'); - for (const initialMemberID of initialMemberIDs) { - const initialMember = fetchInitialMembers[initialMemberID]; + if (fetchMemberIDs) { + invariant(initialMemberIDs || ghostMemberIDs, 'should be set'); + for (const memberID of memberIDs) { + const member = fetchMemberIDs[memberID]; if ( - !initialMember && + !member && shouldCreateRelationships && (threadType !== threadTypes.SIDEBAR || - parentThreadMembers?.includes(initialMemberID)) + parentThreadMembers?.includes(memberID)) ) { - viewerNeedsRelationshipsWith.push(initialMemberID); + viewerNeedsRelationshipsWith.push(memberID); continue; - } else if (!initialMember) { + } else if (!member) { throw new ServerError('invalid_credentials'); } - const { relationshipStatus } = initialMember; + const { relationshipStatus } = member; if ( relationshipStatus === userRelationshipStatus.FRIEND && threadType !== threadTypes.SIDEBAR ) { continue; } else if ( relationshipStatus && relationshipBlockedInEitherDirection(relationshipStatus) ) { throw new ServerError('invalid_credentials'); } else if ( parentThreadMembers && - parentThreadMembers.includes(initialMemberID) + parentThreadMembers.includes(memberID) ) { continue; } else if (!shouldCreateRelationships) { throw new ServerError('invalid_credentials'); } } } const [id] = await createIDs('threads', 1); const newRoles = await createInitialRolesForNewThread(id, threadType); const name = request.name ? request.name : null; const description = request.description ? request.description : null; let color = request.color ? request.color.toLowerCase() : generateRandomColor(); if (threadType === threadTypes.PERSONAL) { color = generatePendingThreadColor( request.initialMemberIDs ?? [], viewer.id, ); } const sourceMessageID = request.sourceMessageID ? request.sourceMessageID : null; invariant( threadType !== threadTypes.SIDEBAR || sourceMessageID, 'sourceMessageID should be set for sidebar', ); const time = Date.now(); const row = [ id, threadType, name, description, viewer.userID, time, color, parentThreadID, newRoles.default.id, sourceMessageID, ]; if (threadType === threadTypes.PERSONAL) { const otherMemberID = initialMemberIDs?.[0]; invariant( otherMemberID, 'Other member id should be set for a PERSONAL thread', ); const query = SQL` INSERT INTO threads(id, type, name, description, creator, creation_time, color, parent_thread_id, default_role, source_message) SELECT ${row} WHERE NOT EXISTS ( SELECT * FROM threads t INNER JOIN memberships m1 ON m1.thread = t.id AND m1.user = ${viewer.userID} INNER JOIN memberships m2 ON m2.thread = t.id AND m2.user = ${otherMemberID} WHERE t.type = ${threadTypes.PERSONAL} AND m1.role != -1 AND m2.role != -1 ) `; const [result] = await dbQuery(query); if (result.affectedRows === 0) { const personalThreadQuery = SQL` SELECT t.id FROM threads t INNER JOIN memberships m1 ON m1.thread = t.id AND m1.user = ${viewer.userID} INNER JOIN memberships m2 ON m2.thread = t.id AND m2.user = ${otherMemberID} WHERE t.type = ${threadTypes.PERSONAL} AND m1.role != -1 AND m2.role != -1 `; const deleteRoles = SQL` DELETE FROM roles WHERE id IN (${newRoles.default.id}, ${newRoles.creator.id}) `; const deleteIDs = SQL` DELETE FROM ids WHERE id IN (${id}, ${newRoles.default.id}, ${newRoles.creator.id}) `; const [[personalThreadResult]] = await Promise.all([ dbQuery(personalThreadQuery), dbQuery(deleteRoles), dbQuery(deleteIDs), ]); invariant( personalThreadResult.length > 0, 'PERSONAL thread should exist', ); const personalThreadID = personalThreadResult[0].id.toString(); return { newThreadID: personalThreadID, updatesResult: { newUpdates: [], }, userInfos: {}, newMessageInfos: [], }; } } else { const query = SQL` INSERT INTO threads(id, type, name, description, creator, creation_time, color, parent_thread_id, default_role, source_message) VALUES ${[row]} `; await dbQuery(query); } const [ creatorChangeset, initialMembersChangeset, + ghostMembersChangeset, recalculatePermissionsChangeset, ] = await Promise.all([ changeRole(id, [viewer.userID], newRoles.creator.id), initialMemberIDs ? changeRole(id, initialMemberIDs, null) : undefined, + ghostMemberIDs ? changeRole(id, ghostMemberIDs, -1) : undefined, recalculateAllPermissions(id, threadType), ]); if (!creatorChangeset) { throw new ServerError('unknown_error'); } const { membershipRows: creatorMembershipRows, relationshipRows: creatorRelationshipRows, } = creatorChangeset; - const initialMemberAndCreatorIDs = initialMemberIDs - ? [...initialMemberIDs, viewer.userID] - : [viewer.userID]; const { membershipRows: recalculateMembershipRows, relationshipRows: recalculateRelationshipRows, } = recalculatePermissionsChangeset; const membershipRows = [ ...creatorMembershipRows, ...recalculateMembershipRows, ]; const relationshipRows = [ ...creatorRelationshipRows, ...recalculateRelationshipRows, ]; - if (initialMemberIDs) { - if (!initialMembersChangeset) { + if (initialMemberIDs || ghostMemberIDs) { + if (!initialMembersChangeset && !ghostMembersChangeset) { throw new ServerError('unknown_error'); } relationshipRows.push( ...getRelationshipRowsForUsers( viewer.userID, viewerNeedsRelationshipsWith, ), ); - const { - membershipRows: initialMembersMembershipRows, - relationshipRows: initialMembersRelationshipRows, - } = initialMembersChangeset; + const membersMembershipRows = []; + const membersRelationshipRows = []; + if (initialMembersChangeset) { + const { + membershipRows: initialMembersMembershipRows, + relationshipRows: initialMembersRelationshipRows, + } = initialMembersChangeset; + membersMembershipRows.push(...initialMembersMembershipRows); + membersRelationshipRows.push(...initialMembersRelationshipRows); + } + + if (ghostMembersChangeset) { + const { + membershipRows: ghostMembersMembershipRows, + relationshipRows: ghostMembersRelationshipRows, + } = ghostMembersChangeset; + membersMembershipRows.push(...ghostMembersMembershipRows); + membersRelationshipRows.push(...ghostMembersRelationshipRows); + } + + const memberAndCreatorIDs = [...memberIDs, viewer.userID]; const parentRelationshipRows = getParentThreadRelationshipRowsForNewUsers( id, recalculateMembershipRows, - initialMemberAndCreatorIDs, + memberAndCreatorIDs, ); - membershipRows.push(...initialMembersMembershipRows); + membershipRows.push(...membersMembershipRows); relationshipRows.push( - ...initialMembersRelationshipRows, + ...membersRelationshipRows, ...parentRelationshipRows, ); } + setJoinsToUnread(membershipRows, viewer.userID, id); const changeset = { membershipRows, relationshipRows }; const { threadInfos, viewerUpdates, userInfos, } = await commitMembershipChangeset(viewer, changeset, { updatesForCurrentSession, }); + const initialMemberAndCreatorIDs = initialMemberIDs + ? [...initialMemberIDs, viewer.userID] + : [viewer.userID]; const messageDatas = []; if (threadType !== threadTypes.SIDEBAR) { messageDatas.push({ type: messageTypes.CREATE_THREAD, threadID: id, creatorID: viewer.userID, time, initialThreadState: { type: threadType, name, parentThreadID, color, memberIDs: initialMemberAndCreatorIDs, }, }); } else { invariant(parentThreadID, 'parentThreadID should be set for sidebar'); invariant(sourceMessageID, 'sourceMessageID should be set for sidebar'); const sourceMessage = await fetchMessageInfoByID(viewer, sourceMessageID); if (!sourceMessage || sourceMessage.type === messageTypes.SIDEBAR_SOURCE) { throw new ServerError('invalid_parameters'); } messageDatas.push( { type: messageTypes.CREATE_SIDEBAR, threadID: id, creatorID: viewer.userID, time, sourceMessageAuthorID: sourceMessage.creatorID, initialThreadState: { name, parentThreadID, color, memberIDs: initialMemberAndCreatorIDs, }, }, { type: messageTypes.SIDEBAR_SOURCE, threadID: id, creatorID: viewer.userID, time, sourceMessage, }, ); } if (parentThreadID && threadType !== threadTypes.SIDEBAR) { messageDatas.push({ type: messageTypes.CREATE_SUB_THREAD, threadID: parentThreadID, creatorID: viewer.userID, time, childThreadID: id, }); } const newMessageInfos = await createMessages( viewer, messageDatas, updatesForCurrentSession, ); if (hasMinCodeVersion(viewer.platformDetails, 62)) { return { newThreadID: id, updatesResult: { newUpdates: viewerUpdates, }, userInfos, newMessageInfos, }; } return { newThreadInfo: threadInfos[id], updatesResult: { newUpdates: viewerUpdates, }, userInfos, newMessageInfos, }; } export default createThread;