diff --git a/lib/shared/message-utils.js b/lib/shared/message-utils.js index 0fa0d371b..2de1b3e9a 100644 --- a/lib/shared/message-utils.js +++ b/lib/shared/message-utils.js @@ -1,548 +1,413 @@ // @flow import invariant from 'invariant'; import _maxBy from 'lodash/fp/maxBy'; -import { shimUploadURI, multimediaMessagePreview } from '../media/media-utils'; +import { 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 { Media } 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 { ImagesMessageData, RawImagesMessageInfo, } from '../types/message/images'; import type { MediaMessageData, RawMediaMessageInfo, } from '../types/message/media'; import { type ThreadInfo } from '../types/thread-types'; import type { RelativeUserInfo, UserInfos } from '../types/user-types'; 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 robotextForMessageInfo( messageInfo: RobotextMessageInfo, threadInfo: ThreadInfo, ): string { const creator = robotextForUser(messageInfo.creator); const messageSpec = messageSpecs[messageInfo.type]; invariant( messageSpec.robotext, `we're not aware of messageType ${messageInfo.type}`, ); return messageSpec.robotext(messageInfo, creator, { encodedThreadEntity, robotextForUsers, robotextForUser, threadInfo, }); } 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; } 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 { const messageSpec = messageSpecs[messageData.type]; invariant( messageSpec.rawMessageInfoFromMessageData, `we're not aware of messageType ${messageData.type}`, ); return messageSpec.rawMessageInfoFromMessageData(messageData, id); } 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, - }; + const { shimUnsupportedMessageInfo } = messageSpecs[rawMessageInfo.type]; + if (shimUnsupportedMessageInfo) { + return shimUnsupportedMessageInfo(rawMessageInfo, platformDetails); } 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/create-sidebar-message-spec.js b/lib/shared/messages/create-sidebar-message-spec.js index a142c6bc9..216148e35 100644 --- a/lib/shared/messages/create-sidebar-message-spec.js +++ b/lib/shared/messages/create-sidebar-message-spec.js @@ -1,88 +1,109 @@ // @flow +import invariant from 'invariant'; + import { messageTypes } from '../../types/message-types'; import type { CreateSidebarMessageData, CreateSidebarMessageInfo, RawCreateSidebarMessageInfo, } from '../../types/message/create-sidebar'; +import { hasMinCodeVersion } from '../version-utils'; 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, ), ), }, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { let text = `started ${params.encodedThreadEntity( messageInfo.threadID, `this sidebar`, )}`; const users = messageInfo.initialThreadState.otherMembers.filter( (member) => member.id !== messageInfo.sourceMessageAuthor.id, ); if (users.length !== 0) { const initialUsersString = params.robotextForUsers(users); text += ` and added ${initialUsersString}`; } return `${creator} ${text}`; }, + + shimUnsupportedMessageInfo(rawMessageInfo, platformDetails) { + // 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, + }; + }, }); diff --git a/lib/shared/messages/message-spec.js b/lib/shared/messages/message-spec.js index 0fb694f9a..5d30eed37 100644 --- a/lib/shared/messages/message-spec.js +++ b/lib/shared/messages/message-spec.js @@ -1,48 +1,54 @@ // @flow +import type { PlatformDetails } from '../../types/device-types'; import type { Media } from '../../types/media-types'; import type { MessageInfo, RawComposableMessageInfo, RawMessageInfo, RawRobotextMessageInfo, } from '../../types/message-types'; +import type { RawUnsupportedMessageInfo } from '../../types/message/unsupported'; import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; 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, +rawMessageInfoFromMessageData?: (messageData: Data, id: string) => RawInfo, +robotext?: ( messageInfo: Info, creator: string, params: {| +encodedThreadEntity: (threadID: string, text: string) => string, +robotextForUsers: (users: RelativeUserInfo[]) => string, +robotextForUser: (user: RelativeUserInfo) => string, +threadInfo: ThreadInfo, |}, ) => string, + +shimUnsupportedMessageInfo?: ( + rawMessageInfo: RawInfo, + platformDetails: ?PlatformDetails, + ) => RawInfo | RawUnsupportedMessageInfo, |}; diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js index 951b8e6bb..0392e3a45 100644 --- a/lib/shared/messages/multimedia-message-spec.js +++ b/lib/shared/messages/multimedia-message-spec.js @@ -1,83 +1,182 @@ // @flow import invariant from 'invariant'; +import { + multimediaMessagePreview, + shimUploadURI, +} from '../../media/media-utils'; +import type { PlatformDetails } from '../../types/device-types'; +import type { Media, Video, Image } from '../../types/media-types'; +import type { RawMultimediaMessageInfo } from '../../types/message-types'; 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 { hasMinCodeVersion } from '../version-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; } }, rawMessageInfoFromMessageData(messageData, id) { if (messageData.type === messageTypes.IMAGES) { return ({ ...messageData, id }: RawImagesMessageInfo); } else { return ({ ...messageData, id }: RawMediaMessageInfo); } }, + + shimUnsupportedMessageInfo(rawMessageInfo, platformDetails) { + 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 { + 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, + }; + } + }, }); + +function shimMediaMessageInfo( + rawMessageInfo: RawMultimediaMessageInfo, + platformDetails: ?PlatformDetails, +): RawMultimediaMessageInfo { + if (rawMessageInfo.type === messageTypes.IMAGES) { + let uriChanged = false; + const newMedia: Image[] = []; + for (const 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 (const 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); + } +} diff --git a/lib/shared/messages/sidebar-source-message-spec.js b/lib/shared/messages/sidebar-source-message-spec.js index 626b407cf..105c91423 100644 --- a/lib/shared/messages/sidebar-source-message-spec.js +++ b/lib/shared/messages/sidebar-source-message-spec.js @@ -1,73 +1,95 @@ // @flow import invariant from 'invariant'; import type { RawSidebarSourceMessageInfo, SidebarSourceMessageData, SidebarSourceMessageInfo, } from '../../types/message-types'; import { messageTypes } from '../../types/message-types'; +import { hasMinCodeVersion } from '../version-utils'; 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, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, + + shimUnsupportedMessageInfo(rawMessageInfo, platformDetails) { + // 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, + }; + }, }); diff --git a/lib/shared/messages/update-relationship-message-spec.js b/lib/shared/messages/update-relationship-message-spec.js index 861dede16..642df52a6 100644 --- a/lib/shared/messages/update-relationship-message-spec.js +++ b/lib/shared/messages/update-relationship-message-spec.js @@ -1,74 +1,92 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; import type { RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageData, UpdateRelationshipMessageInfo, } from '../../types/message/update-relationship'; +import { hasMinCodeVersion } from '../version-utils'; 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, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { const target = params.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}`, ); }, + + shimUnsupportedMessageInfo(rawMessageInfo, platformDetails) { + 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, + }; + }, });