diff --git a/lib/selectors/user-selectors.js b/lib/selectors/user-selectors.js index 780ac4ccf..009fe0449 100644 --- a/lib/selectors/user-selectors.js +++ b/lib/selectors/user-selectors.js @@ -1,213 +1,213 @@ // @flow import _memoize from 'lodash/memoize'; import { createSelector } from 'reselect'; import SearchIndex from '../shared/search-index'; import { getSingleOtherUser, memberHasAdminPowers, } from '../shared/thread-utils'; import type { BaseAppState } from '../types/redux-types'; import { userRelationshipStatus } from '../types/relationship-types'; import { type RawThreadInfo, type RelativeMemberInfo, threadTypes, } from '../types/thread-types'; import type { UserInfos, RelativeUserInfo, AccountUserInfo, } from '../types/user-types'; // Used for specific message payloads that include an array of user IDs, ie. // array of initial users, array of added users function userIDsToRelativeUserInfos( - userIDs: string[], + userIDs: $ReadOnlyArray, viewerID: ?string, userInfos: UserInfos, ): RelativeUserInfo[] { const relativeUserInfos = []; for (let userID of userIDs) { const username = userInfos[userID] ? userInfos[userID].username : null; if (userID === viewerID) { relativeUserInfos.unshift({ id: userID, username, isViewer: true, }); } else { relativeUserInfos.push({ id: userID, username, isViewer: false, }); } } return relativeUserInfos; } const emptyArray = []; // Includes current user at the start const baseRelativeMemberInfoSelectorForMembersOfThread = ( threadID: ?string, ) => { if (!threadID) { return () => emptyArray; } return createSelector( (state: BaseAppState<*>) => state.threadStore.threadInfos[threadID], (state: BaseAppState<*>) => state.currentUserInfo && state.currentUserInfo.id, (state: BaseAppState<*>) => state.userStore.userInfos, ( threadInfo: ?RawThreadInfo, currentUserID: ?string, userInfos: UserInfos, ): $ReadOnlyArray => { const relativeMemberInfos = []; if (!threadInfo) { return relativeMemberInfos; } const memberInfos = threadInfo.members; for (const memberInfo of memberInfos) { const isParentAdmin = memberHasAdminPowers(memberInfo); if (!memberInfo.role && !isParentAdmin) { continue; } const username = userInfos[memberInfo.id] ? userInfos[memberInfo.id].username : null; if (memberInfo.id === currentUserID) { relativeMemberInfos.unshift({ id: memberInfo.id, role: memberInfo.role, permissions: memberInfo.permissions, username, isViewer: true, }); } else { relativeMemberInfos.push({ id: memberInfo.id, role: memberInfo.role, permissions: memberInfo.permissions, username, isViewer: false, }); } } return relativeMemberInfos; }, ); }; const relativeMemberInfoSelectorForMembersOfThread: ( threadID: ?string, ) => (state: BaseAppState<*>) => $ReadOnlyArray = _memoize( baseRelativeMemberInfoSelectorForMembersOfThread, ); const userInfoSelectorForPotentialMembers: ( state: BaseAppState<*>, ) => { [id: string]: AccountUserInfo } = createSelector( (state: BaseAppState<*>) => state.userStore.userInfos, (state: BaseAppState<*>) => state.currentUserInfo && state.currentUserInfo.id, ( userInfos: UserInfos, currentUserID: ?string, ): { [id: string]: AccountUserInfo } => { const availableUsers: { [id: string]: AccountUserInfo } = {}; for (const id in userInfos) { const { username, relationshipStatus } = userInfos[id]; if (id === currentUserID || !username) { continue; } if ( relationshipStatus !== userRelationshipStatus.BLOCKED_VIEWER && relationshipStatus !== userRelationshipStatus.BOTH_BLOCKED ) { availableUsers[id] = { id, username, relationshipStatus }; } } return availableUsers; }, ); function searchIndexFromUserInfos(userInfos: { [id: string]: AccountUserInfo, }) { const searchIndex = new SearchIndex(); for (const id in userInfos) { searchIndex.addEntry(id, userInfos[id].username); } return searchIndex; } const userSearchIndexForPotentialMembers: ( state: BaseAppState<*>, ) => SearchIndex = createSelector( userInfoSelectorForPotentialMembers, searchIndexFromUserInfos, ); const isLoggedIn = (state: BaseAppState<*>) => !!( state.currentUserInfo && !state.currentUserInfo.anonymous && state.dataLoaded ); const userStoreSearchIndex: ( state: BaseAppState<*>, ) => SearchIndex = createSelector( (state: BaseAppState<*>) => state.userStore.userInfos, (userInfos: UserInfos) => { const searchIndex = new SearchIndex(); for (const id in userInfos) { const { username } = userInfos[id]; if (!username) { continue; } searchIndex.addEntry(id, username); } return searchIndex; }, ); const usersWithPersonalThreadSelector: ( state: BaseAppState<*>, ) => $ReadOnlySet = createSelector( (state) => state.currentUserInfo && state.currentUserInfo.id, (state) => state.threadStore.threadInfos, (viewerID, threadInfos) => { const personalThreadMembers = new Set(); for (const threadID in threadInfos) { const thread = threadInfos[threadID]; if ( thread.type !== threadTypes.PERSONAL || !thread.members.find((member) => member.id === viewerID) ) { continue; } const otherMemberID = getSingleOtherUser(thread, viewerID); if (otherMemberID) { personalThreadMembers.add(otherMemberID); } } return personalThreadMembers; }, ); export { userIDsToRelativeUserInfos, relativeMemberInfoSelectorForMembersOfThread, userInfoSelectorForPotentialMembers, userSearchIndexForPotentialMembers, isLoggedIn, userStoreSearchIndex, usersWithPersonalThreadSelector, }; diff --git a/lib/shared/message-utils.js b/lib/shared/message-utils.js index 5f9765532..aee2b9de8 100644 --- a/lib/shared/message-utils.js +++ b/lib/shared/message-utils.js @@ -1,1112 +1,771 @@ // @flow import invariant from 'invariant'; import _maxBy from 'lodash/fp/maxBy'; import { shimUploadURI, multimediaMessagePreview } from '../media/media-utils'; import { userIDsToRelativeUserInfos } from '../selectors/user-selectors'; import type { PlatformDetails } from '../types/device-types'; import type { Media, Image, Video } from '../types/media-types'; import { type MessageInfo, type RawMessageInfo, type RobotextMessageInfo, type PreviewableMessageInfo, type RawMultimediaMessageInfo, type MessageData, type MessageType, type MessageTruncationStatus, type MultimediaMessageData, type MessageStore, messageTypes, messageTruncationStatus, } from '../types/message-types'; import type { CreateSidebarMessageInfo } from '../types/message/create-sidebar'; import type { CreateThreadMessageInfo } from '../types/message/create-thread'; import type { ImagesMessageData, - ImagesMessageInfo, RawImagesMessageInfo, } from '../types/message/images'; import type { MediaMessageData, - MediaMessageInfo, RawMediaMessageInfo, } from '../types/message/media'; -import type { TextMessageInfo } from '../types/message/text'; import { type ThreadInfo, threadTypes } from '../types/thread-types'; import type { RelativeUserInfo, UserInfos } from '../types/user-types'; import { prettyDate } from '../utils/date-utils'; import { codeBlockRegex } from './markdown'; +import { messageSpecs } from './messages/message-specs'; import { stringForUser } from './user-utils'; import { hasMinCodeVersion } from './version-utils'; // Prefers localID function messageKey(messageInfo: MessageInfo | RawMessageInfo): string { if (messageInfo.localID) { return messageInfo.localID; } invariant(messageInfo.id, 'localID should exist if ID does not'); return messageInfo.id; } // Prefers serverID function messageID(messageInfo: MessageInfo | RawMessageInfo): string { if (messageInfo.id) { return messageInfo.id; } invariant(messageInfo.localID, 'localID should exist if ID does not'); return messageInfo.localID; } function robotextForUser(user: RelativeUserInfo): string { if (user.isViewer) { return 'you'; } else if (user.username) { return `<${encodeURI(user.username)}|u${user.id}>`; } else { return 'anonymous'; } } function robotextForUsers(users: RelativeUserInfo[]): string { if (users.length === 1) { return robotextForUser(users[0]); } else if (users.length === 2) { return `${robotextForUser(users[0])} and ${robotextForUser(users[1])}`; } else if (users.length === 3) { return ( `${robotextForUser(users[0])}, ${robotextForUser(users[1])}, ` + `and ${robotextForUser(users[2])}` ); } else { return ( `${robotextForUser(users[0])}, ${robotextForUser(users[1])}, ` + `and ${users.length - 2} others` ); } } function encodedThreadEntity(threadID: string, text: string): string { return `<${text}|t${threadID}>`; } function newThreadRobotext( messageInfo: CreateThreadMessageInfo, creator: string, ) { let text = `created ${encodedThreadEntity( messageInfo.threadID, `this thread`, )}`; const parentThread = messageInfo.initialThreadState.parentThreadInfo; if (parentThread) { text += ' as a child of ' + `<${encodeURI(parentThread.uiName)}|t${parentThread.id}>`; } if (messageInfo.initialThreadState.name) { text += ` with the name "${encodeURI( messageInfo.initialThreadState.name, )}"`; } const users = messageInfo.initialThreadState.otherMembers; if (users.length !== 0) { const initialUsersString = robotextForUsers(users); text += ` and added ${initialUsersString}`; } return `${creator} ${text}`; } function newSidebarRobotext( messageInfo: CreateSidebarMessageInfo, creator: string, ) { let text = `started ${encodedThreadEntity( messageInfo.threadID, `this sidebar`, )}`; const users = messageInfo.initialThreadState.otherMembers.filter( (member) => member.id !== messageInfo.sourceMessageAuthor.id, ); if (users.length !== 0) { const initialUsersString = robotextForUsers(users); text += ` and added ${initialUsersString}`; } return `${creator} ${text}`; } function robotextForMessageInfo( messageInfo: RobotextMessageInfo, threadInfo: ThreadInfo, ): string { const creator = robotextForUser(messageInfo.creator); if (messageInfo.type === messageTypes.CREATE_THREAD) { return newThreadRobotext(messageInfo, creator); } else if (messageInfo.type === messageTypes.ADD_MEMBERS) { const users = messageInfo.addedMembers; invariant(users.length !== 0, 'added who??'); const addedUsersString = robotextForUsers(users); return `${creator} added ${addedUsersString}`; } else if (messageInfo.type === messageTypes.CREATE_SUB_THREAD) { const childName = messageInfo.childThreadInfo.name; const childNoun = messageInfo.childThreadInfo.type === threadTypes.SIDEBAR ? 'sidebar' : 'child thread'; if (childName) { return ( `${creator} created a ${childNoun}` + ` named "<${encodeURI(childName)}|t${messageInfo.childThreadInfo.id}>"` ); } else { return ( `${creator} created a ` + `<${childNoun}|t${messageInfo.childThreadInfo.id}>` ); } } else if (messageInfo.type === messageTypes.CHANGE_SETTINGS) { let value; if (messageInfo.field === 'color') { value = `<#${messageInfo.value}|c${messageInfo.threadID}>`; } else { value = messageInfo.value; } return ( `${creator} updated ` + `${encodedThreadEntity(messageInfo.threadID, 'the thread')}'s ` + `${messageInfo.field} to "${value}"` ); } else if (messageInfo.type === messageTypes.REMOVE_MEMBERS) { const users = messageInfo.removedMembers; invariant(users.length !== 0, 'removed who??'); const removedUsersString = robotextForUsers(users); return `${creator} removed ${removedUsersString}`; } else if (messageInfo.type === messageTypes.CHANGE_ROLE) { const users = messageInfo.members; invariant(users.length !== 0, 'changed whose role??'); const usersString = robotextForUsers(users); const verb = threadInfo.roles[messageInfo.newRole].isDefault ? 'removed' : 'added'; const noun = users.length === 1 ? 'an admin' : 'admins'; return `${creator} ${verb} ${usersString} as ${noun}`; } else if (messageInfo.type === messageTypes.LEAVE_THREAD) { return ( `${creator} left ` + encodedThreadEntity(messageInfo.threadID, 'this thread') ); } else if (messageInfo.type === messageTypes.JOIN_THREAD) { return ( `${creator} joined ` + encodedThreadEntity(messageInfo.threadID, 'this thread') ); } else if (messageInfo.type === messageTypes.CREATE_ENTRY) { const date = prettyDate(messageInfo.date); return ( `${creator} created an event scheduled for ${date}: ` + `"${messageInfo.text}"` ); } else if (messageInfo.type === messageTypes.EDIT_ENTRY) { const date = prettyDate(messageInfo.date); return ( `${creator} updated the text of an event scheduled for ` + `${date}: "${messageInfo.text}"` ); } else if (messageInfo.type === messageTypes.DELETE_ENTRY) { const date = prettyDate(messageInfo.date); return ( `${creator} deleted an event scheduled for ${date}: ` + `"${messageInfo.text}"` ); } else if (messageInfo.type === messageTypes.RESTORE_ENTRY) { const date = prettyDate(messageInfo.date); return ( `${creator} restored an event scheduled for ${date}: ` + `"${messageInfo.text}"` ); } else if (messageInfo.type === messageTypes.UPDATE_RELATIONSHIP) { const target = robotextForUser(messageInfo.target); if (messageInfo.operation === 'request_sent') { return `${creator} sent ${target} a friend request`; } else if (messageInfo.operation === 'request_accepted') { const targetPossessive = messageInfo.target.isViewer ? 'your' : `${target}'s`; return `${creator} accepted ${targetPossessive} friend request`; } invariant( false, `Invalid operation ${messageInfo.operation} ` + `of message with type ${messageInfo.type}`, ); } else if (messageInfo.type === messageTypes.CREATE_SIDEBAR) { return newSidebarRobotext(messageInfo, creator); } else if (messageInfo.type === messageTypes.UNSUPPORTED) { return `${creator} ${messageInfo.robotext}`; } invariant(false, `we're not aware of messageType ${messageInfo.type}`); } function robotextToRawString(robotext: string): string { return decodeURI(robotext.replace(/<([^<>|]+)\|[^<>|]+>/g, '$1')); } function createMessageInfo( rawMessageInfo: RawMessageInfo, viewerID: ?string, userInfos: UserInfos, threadInfos: { [id: string]: ThreadInfo }, ): ?MessageInfo { const creatorInfo = userInfos[rawMessageInfo.creatorID]; if (!creatorInfo) { return null; } - if (rawMessageInfo.type === messageTypes.TEXT) { - const messageInfo: TextMessageInfo = { - type: messageTypes.TEXT, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - text: rawMessageInfo.text, - }; - if (rawMessageInfo.id) { - messageInfo.id = rawMessageInfo.id; - } - if (rawMessageInfo.localID) { - messageInfo.localID = rawMessageInfo.localID; - } - return messageInfo; - } else if (rawMessageInfo.type === messageTypes.CREATE_THREAD) { - const initialParentThreadID = - rawMessageInfo.initialThreadState.parentThreadID; - let parentThreadInfo = null; - if (initialParentThreadID) { - parentThreadInfo = threadInfos[initialParentThreadID]; - } - return { - type: messageTypes.CREATE_THREAD, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - initialThreadState: { - name: rawMessageInfo.initialThreadState.name, - parentThreadInfo, - type: rawMessageInfo.initialThreadState.type, - color: rawMessageInfo.initialThreadState.color, - otherMembers: userIDsToRelativeUserInfos( - rawMessageInfo.initialThreadState.memberIDs.filter( - (userID: string) => userID !== rawMessageInfo.creatorID, - ), - viewerID, - userInfos, - ), - }, - }; - } else if (rawMessageInfo.type === messageTypes.ADD_MEMBERS) { - const addedMembers = userIDsToRelativeUserInfos( - rawMessageInfo.addedUserIDs, - viewerID, - userInfos, - ); - return { - type: messageTypes.ADD_MEMBERS, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - addedMembers, - }; - } else if (rawMessageInfo.type === messageTypes.CREATE_SUB_THREAD) { - const childThreadInfo = threadInfos[rawMessageInfo.childThreadID]; - if (!childThreadInfo) { - return null; - } - return { - type: messageTypes.CREATE_SUB_THREAD, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - childThreadInfo, - }; - } else if (rawMessageInfo.type === messageTypes.CHANGE_SETTINGS) { - return { - type: messageTypes.CHANGE_SETTINGS, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - field: rawMessageInfo.field, - value: rawMessageInfo.value, - }; - } else if (rawMessageInfo.type === messageTypes.REMOVE_MEMBERS) { - const removedMembers = userIDsToRelativeUserInfos( - rawMessageInfo.removedUserIDs, - viewerID, - userInfos, - ); - return { - type: messageTypes.REMOVE_MEMBERS, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - removedMembers, - }; - } else if (rawMessageInfo.type === messageTypes.CHANGE_ROLE) { - const members = userIDsToRelativeUserInfos( - rawMessageInfo.userIDs, - viewerID, - userInfos, - ); - return { - type: messageTypes.CHANGE_ROLE, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - members, - newRole: rawMessageInfo.newRole, - }; - } else if (rawMessageInfo.type === messageTypes.LEAVE_THREAD) { - return { - type: messageTypes.LEAVE_THREAD, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - }; - } else if (rawMessageInfo.type === messageTypes.JOIN_THREAD) { - return { - type: messageTypes.JOIN_THREAD, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - }; - } else if (rawMessageInfo.type === messageTypes.CREATE_ENTRY) { - return { - type: messageTypes.CREATE_ENTRY, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - entryID: rawMessageInfo.entryID, - date: rawMessageInfo.date, - text: rawMessageInfo.text, - }; - } else if (rawMessageInfo.type === messageTypes.EDIT_ENTRY) { - return { - type: messageTypes.EDIT_ENTRY, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - entryID: rawMessageInfo.entryID, - date: rawMessageInfo.date, - text: rawMessageInfo.text, - }; - } else if (rawMessageInfo.type === messageTypes.DELETE_ENTRY) { - return { - type: messageTypes.DELETE_ENTRY, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - entryID: rawMessageInfo.entryID, - date: rawMessageInfo.date, - text: rawMessageInfo.text, - }; - } else if (rawMessageInfo.type === messageTypes.RESTORE_ENTRY) { - return { - type: messageTypes.RESTORE_ENTRY, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - entryID: rawMessageInfo.entryID, - date: rawMessageInfo.date, - text: rawMessageInfo.text, - }; - } else if (rawMessageInfo.type === messageTypes.UNSUPPORTED) { - return { - type: messageTypes.UNSUPPORTED, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - robotext: rawMessageInfo.robotext, - unsupportedMessageInfo: rawMessageInfo.unsupportedMessageInfo, - }; - } else if (rawMessageInfo.type === messageTypes.IMAGES) { - const messageInfo: ImagesMessageInfo = { - type: messageTypes.IMAGES, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - media: rawMessageInfo.media, - }; - if (rawMessageInfo.id) { - messageInfo.id = rawMessageInfo.id; - } - if (rawMessageInfo.localID) { - messageInfo.localID = rawMessageInfo.localID; - } - return messageInfo; - } else if (rawMessageInfo.type === messageTypes.MULTIMEDIA) { - const messageInfo: MediaMessageInfo = { - type: messageTypes.MULTIMEDIA, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - media: rawMessageInfo.media, - }; - if (rawMessageInfo.id) { - messageInfo.id = rawMessageInfo.id; - } - if (rawMessageInfo.localID) { - messageInfo.localID = rawMessageInfo.localID; - } - return messageInfo; - } else if (rawMessageInfo.type === messageTypes.UPDATE_RELATIONSHIP) { - const target = userInfos[rawMessageInfo.targetID]; - if (!target) { - return null; - } - return { - type: messageTypes.UPDATE_RELATIONSHIP, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - target: { - id: target.id, - username: target.username, - isViewer: target.id === viewerID, - }, - time: rawMessageInfo.time, - operation: rawMessageInfo.operation, - }; - } else if (rawMessageInfo.type === messageTypes.SIDEBAR_SOURCE) { - if (!rawMessageInfo.sourceMessage) { - return null; - } - const sourceMessage = createMessageInfo( - rawMessageInfo.sourceMessage, - viewerID, - userInfos, - threadInfos, - ); - invariant( - sourceMessage && sourceMessage.type !== messageTypes.SIDEBAR_SOURCE, - 'Sidebars can not be created from SIDEBAR SOURCE', - ); - return { - type: messageTypes.SIDEBAR_SOURCE, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - sourceMessage, - }; - } else if (rawMessageInfo.type === messageTypes.CREATE_SIDEBAR) { - const parentThreadInfo = - threadInfos[rawMessageInfo.initialThreadState.parentThreadID]; - const sourceMessageAuthor = userInfos[rawMessageInfo.sourceMessageAuthorID]; - if (!sourceMessageAuthor) { - return null; - } - return { - type: messageTypes.CREATE_SIDEBAR, - id: rawMessageInfo.id, - threadID: rawMessageInfo.threadID, - creator: { - id: rawMessageInfo.creatorID, - username: creatorInfo.username, - isViewer: rawMessageInfo.creatorID === viewerID, - }, - time: rawMessageInfo.time, - sourceMessageAuthor: { - id: rawMessageInfo.sourceMessageAuthorID, - username: sourceMessageAuthor.username, - isViewer: rawMessageInfo.sourceMessageAuthorID === viewerID, - }, - initialThreadState: { - name: rawMessageInfo.initialThreadState.name, - parentThreadInfo, - color: rawMessageInfo.initialThreadState.color, - otherMembers: userIDsToRelativeUserInfos( - rawMessageInfo.initialThreadState.memberIDs.filter( - (userID: string) => userID !== rawMessageInfo.creatorID, - ), - viewerID, - userInfos, - ), - }, - }; - } - invariant(false, `we're not aware of messageType ${rawMessageInfo.type}`); + const creator = { + id: rawMessageInfo.creatorID, + username: creatorInfo.username, + isViewer: rawMessageInfo.creatorID === viewerID, + }; + + const createRelativeUserInfos = (userIDs: $ReadOnlyArray) => + userIDsToRelativeUserInfos(userIDs, viewerID, userInfos); + const createMessageInfoFromRaw = (rawInfo: RawMessageInfo) => + createMessageInfo(rawInfo, viewerID, userInfos, threadInfos); + + const messageSpec = messageSpecs[rawMessageInfo.type]; + + return messageSpec.createMessageInfo(rawMessageInfo, creator, { + threadInfos, + createMessageInfoFromRaw, + createRelativeUserInfos, + }); } function sortMessageInfoList( messageInfos: T[], ): T[] { return messageInfos.sort((a: T, b: T) => b.time - a.time); } function rawMessageInfoFromMessageData( messageData: MessageData, id: string, ): RawMessageInfo { if (messageData.type === messageTypes.TEXT) { return { ...messageData, id }; } else if (messageData.type === messageTypes.CREATE_THREAD) { return { ...messageData, id }; } else if (messageData.type === messageTypes.ADD_MEMBERS) { return { ...messageData, id }; } else if (messageData.type === messageTypes.CREATE_SUB_THREAD) { return { ...messageData, id }; } else if (messageData.type === messageTypes.CHANGE_SETTINGS) { return { ...messageData, id }; } else if (messageData.type === messageTypes.REMOVE_MEMBERS) { return { ...messageData, id }; } else if (messageData.type === messageTypes.CHANGE_ROLE) { return { ...messageData, id }; } else if (messageData.type === messageTypes.LEAVE_THREAD) { return { type: messageTypes.LEAVE_THREAD, id, threadID: messageData.threadID, creatorID: messageData.creatorID, time: messageData.time, }; } else if (messageData.type === messageTypes.JOIN_THREAD) { return { type: messageTypes.JOIN_THREAD, id, threadID: messageData.threadID, creatorID: messageData.creatorID, time: messageData.time, }; } else if (messageData.type === messageTypes.CREATE_ENTRY) { return { type: messageTypes.CREATE_ENTRY, id, threadID: messageData.threadID, creatorID: messageData.creatorID, time: messageData.time, entryID: messageData.entryID, date: messageData.date, text: messageData.text, }; } else if (messageData.type === messageTypes.EDIT_ENTRY) { return { type: messageTypes.EDIT_ENTRY, id, threadID: messageData.threadID, creatorID: messageData.creatorID, time: messageData.time, entryID: messageData.entryID, date: messageData.date, text: messageData.text, }; } else if (messageData.type === messageTypes.DELETE_ENTRY) { return { type: messageTypes.DELETE_ENTRY, id, threadID: messageData.threadID, creatorID: messageData.creatorID, time: messageData.time, entryID: messageData.entryID, date: messageData.date, text: messageData.text, }; } else if (messageData.type === messageTypes.RESTORE_ENTRY) { return { type: messageTypes.RESTORE_ENTRY, id, threadID: messageData.threadID, creatorID: messageData.creatorID, time: messageData.time, entryID: messageData.entryID, date: messageData.date, text: messageData.text, }; } else if (messageData.type === messageTypes.IMAGES) { return ({ ...messageData, id }: RawImagesMessageInfo); } else if (messageData.type === messageTypes.MULTIMEDIA) { return ({ ...messageData, id }: RawMediaMessageInfo); } else if (messageData.type === messageTypes.UPDATE_RELATIONSHIP) { return { ...messageData, id }; } else if (messageData.type === messageTypes.SIDEBAR_SOURCE) { return { ...messageData, id }; } else if (messageData.type === messageTypes.CREATE_SIDEBAR) { return { ...messageData, id }; } else { invariant(false, `we're not aware of messageType ${messageData.type}`); } } function mostRecentMessageTimestamp( messageInfos: RawMessageInfo[], previousTimestamp: number, ): number { if (messageInfos.length === 0) { return previousTimestamp; } return _maxBy('time')(messageInfos).time; } function messageTypeGeneratesNotifs(type: MessageType) { return ( type !== messageTypes.JOIN_THREAD && type !== messageTypes.LEAVE_THREAD && type !== messageTypes.ADD_MEMBERS && type !== messageTypes.REMOVE_MEMBERS && type !== messageTypes.SIDEBAR_SOURCE ); } function splitRobotext(robotext: string) { return robotext.split(/(<[^<>|]+\|[^<>|]+>)/g); } const robotextEntityRegex = /<([^<>|]+)\|([^<>|]+)>/; function parseRobotextEntity(robotextPart: string) { const entityParts = robotextPart.match(robotextEntityRegex); invariant(entityParts && entityParts[1], 'malformed robotext'); const rawText = decodeURI(entityParts[1]); const entityType = entityParts[2].charAt(0); const id = entityParts[2].substr(1); return { rawText, entityType, id }; } function usersInMessageInfos( messageInfos: $ReadOnlyArray, ): string[] { const userIDs = new Set(); for (let messageInfo of messageInfos) { if (messageInfo.creatorID) { userIDs.add(messageInfo.creatorID); } else if (messageInfo.creator) { userIDs.add(messageInfo.creator.id); } } return [...userIDs]; } function combineTruncationStatuses( first: MessageTruncationStatus, second: ?MessageTruncationStatus, ): MessageTruncationStatus { if ( first === messageTruncationStatus.EXHAUSTIVE || second === messageTruncationStatus.EXHAUSTIVE ) { return messageTruncationStatus.EXHAUSTIVE; } else if ( first === messageTruncationStatus.UNCHANGED && second !== null && second !== undefined ) { return second; } else { return first; } } function shimUnsupportedRawMessageInfos( rawMessageInfos: $ReadOnlyArray, platformDetails: ?PlatformDetails, ): RawMessageInfo[] { if (platformDetails && platformDetails.platform === 'web') { return [...rawMessageInfos]; } return rawMessageInfos.map((rawMessageInfo) => { if (rawMessageInfo.type === messageTypes.IMAGES) { const shimmedRawMessageInfo = shimMediaMessageInfo( rawMessageInfo, platformDetails, ); if (hasMinCodeVersion(platformDetails, 30)) { return shimmedRawMessageInfo; } const { id } = shimmedRawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: shimmedRawMessageInfo.threadID, creatorID: shimmedRawMessageInfo.creatorID, time: shimmedRawMessageInfo.time, robotext: multimediaMessagePreview(shimmedRawMessageInfo), unsupportedMessageInfo: shimmedRawMessageInfo, }; } else if (rawMessageInfo.type === messageTypes.MULTIMEDIA) { const shimmedRawMessageInfo = shimMediaMessageInfo( rawMessageInfo, platformDetails, ); // TODO figure out first native codeVersion supporting video playback if (hasMinCodeVersion(platformDetails, 62)) { return shimmedRawMessageInfo; } const { id } = shimmedRawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: shimmedRawMessageInfo.threadID, creatorID: shimmedRawMessageInfo.creatorID, time: shimmedRawMessageInfo.time, robotext: multimediaMessagePreview(shimmedRawMessageInfo), unsupportedMessageInfo: shimmedRawMessageInfo, }; } else if (rawMessageInfo.type === messageTypes.UPDATE_RELATIONSHIP) { if (hasMinCodeVersion(platformDetails, 71)) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'performed a relationship action', unsupportedMessageInfo: rawMessageInfo, }; } else if (rawMessageInfo.type === messageTypes.SIDEBAR_SOURCE) { // TODO determine min code version if ( hasMinCodeVersion(platformDetails, 75) && rawMessageInfo.sourceMessage ) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'first message in sidebar', unsupportedMessageInfo: rawMessageInfo, }; } else if (rawMessageInfo.type === messageTypes.CREATE_SIDEBAR) { // TODO determine min code version if (hasMinCodeVersion(platformDetails, 75)) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'created a sidebar', unsupportedMessageInfo: rawMessageInfo, }; } return rawMessageInfo; }); } function shimMediaMessageInfo( rawMessageInfo: RawMultimediaMessageInfo, platformDetails: ?PlatformDetails, ): RawMultimediaMessageInfo { if (rawMessageInfo.type === messageTypes.IMAGES) { let uriChanged = false; const newMedia: Image[] = []; for (let singleMedia of rawMessageInfo.media) { const shimmedURI = shimUploadURI(singleMedia.uri, platformDetails); if (shimmedURI === singleMedia.uri) { newMedia.push(singleMedia); } else { newMedia.push(({ ...singleMedia, uri: shimmedURI }: Image)); uriChanged = true; } } if (!uriChanged) { return rawMessageInfo; } return ({ ...rawMessageInfo, media: newMedia, }: RawImagesMessageInfo); } else { let uriChanged = false; const newMedia: Media[] = []; for (let singleMedia of rawMessageInfo.media) { const shimmedURI = shimUploadURI(singleMedia.uri, platformDetails); if (shimmedURI === singleMedia.uri) { newMedia.push(singleMedia); } else if (singleMedia.type === 'photo') { newMedia.push(({ ...singleMedia, uri: shimmedURI }: Image)); uriChanged = true; } else { newMedia.push(({ ...singleMedia, uri: shimmedURI }: Video)); uriChanged = true; } } if (!uriChanged) { return rawMessageInfo; } return ({ ...rawMessageInfo, media: newMedia, }: RawMediaMessageInfo); } } function messagePreviewText( messageInfo: PreviewableMessageInfo, threadInfo: ThreadInfo, ): string { if ( messageInfo.type === messageTypes.IMAGES || messageInfo.type === messageTypes.MULTIMEDIA ) { const creator = stringForUser(messageInfo.creator); const preview = multimediaMessagePreview(messageInfo); return `${creator} ${preview}`; } return robotextToRawString(robotextForMessageInfo(messageInfo, threadInfo)); } type MediaMessageDataCreationInput = $ReadOnly<{ threadID: string, creatorID: string, media: $ReadOnlyArray, localID?: ?string, time?: ?number, ... }>; function createMediaMessageData( input: MediaMessageDataCreationInput, ): MultimediaMessageData { let allMediaArePhotos = true; const photoMedia = []; for (let singleMedia of input.media) { if (singleMedia.type === 'video') { allMediaArePhotos = false; break; } else { photoMedia.push(singleMedia); } } const { localID, threadID, creatorID } = input; const time = input.time ? input.time : Date.now(); let messageData; if (allMediaArePhotos) { messageData = ({ type: messageTypes.IMAGES, threadID, creatorID, time, media: photoMedia, }: ImagesMessageData); } else { messageData = ({ type: messageTypes.MULTIMEDIA, threadID, creatorID, time, media: input.media, }: MediaMessageData); } if (localID) { messageData.localID = localID; } return messageData; } type MediaMessageInfoCreationInput = $ReadOnly<{ ...$Exact, id?: ?string, }>; function createMediaMessageInfo( input: MediaMessageInfoCreationInput, ): RawMultimediaMessageInfo { const messageData = createMediaMessageData(input); // This conditional is for Flow let rawMessageInfo; if (messageData.type === messageTypes.IMAGES) { rawMessageInfo = ({ ...messageData, type: messageTypes.IMAGES, }: RawImagesMessageInfo); } else { rawMessageInfo = ({ ...messageData, type: messageTypes.MULTIMEDIA, }: RawMediaMessageInfo); } if (input.id) { rawMessageInfo.id = input.id; } return rawMessageInfo; } function stripLocalIDs( input: $ReadOnlyArray, ): RawMessageInfo[] { const output = []; for (let rawMessageInfo of input) { if ( rawMessageInfo.localID === null || rawMessageInfo.localID === undefined ) { output.push(rawMessageInfo); continue; } invariant( rawMessageInfo.id, 'serverID should be set if localID is being stripped', ); if (rawMessageInfo.type === messageTypes.TEXT) { const { localID, ...rest } = rawMessageInfo; output.push({ ...rest }); } else if (rawMessageInfo.type === messageTypes.IMAGES) { const { localID, ...rest } = rawMessageInfo; output.push(({ ...rest }: RawImagesMessageInfo)); } else if (rawMessageInfo.type === messageTypes.MULTIMEDIA) { const { localID, ...rest } = rawMessageInfo; output.push(({ ...rest }: RawMediaMessageInfo)); } else { invariant( false, `message ${rawMessageInfo.id} of type ${rawMessageInfo.type} ` + `unexpectedly has localID`, ); } } return output; } // Normally we call trim() to remove whitespace at the beginning and end of each // message. However, our Markdown parser supports a "codeBlock" format where the // user can indent each line to indicate a code block. If we match the // corresponding RegEx, we'll only trim whitespace off the end. function trimMessage(message: string) { message = message.replace(/^\n*/, ''); return codeBlockRegex.exec(message) ? message.trimEnd() : message.trim(); } function createMessageReply(message: string) { // add `>` to each line to include empty lines in the quote const quotedMessage = message.replace(/^/gm, '> '); return quotedMessage + '\n\n'; } function getMostRecentNonLocalMessageID( threadInfo: ThreadInfo, messageStore: MessageStore, ): ?string { const thread = messageStore.threads[threadInfo.id]; return thread?.messageIDs.find((id) => !id.startsWith('local')); } export { messageKey, messageID, robotextForMessageInfo, robotextToRawString, createMessageInfo, sortMessageInfoList, rawMessageInfoFromMessageData, mostRecentMessageTimestamp, messageTypeGeneratesNotifs, splitRobotext, parseRobotextEntity, usersInMessageInfos, combineTruncationStatuses, shimUnsupportedRawMessageInfos, messagePreviewText, createMediaMessageData, createMediaMessageInfo, stripLocalIDs, trimMessage, createMessageReply, getMostRecentNonLocalMessageID, }; diff --git a/lib/shared/messages/add-members-message-spec.js b/lib/shared/messages/add-members-message-spec.js index 884206a4a..b47a65dd7 100644 --- a/lib/shared/messages/add-members-message-spec.js +++ b/lib/shared/messages/add-members-message-spec.js @@ -1,28 +1,44 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { AddMembersMessageData, + AddMembersMessageInfo, RawAddMembersMessageInfo, } from '../../types/message/add-members'; import type { MessageSpec } from './message-spec'; export const addMembersMessageSpec: MessageSpec< AddMembersMessageData, RawAddMembersMessageInfo, + AddMembersMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify(data.addedUserIDs); }, rawMessageInfoFromRow(row) { return { type: messageTypes.ADD_MEMBERS, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), addedUserIDs: JSON.parse(row.content), }; }, + + createMessageInfo(rawMessageInfo, creator, params) { + const addedMembers = params.createRelativeUserInfos( + rawMessageInfo.addedUserIDs, + ); + return { + type: messageTypes.ADD_MEMBERS, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + addedMembers, + }; + }, }); diff --git a/lib/shared/messages/change-role-message-spec.js b/lib/shared/messages/change-role-message-spec.js index c94d885b6..4f02ee884 100644 --- a/lib/shared/messages/change-role-message-spec.js +++ b/lib/shared/messages/change-role-message-spec.js @@ -1,33 +1,48 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { ChangeRoleMessageData, + ChangeRoleMessageInfo, RawChangeRoleMessageInfo, } from '../../types/message/change-role'; import type { MessageSpec } from './message-spec'; export const changeRoleMessageSpec: MessageSpec< ChangeRoleMessageData, RawChangeRoleMessageInfo, + ChangeRoleMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ userIDs: data.userIDs, newRole: data.newRole, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); return { type: messageTypes.CHANGE_ROLE, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), userIDs: content.userIDs, newRole: content.newRole, }; }, + + createMessageInfo(rawMessageInfo, creator, params) { + const members = params.createRelativeUserInfos(rawMessageInfo.userIDs); + return { + type: messageTypes.CHANGE_ROLE, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + members, + newRole: rawMessageInfo.newRole, + }; + }, }); diff --git a/lib/shared/messages/change-settings-message-spec.js b/lib/shared/messages/change-settings-message-spec.js index a842d95aa..0b642350f 100644 --- a/lib/shared/messages/change-settings-message-spec.js +++ b/lib/shared/messages/change-settings-message-spec.js @@ -1,33 +1,47 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { ChangeSettingsMessageData, + ChangeSettingsMessageInfo, RawChangeSettingsMessageInfo, } from '../../types/message/change-settings'; import type { MessageSpec } from './message-spec'; export const changeSettingsMessageSpec: MessageSpec< ChangeSettingsMessageData, RawChangeSettingsMessageInfo, + ChangeSettingsMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ [data.field]: data.value, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); const field = Object.keys(content)[0]; return { type: messageTypes.CHANGE_SETTINGS, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), field, value: content[field], }; }, + + createMessageInfo(rawMessageInfo, creator) { + return { + type: messageTypes.CHANGE_SETTINGS, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + field: rawMessageInfo.field, + value: rawMessageInfo.value, + }; + }, }); diff --git a/lib/shared/messages/create-entry-message-spec.js b/lib/shared/messages/create-entry-message-spec.js index 053a245b3..001f77a29 100644 --- a/lib/shared/messages/create-entry-message-spec.js +++ b/lib/shared/messages/create-entry-message-spec.js @@ -1,35 +1,50 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { CreateEntryMessageData, + CreateEntryMessageInfo, RawCreateEntryMessageInfo, } from '../../types/message/create-entry'; import type { MessageSpec } from './message-spec'; export const createEntryMessageSpec: MessageSpec< CreateEntryMessageData, RawCreateEntryMessageInfo, + CreateEntryMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); return { type: messageTypes.CREATE_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, + + createMessageInfo(rawMessageInfo, creator) { + return { + type: messageTypes.CREATE_ENTRY, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + entryID: rawMessageInfo.entryID, + date: rawMessageInfo.date, + text: rawMessageInfo.text, + }; + }, }); diff --git a/lib/shared/messages/create-sidebar-message-spec.js b/lib/shared/messages/create-sidebar-message-spec.js index 90f9a21f0..cf032e84b 100644 --- a/lib/shared/messages/create-sidebar-message-spec.js +++ b/lib/shared/messages/create-sidebar-message-spec.js @@ -1,35 +1,69 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { CreateSidebarMessageData, + CreateSidebarMessageInfo, RawCreateSidebarMessageInfo, } from '../../types/message/create-sidebar'; import type { MessageSpec } from './message-spec'; export const createSidebarMessageSpec: MessageSpec< CreateSidebarMessageData, RawCreateSidebarMessageInfo, + CreateSidebarMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ ...data.initialThreadState, sourceMessageAuthorID: data.sourceMessageAuthorID, }); }, rawMessageInfoFromRow(row) { const { sourceMessageAuthorID, ...initialThreadState } = JSON.parse( row.content, ); return { type: messageTypes.CREATE_SIDEBAR, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), sourceMessageAuthorID, initialThreadState, }; }, + + createMessageInfo(rawMessageInfo, creator, params) { + const { threadInfos } = params; + const parentThreadInfo = + threadInfos[rawMessageInfo.initialThreadState.parentThreadID]; + + const sourceMessageAuthor = params.createRelativeUserInfos([ + rawMessageInfo.sourceMessageAuthorID, + ])[0]; + if (!sourceMessageAuthor) { + return null; + } + + return { + type: messageTypes.CREATE_SIDEBAR, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + sourceMessageAuthor, + initialThreadState: { + name: rawMessageInfo.initialThreadState.name, + parentThreadInfo, + color: rawMessageInfo.initialThreadState.color, + otherMembers: params.createRelativeUserInfos( + rawMessageInfo.initialThreadState.memberIDs.filter( + (userID: string) => userID !== rawMessageInfo.creatorID, + ), + ), + }, + }; + }, }); diff --git a/lib/shared/messages/create-sub-thread-message-spec.js b/lib/shared/messages/create-sub-thread-message-spec.js index ad17426f1..3055afcac 100644 --- a/lib/shared/messages/create-sub-thread-message-spec.js +++ b/lib/shared/messages/create-sub-thread-message-spec.js @@ -1,34 +1,52 @@ // @flow import { permissionLookup } from '../../permissions/thread-permissions'; import { messageTypes } from '../../types/message-types'; import type { CreateSubthreadMessageData, + CreateSubthreadMessageInfo, RawCreateSubthreadMessageInfo, } from '../../types/message/create-subthread'; import { threadPermissions } from '../../types/thread-types'; import type { MessageSpec } from './message-spec'; export const createSubThreadMessageSpec: MessageSpec< CreateSubthreadMessageData, RawCreateSubthreadMessageInfo, + CreateSubthreadMessageInfo, > = Object.freeze({ messageContent(data) { return data.childThreadID; }, rawMessageInfoFromRow(row) { const subthreadPermissions = row.subthread_permissions; if (!permissionLookup(subthreadPermissions, threadPermissions.KNOW_OF)) { return null; } return { type: messageTypes.CREATE_SUB_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), childThreadID: row.content, }; }, + + createMessageInfo(rawMessageInfo, creator, params) { + const { threadInfos } = params; + const childThreadInfo = threadInfos[rawMessageInfo.childThreadID]; + if (!childThreadInfo) { + return null; + } + return { + type: messageTypes.CREATE_SUB_THREAD, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + childThreadInfo, + }; + }, }); diff --git a/lib/shared/messages/create-thread-message-spec.js b/lib/shared/messages/create-thread-message-spec.js index 0558fb508..9976018e0 100644 --- a/lib/shared/messages/create-thread-message-spec.js +++ b/lib/shared/messages/create-thread-message-spec.js @@ -1,28 +1,57 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { CreateThreadMessageData, + CreateThreadMessageInfo, RawCreateThreadMessageInfo, } from '../../types/message/create-thread'; import type { MessageSpec } from './message-spec'; export const createThreadMessageSpec: MessageSpec< CreateThreadMessageData, RawCreateThreadMessageInfo, + CreateThreadMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify(data.initialThreadState); }, rawMessageInfoFromRow(row) { return { type: messageTypes.CREATE_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), initialThreadState: JSON.parse(row.content), }; }, + + createMessageInfo(rawMessageInfo, creator, params) { + const initialParentThreadID = + rawMessageInfo.initialThreadState.parentThreadID; + const parentThreadInfo = initialParentThreadID + ? params.threadInfos[initialParentThreadID] + : null; + + return { + type: messageTypes.CREATE_THREAD, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + initialThreadState: { + name: rawMessageInfo.initialThreadState.name, + parentThreadInfo, + type: rawMessageInfo.initialThreadState.type, + color: rawMessageInfo.initialThreadState.color, + otherMembers: params.createRelativeUserInfos( + rawMessageInfo.initialThreadState.memberIDs.filter( + (userID: string) => userID !== rawMessageInfo.creatorID, + ), + ), + }, + }; + }, }); diff --git a/lib/shared/messages/delete-entry-message-spec.js b/lib/shared/messages/delete-entry-message-spec.js index abd376148..5eec89e09 100644 --- a/lib/shared/messages/delete-entry-message-spec.js +++ b/lib/shared/messages/delete-entry-message-spec.js @@ -1,35 +1,50 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { DeleteEntryMessageData, + DeleteEntryMessageInfo, RawDeleteEntryMessageInfo, } from '../../types/message/delete-entry'; import type { MessageSpec } from './message-spec'; export const deleteEntryMessageSpec: MessageSpec< DeleteEntryMessageData, RawDeleteEntryMessageInfo, + DeleteEntryMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); return { type: messageTypes.DELETE_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, + + createMessageInfo(rawMessageInfo, creator) { + return { + type: messageTypes.DELETE_ENTRY, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + entryID: rawMessageInfo.entryID, + date: rawMessageInfo.date, + text: rawMessageInfo.text, + }; + }, }); diff --git a/lib/shared/messages/edit-entry-message-spec.js b/lib/shared/messages/edit-entry-message-spec.js index 0f4892e48..ef74ca240 100644 --- a/lib/shared/messages/edit-entry-message-spec.js +++ b/lib/shared/messages/edit-entry-message-spec.js @@ -1,35 +1,50 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { EditEntryMessageData, + EditEntryMessageInfo, RawEditEntryMessageInfo, } from '../../types/message/edit-entry'; import type { MessageSpec } from './message-spec'; export const editEntryMessageSpec: MessageSpec< EditEntryMessageData, RawEditEntryMessageInfo, + EditEntryMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); return { type: messageTypes.EDIT_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, + + createMessageInfo(rawMessageInfo, creator) { + return { + type: messageTypes.EDIT_ENTRY, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + entryID: rawMessageInfo.entryID, + date: rawMessageInfo.date, + text: rawMessageInfo.text, + }; + }, }); diff --git a/lib/shared/messages/join-thread-message-spec.js b/lib/shared/messages/join-thread-message-spec.js index 231ce7368..57a70ef15 100644 --- a/lib/shared/messages/join-thread-message-spec.js +++ b/lib/shared/messages/join-thread-message-spec.js @@ -1,23 +1,35 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { JoinThreadMessageData, + JoinThreadMessageInfo, RawJoinThreadMessageInfo, } from '../../types/message/join-thread'; import type { MessageSpec } from './message-spec'; export const joinThreadMessageSpec: MessageSpec< JoinThreadMessageData, RawJoinThreadMessageInfo, + JoinThreadMessageInfo, > = Object.freeze({ rawMessageInfoFromRow(row) { return { type: messageTypes.JOIN_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), }; }, + + createMessageInfo(rawMessageInfo, creator) { + return { + type: messageTypes.JOIN_THREAD, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + }; + }, }); diff --git a/lib/shared/messages/leave-thread-message-spec.js b/lib/shared/messages/leave-thread-message-spec.js index 919f8b2ff..df8311fc2 100644 --- a/lib/shared/messages/leave-thread-message-spec.js +++ b/lib/shared/messages/leave-thread-message-spec.js @@ -1,23 +1,35 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { LeaveThreadMessageData, + LeaveThreadMessageInfo, RawLeaveThreadMessageInfo, } from '../../types/message/leave-thread'; import type { MessageSpec } from './message-spec'; export const leaveThreadMessageSpec: MessageSpec< LeaveThreadMessageData, RawLeaveThreadMessageInfo, + LeaveThreadMessageInfo, > = Object.freeze({ rawMessageInfoFromRow(row) { return { type: messageTypes.LEAVE_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), }; }, + + createMessageInfo(rawMessageInfo, creator) { + return { + type: messageTypes.LEAVE_THREAD, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + }; + }, }); diff --git a/lib/shared/messages/message-spec.js b/lib/shared/messages/message-spec.js index fe40ada1b..aed58e194 100644 --- a/lib/shared/messages/message-spec.js +++ b/lib/shared/messages/message-spec.js @@ -1,22 +1,37 @@ // @flow import type { Media } from '../../types/media-types'; import type { + MessageInfo, RawComposableMessageInfo, + RawMessageInfo, RawRobotextMessageInfo, } from '../../types/message-types'; +import type { ThreadInfo } from '../../types/thread-types'; +import type { RelativeUserInfo } from '../../types/user-types'; -export type MessageSpec = {| +export type MessageSpec = {| +messageContent?: (data: Data) => string | null, +rawMessageInfoFromRow?: ( row: Object, params: {| +localID: string, +media?: $ReadOnlyArray, +derivedMessages: $ReadOnlyMap< string, RawComposableMessageInfo | RawRobotextMessageInfo, >, |}, ) => ?RawInfo, + +createMessageInfo: ( + rawMessageInfo: RawInfo, + creator: RelativeUserInfo, + params: {| + +threadInfos: {| [id: string]: ThreadInfo |}, + +createMessageInfoFromRaw: (rawInfo: RawMessageInfo) => MessageInfo, + +createRelativeUserInfos: ( + userIDs: $ReadOnlyArray, + ) => RelativeUserInfo[], + |}, + ) => ?Info, |}; diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js index ee1677879..081e4b150 100644 --- a/lib/shared/messages/multimedia-message-spec.js +++ b/lib/shared/messages/multimedia-message-spec.js @@ -1,37 +1,75 @@ // @flow import invariant from 'invariant'; +import { messageTypes } from '../../types/message-types'; import type { ImagesMessageData, RawImagesMessageInfo, + ImagesMessageInfo, } from '../../types/message/images'; import type { MediaMessageData, + MediaMessageInfo, RawMediaMessageInfo, } from '../../types/message/media'; import { createMediaMessageInfo } from '../message-utils'; import type { MessageSpec } from './message-spec'; export const multimediaMessageSpec: MessageSpec< MediaMessageData | ImagesMessageData, RawMediaMessageInfo | RawImagesMessageInfo, + MediaMessageInfo | ImagesMessageInfo, > = Object.freeze({ messageContent(data) { const mediaIDs = data.media.map((media) => parseInt(media.id, 10)); return JSON.stringify(mediaIDs); }, rawMessageInfoFromRow(row, params) { const { localID, media } = params; invariant(media, 'Media should be provided'); return createMediaMessageInfo({ threadID: row.threadID.toString(), creatorID: row.creatorID.toString(), media, id: row.id.toString(), localID, time: row.time, }); }, + + createMessageInfo(rawMessageInfo, creator) { + if (rawMessageInfo.type === messageTypes.IMAGES) { + const messageInfo: ImagesMessageInfo = { + type: messageTypes.IMAGES, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + media: rawMessageInfo.media, + }; + if (rawMessageInfo.id) { + messageInfo.id = rawMessageInfo.id; + } + if (rawMessageInfo.localID) { + messageInfo.localID = rawMessageInfo.localID; + } + return messageInfo; + } else if (rawMessageInfo.type === messageTypes.MULTIMEDIA) { + const messageInfo: MediaMessageInfo = { + type: messageTypes.MULTIMEDIA, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + media: rawMessageInfo.media, + }; + if (rawMessageInfo.id) { + messageInfo.id = rawMessageInfo.id; + } + if (rawMessageInfo.localID) { + messageInfo.localID = rawMessageInfo.localID; + } + return messageInfo; + } + }, }); diff --git a/lib/shared/messages/remove-members-message-spec.js b/lib/shared/messages/remove-members-message-spec.js index f794720f8..3a5845f57 100644 --- a/lib/shared/messages/remove-members-message-spec.js +++ b/lib/shared/messages/remove-members-message-spec.js @@ -1,28 +1,44 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { RawRemoveMembersMessageInfo, RemoveMembersMessageData, + RemoveMembersMessageInfo, } from '../../types/message/remove-members'; import type { MessageSpec } from './message-spec'; export const removeMembersMessageSpec: MessageSpec< RemoveMembersMessageData, RawRemoveMembersMessageInfo, + RemoveMembersMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify(data.removedUserIDs); }, rawMessageInfoFromRow(row) { return { type: messageTypes.REMOVE_MEMBERS, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), removedUserIDs: JSON.parse(row.content), }; }, + + createMessageInfo(rawMessageInfo, creator, params) { + const removedMembers = params.createRelativeUserInfos( + rawMessageInfo.removedUserIDs, + ); + return { + type: messageTypes.REMOVE_MEMBERS, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + removedMembers, + }; + }, }); diff --git a/lib/shared/messages/restore-entry-message-spec.js b/lib/shared/messages/restore-entry-message-spec.js index b81ac0bc7..4ba097ec7 100644 --- a/lib/shared/messages/restore-entry-message-spec.js +++ b/lib/shared/messages/restore-entry-message-spec.js @@ -1,35 +1,50 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { RawRestoreEntryMessageInfo, RestoreEntryMessageData, + RestoreEntryMessageInfo, } from '../../types/message/restore-entry'; import type { MessageSpec } from './message-spec'; export const restoreEntryMessageSpec: MessageSpec< RestoreEntryMessageData, RawRestoreEntryMessageInfo, + RestoreEntryMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); return { type: messageTypes.RESTORE_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, + + createMessageInfo(rawMessageInfo, creator) { + return { + type: messageTypes.RESTORE_ENTRY, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + entryID: rawMessageInfo.entryID, + date: rawMessageInfo.date, + text: rawMessageInfo.text, + }; + }, }); diff --git a/lib/shared/messages/sidebar-source-message-spec.js b/lib/shared/messages/sidebar-source-message-spec.js index 4ff08a9ec..1dff675de 100644 --- a/lib/shared/messages/sidebar-source-message-spec.js +++ b/lib/shared/messages/sidebar-source-message-spec.js @@ -1,45 +1,69 @@ // @flow import invariant from 'invariant'; import type { RawSidebarSourceMessageInfo, SidebarSourceMessageData, + SidebarSourceMessageInfo, } from '../../types/message-types'; import { messageTypes } from '../../types/message-types'; import type { MessageSpec } from './message-spec'; export const sidebarSourceMessageSpec: MessageSpec< SidebarSourceMessageData, RawSidebarSourceMessageInfo, + SidebarSourceMessageInfo, > = Object.freeze({ messageContent(data) { const sourceMessageID = data.sourceMessage?.id; invariant(sourceMessageID, 'Source message id should be set'); return JSON.stringify({ sourceMessageID, }); }, rawMessageInfoFromRow(row, params) { const { derivedMessages } = params; invariant(derivedMessages, 'Derived messages should be provided'); const content = JSON.parse(row.content); const sourceMessage = derivedMessages.get(content.sourceMessageID); if (!sourceMessage) { console.warn( `Message with id ${row.id} has a derived message ` + `${content.sourceMessageID} which is not present in the database`, ); } return { type: messageTypes.SIDEBAR_SOURCE, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), sourceMessage, }; }, + + createMessageInfo(rawMessageInfo, creator, params) { + if (!rawMessageInfo.sourceMessage) { + return null; + } + const sourceMessage = params.createMessageInfoFromRaw( + rawMessageInfo.sourceMessage, + ); + invariant( + sourceMessage && sourceMessage.type !== messageTypes.SIDEBAR_SOURCE, + 'Sidebars can not be created from SIDEBAR SOURCE', + ); + + return { + type: messageTypes.SIDEBAR_SOURCE, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + sourceMessage, + }; + }, }); diff --git a/lib/shared/messages/text-message-spec.js b/lib/shared/messages/text-message-spec.js index 0c32e57c3..ce1c427ee 100644 --- a/lib/shared/messages/text-message-spec.js +++ b/lib/shared/messages/text-message-spec.js @@ -1,32 +1,51 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { RawTextMessageInfo, TextMessageData, + TextMessageInfo, } from '../../types/message/text'; import type { MessageSpec } from './message-spec'; export const textMessageSpec: MessageSpec< TextMessageData, RawTextMessageInfo, + TextMessageInfo, > = Object.freeze({ messageContent(data) { return data.text; }, rawMessageInfoFromRow(row, params) { const rawTextMessageInfo: RawTextMessageInfo = { type: messageTypes.TEXT, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), text: row.content, }; if (params.localID) { rawTextMessageInfo.localID = params.localID; } return rawTextMessageInfo; }, + + createMessageInfo(rawMessageInfo, creator) { + const messageInfo: TextMessageInfo = { + type: messageTypes.TEXT, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + text: rawMessageInfo.text, + }; + if (rawMessageInfo.id) { + messageInfo.id = rawMessageInfo.id; + } + if (rawMessageInfo.localID) { + messageInfo.localID = rawMessageInfo.localID; + } + return messageInfo; + }, }); diff --git a/lib/shared/messages/unsupported-message-spec.js b/lib/shared/messages/unsupported-message-spec.js index 82f1dcf9b..18107845e 100644 --- a/lib/shared/messages/unsupported-message-spec.js +++ b/lib/shared/messages/unsupported-message-spec.js @@ -1,9 +1,26 @@ // @flow -import type { RawUnsupportedMessageInfo } from '../../types/message/unsupported'; +import { messageTypes } from '../../types/message-types'; +import type { + RawUnsupportedMessageInfo, + UnsupportedMessageInfo, +} from '../../types/message/unsupported'; import type { MessageSpec } from './message-spec'; export const unsupportedMessageSpec: MessageSpec< null, RawUnsupportedMessageInfo, -> = Object.freeze({}); + UnsupportedMessageInfo, +> = Object.freeze({ + createMessageInfo(rawMessageInfo, creator) { + return { + type: messageTypes.UNSUPPORTED, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + time: rawMessageInfo.time, + robotext: rawMessageInfo.robotext, + unsupportedMessageInfo: rawMessageInfo.unsupportedMessageInfo, + }; + }, +}); diff --git a/lib/shared/messages/update-relationship-message-spec.js b/lib/shared/messages/update-relationship-message-spec.js index f6afec8db..928bffb24 100644 --- a/lib/shared/messages/update-relationship-message-spec.js +++ b/lib/shared/messages/update-relationship-message-spec.js @@ -1,33 +1,51 @@ // @flow import { messageTypes } from '../../types/message-types'; import type { RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageData, + UpdateRelationshipMessageInfo, } from '../../types/message/update-relationship'; import type { MessageSpec } from './message-spec'; export const updateRelationshipMessageSpec: MessageSpec< UpdateRelationshipMessageData, RawUpdateRelationshipMessageInfo, + UpdateRelationshipMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ operation: data.operation, targetID: data.targetID, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); return { type: messageTypes.UPDATE_RELATIONSHIP, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), targetID: content.targetID, operation: content.operation, }; }, + + createMessageInfo(rawMessageInfo, creator, params) { + const target = params.createRelativeUserInfos([rawMessageInfo.targetID])[0]; + if (!target) { + return null; + } + return { + type: messageTypes.UPDATE_RELATIONSHIP, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + creator, + target, + time: rawMessageInfo.time, + operation: rawMessageInfo.operation, + }; + }, });