diff --git a/keyserver/src/fetchers/message-fetchers.js b/keyserver/src/fetchers/message-fetchers.js --- a/keyserver/src/fetchers/message-fetchers.js +++ b/keyserver/src/fetchers/message-fetchers.js @@ -719,6 +719,9 @@ if (row.type === messageTypes.SIDEBAR_SOURCE) { const content = JSON.parse(row.content); requiredIDs.add(content.sourceMessageID); + } else if (row.type === messageTypes.TOGGLE_PIN) { + const content = JSON.parse(row.content); + requiredIDs.add(content.targetMessageID); } } diff --git a/keyserver/src/updaters/thread-updaters.js b/keyserver/src/updaters/thread-updaters.js --- a/keyserver/src/updaters/thread-updaters.js +++ b/keyserver/src/updaters/thread-updaters.js @@ -1,6 +1,7 @@ // @flow import { filteredThreadIDs } from 'lib/selectors/calendar-filter-selectors.js'; +import { getPinnedContentFromMessage } from 'lib/shared/message-utils.js'; import { threadHasAdminRole, roleIsAdminRole, @@ -43,7 +44,10 @@ import { createUpdates } from '../creators/update-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; -import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; +import { + fetchMessageInfos, + fetchMessageInfoByID, +} from '../fetchers/message-fetchers.js'; import { fetchThreadInfos, fetchServerThreadInfos, @@ -852,14 +856,15 @@ const [threadResult] = await dbQuery(threadQuery); const threadID = threadResult[0].thread.toString(); - const hasPermission = await checkThreadPermission( - viewer, - threadID, - threadPermissions.MANAGE_PINS, - ); + const [hasPermission, targetMessage] = await Promise.all([ + checkThreadPermission(viewer, threadID, threadPermissions.MANAGE_PINS), + fetchMessageInfoByID(viewer, messageID), + ]); if (!hasPermission) { throw new ServerError('invalid_credentials'); + } else if (!targetMessage) { + throw new ServerError('invalid_parameters'); } const pinnedValue = action === 'pin' ? 1 : 0; @@ -872,6 +877,18 @@ `; await dbQuery(togglePinQuery); + + const messageData = { + type: messageTypes.TOGGLE_PIN, + threadID, + targetMessageID: messageID, + action, + pinnedContent: getPinnedContentFromMessage(targetMessage), + creatorID: viewer.userID, + time: Date.now(), + }; + + await createMessages(viewer, [messageData]); } export { diff --git a/lib/shared/message-utils.js b/lib/shared/message-utils.js --- a/lib/shared/message-utils.js +++ b/lib/shared/message-utils.js @@ -10,6 +10,7 @@ import { messageSpecs } from './messages/message-specs.js'; import { threadIsGroupChat } from './thread-utils.js'; import { useStringForUser } from '../hooks/ens-cache.js'; +import { contentStringForMediaArray } from '../media/media-utils.js'; import { userIDsToRelativeUserInfos } from '../selectors/user-selectors.js'; import { type PlatformDetails, isWebPlatform } from '../types/device-types.js'; import type { Media } from '../types/media-types.js'; @@ -551,6 +552,20 @@ return messageSpec.useCreationSideEffectsFunc(); } +function getPinnedContentFromMessage(targetMessage: RawMessageInfo): string { + let pinnedContent; + if ( + targetMessage.type === messageTypes.IMAGES || + targetMessage.type === messageTypes.MULTIMEDIA + ) { + pinnedContent = contentStringForMediaArray(targetMessage.media); + } else { + pinnedContent = 'a message'; + } + + return pinnedContent; +} + export { localIDPrefix, messageKey, @@ -575,4 +590,5 @@ mergeThreadMessageInfos, useMessagePreview, useMessageCreationSideEffectsFunc, + getPinnedContentFromMessage, }; diff --git a/lib/shared/messages/message-specs.js b/lib/shared/messages/message-specs.js --- a/lib/shared/messages/message-specs.js +++ b/lib/shared/messages/message-specs.js @@ -18,6 +18,7 @@ import { restoreEntryMessageSpec } from './restore-entry-message-spec.js'; import { sidebarSourceMessageSpec } from './sidebar-source-message-spec.js'; import { textMessageSpec } from './text-message-spec.js'; +import { togglePinMessageSpec } from './toggle-pin-message-spec.js'; import { unsupportedMessageSpec } from './unsupported-message-spec.js'; import { updateRelationshipMessageSpec } from './update-relationship-message-spec.js'; import { messageTypes, type MessageType } from '../../types/message-types.js'; @@ -45,4 +46,5 @@ [messageTypes.SIDEBAR_SOURCE]: sidebarSourceMessageSpec, [messageTypes.CREATE_SIDEBAR]: createSidebarMessageSpec, [messageTypes.REACTION]: reactionMessageSpec, + [messageTypes.TOGGLE_PIN]: togglePinMessageSpec, }); diff --git a/lib/shared/messages/toggle-pin-message-spec.js b/lib/shared/messages/toggle-pin-message-spec.js new file mode 100644 --- /dev/null +++ b/lib/shared/messages/toggle-pin-message-spec.js @@ -0,0 +1,159 @@ +// @flow + +import invariant from 'invariant'; + +import type { + MessageSpec, + RobotextParams, + RawMessageInfoFromServerDBRowParams, +} from './message-spec.js'; +import type { PlatformDetails } from '../../types/device-types'; +import { messageTypes } from '../../types/message-types.js'; +import type { ClientDBMessageInfo } from '../../types/message-types.js'; +import type { + TogglePinMessageData, + TogglePinMessageInfo, + RawTogglePinMessageInfo, +} from '../../types/messages/toggle-pin.js'; +import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; +import type { RelativeUserInfo } from '../../types/user-types.js'; +import { ET, type EntityText } from '../../utils/entity-text.js'; +import { getPinnedContentFromClientDBMessageInfo } from '../../utils/message-ops-utils.js'; +import { getPinnedContentFromMessage } from '../message-utils.js'; +import { hasMinCodeVersion } from '../version-utils.js'; + +export const togglePinMessageSpec: MessageSpec< + TogglePinMessageData, + RawTogglePinMessageInfo, + TogglePinMessageInfo, +> = Object.freeze({ + messageContentForServerDB( + data: TogglePinMessageData | RawTogglePinMessageInfo, + ): string { + return JSON.stringify({ + action: data.action, + threadID: data.threadID, + targetMessageID: data.targetMessageID, + }); + }, + + messageContentForClientDB(data: RawTogglePinMessageInfo): string { + return this.messageContentForServerDB(data); + }, + + rawMessageInfoFromServerDBRow( + row: Object, + params: RawMessageInfoFromServerDBRowParams, + ): RawTogglePinMessageInfo { + const content = JSON.parse(row.content); + const { derivedMessages } = params; + const targetMessage = derivedMessages.get(content.targetMessageID); + invariant(targetMessage, 'targetMessage should be defined'); + + return { + type: messageTypes.TOGGLE_PIN, + id: row.id.toString(), + threadID: row.threadID.toString(), + targetMessageID: content.targetMessageID.toString(), + action: content.action, + pinnedContent: getPinnedContentFromMessage(targetMessage), + time: row.time, + creatorID: row.creatorID.toString(), + }; + }, + + rawMessageInfoFromClientDB( + clientDBMessageInfo: ClientDBMessageInfo, + ): RawTogglePinMessageInfo { + invariant( + clientDBMessageInfo.content !== undefined && + clientDBMessageInfo.content !== null, + 'content must be defined for TogglePin', + ); + const content = JSON.parse(clientDBMessageInfo.content); + const pinnedContent = + getPinnedContentFromClientDBMessageInfo(clientDBMessageInfo); + + const rawTogglePinMessageInfo: RawTogglePinMessageInfo = { + type: messageTypes.TOGGLE_PIN, + id: clientDBMessageInfo.id, + threadID: clientDBMessageInfo.thread, + targetMessageID: content.targetMessageID, + action: content.action, + pinnedContent, + time: parseInt(clientDBMessageInfo.time), + creatorID: clientDBMessageInfo.user, + }; + return rawTogglePinMessageInfo; + }, + + createMessageInfo( + rawMessageInfo: RawTogglePinMessageInfo, + creator: RelativeUserInfo, + ): TogglePinMessageInfo { + return { + type: messageTypes.TOGGLE_PIN, + id: rawMessageInfo.id, + threadID: rawMessageInfo.threadID, + targetMessageID: rawMessageInfo.targetMessageID, + action: rawMessageInfo.action, + pinnedContent: rawMessageInfo.pinnedContent, + creator, + time: rawMessageInfo.time, + }; + }, + + rawMessageInfoFromMessageData( + messageData: TogglePinMessageData, + id: ?string, + ): RawTogglePinMessageInfo { + invariant(id, 'RawTogglePinMessageInfo needs id'); + return { ...messageData, id }; + }, + + robotext( + messageInfo: TogglePinMessageInfo, + params: RobotextParams, + ): EntityText { + const creator = ET.user({ userInfo: messageInfo.creator }); + const action = messageInfo.action === 'pin' ? 'pinned' : 'unpinned'; + const pinnedContent = messageInfo.pinnedContent; + const preposition = messageInfo.action === 'pin' ? 'in' : 'from'; + return ET`${creator} ${action} ${pinnedContent} ${preposition} ${ET.thread({ + display: 'alwaysDisplayShortName', + threadID: messageInfo.threadID, + threadType: params.threadInfo?.type, + parentThreadID: params.threadInfo?.parentThreadID, + })}`; + }, + + shimUnsupportedMessageInfo( + rawMessageInfo: RawTogglePinMessageInfo, + platformDetails: ?PlatformDetails, + ): RawTogglePinMessageInfo | RawUnsupportedMessageInfo { + // TODO: Change before landing + if (hasMinCodeVersion(platformDetails, 10000)) { + 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: 'toggled a message pin', + unsupportedMessageInfo: rawMessageInfo, + }; + }, + + unshimMessageInfo( + unwrapped: RawTogglePinMessageInfo, + ): RawTogglePinMessageInfo { + return unwrapped; + }, + + generatesNotifs: async () => undefined, +}); diff --git a/lib/types/message-types.js b/lib/types/message-types.js --- a/lib/types/message-types.js +++ b/lib/types/message-types.js @@ -89,6 +89,11 @@ TextMessageData, TextMessageInfo, } from './messages/text.js'; +import type { + TogglePinMessageData, + TogglePinMessageInfo, + RawTogglePinMessageInfo, +} from './messages/toggle-pin.js'; import type { RawUnsupportedMessageInfo, UnsupportedMessageInfo, @@ -131,6 +136,7 @@ // Appears in the newly created sidebar CREATE_SIDEBAR: 18, REACTION: 19, + TOGGLE_PIN: 20, }); export type MessageType = $Values; export function assertMessageType(ourMessageType: number): MessageType { @@ -154,7 +160,8 @@ ourMessageType === 16 || ourMessageType === 17 || ourMessageType === 18 || - ourMessageType === 19, + ourMessageType === 19 || + ourMessageType === 20, 'number is not MessageType enum', ); return ourMessageType; @@ -245,7 +252,8 @@ | UpdateRelationshipMessageData | SidebarSourceMessageData | CreateSidebarMessageData - | ReactionMessageData; + | ReactionMessageData + | TogglePinMessageData; export type MultimediaMessageData = ImagesMessageData | MediaMessageData; @@ -271,7 +279,8 @@ | RawRestoreEntryMessageInfo | RawUpdateRelationshipMessageInfo | RawCreateSidebarMessageInfo - | RawUnsupportedMessageInfo; + | RawUnsupportedMessageInfo + | RawTogglePinMessageInfo; export type RawSidebarSourceMessageInfo = { ...SidebarSourceMessageData, id: string, @@ -319,7 +328,8 @@ | RestoreEntryMessageInfo | UnsupportedMessageInfo | UpdateRelationshipMessageInfo - | CreateSidebarMessageInfo; + | CreateSidebarMessageInfo + | TogglePinMessageInfo; export type PreviewableMessageInfo = | RobotextMessageInfo | MultimediaMessageInfo diff --git a/lib/types/messages/toggle-pin.js b/lib/types/messages/toggle-pin.js new file mode 100644 --- /dev/null +++ b/lib/types/messages/toggle-pin.js @@ -0,0 +1,29 @@ +// @flow + +import type { RelativeUserInfo } from '../user-types.js'; + +export type TogglePinMessageData = { + +type: 20, + +threadID: string, + +targetMessageID: string, + +action: 'pin' | 'unpin', + +pinnedContent: string, + +creatorID: string, + +time: number, +}; + +export type RawTogglePinMessageInfo = { + ...TogglePinMessageData, + +id: string, +}; + +export type TogglePinMessageInfo = { + +type: 20, + +id: string, + +threadID: string, + +targetMessageID: string, + +action: 'pin' | 'unpin', + +pinnedContent: string, + +creator: RelativeUserInfo, + +time: number, +}; diff --git a/lib/utils/message-ops-utils.js b/lib/utils/message-ops-utils.js --- a/lib/utils/message-ops-utils.js +++ b/lib/utils/message-ops-utils.js @@ -2,6 +2,7 @@ import _keyBy from 'lodash/fp/keyBy.js'; +import { contentStringForMediaArray } from '../media/media-utils.js'; import { messageID } from '../shared/message-utils.js'; import { messageSpecs } from '../shared/messages/message-specs.js'; import type { @@ -197,6 +198,21 @@ }); } +function getPinnedContentFromClientDBMessageInfo( + clientDBMessageInfo: ClientDBMessageInfo, +): string { + const { media_infos } = clientDBMessageInfo; + + let pinnedContent; + if (!media_infos) { + pinnedContent = 'a message'; + } else { + const media = translateClientDBMediaInfosToMedia(clientDBMessageInfo); + pinnedContent = contentStringForMediaArray(media); + } + return pinnedContent; +} + export { translateClientDBMediaInfoToImage, translateRawMessageInfoToClientDBMessageInfo, @@ -204,4 +220,5 @@ translateClientDBMessageInfosToRawMessageInfos, convertMessageStoreOperationsToClientDBOperations, translateClientDBMediaInfosToMedia, + getPinnedContentFromClientDBMessageInfo, };