diff --git a/lib/shared/messages/create-sidebar-message-spec.js b/lib/shared/messages/create-sidebar-message-spec.js index d5adbf6de..737b1b802 100644 --- a/lib/shared/messages/create-sidebar-message-spec.js +++ b/lib/shared/messages/create-sidebar-message-spec.js @@ -1,207 +1,209 @@ // @flow import invariant from 'invariant'; import type { PlatformDetails } from '../../types/device-types'; import { messageTypes } from '../../types/message-types'; import type { CreateSidebarMessageData, CreateSidebarMessageInfo, RawCreateSidebarMessageInfo, } from '../../types/messages/create-sidebar'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; import type { RelativeUserInfo } from '../../types/user-types'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; import { hasMinCodeVersion } from '../version-utils'; import type { CreateMessageInfoParams, MessageSpec, MessageTitleParam, RobotextParams, } from './message-spec'; import { assertSingleMessageInfo } from './utils'; export const createSidebarMessageSpec: MessageSpec< CreateSidebarMessageData, RawCreateSidebarMessageInfo, CreateSidebarMessageInfo, > = Object.freeze({ messageContent(data: CreateSidebarMessageData): string { return JSON.stringify({ ...data.initialThreadState, sourceMessageAuthorID: data.sourceMessageAuthorID, }); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: CreateSidebarMessageInfo = (messageInfo: CreateSidebarMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); validMessageInfo = { ...validMessageInfo, sourceMessageAuthor: { ...validMessageInfo.sourceMessageAuthor, isViewer: false, }, initialThreadState: { ...validMessageInfo.initialThreadState, otherMembers: validMessageInfo.initialThreadState.otherMembers.map( (item) => ({ ...item, isViewer: false, }), ), }, }; } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawCreateSidebarMessageInfo { 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: RawCreateSidebarMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?CreateSidebarMessageInfo { 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: CreateSidebarMessageData, id: string, ): RawCreateSidebarMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: CreateSidebarMessageInfo, creator: string, params: RobotextParams, ): string { 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: RawCreateSidebarMessageInfo, platformDetails: ?PlatformDetails, ): RawCreateSidebarMessageInfo | RawUnsupportedMessageInfo { // 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, }; }, - unshimMessageInfo(unwrapped) { + unshimMessageInfo( + unwrapped: RawCreateSidebarMessageInfo, + ): RawCreateSidebarMessageInfo { return unwrapped; }, notificationTexts(messageInfos, threadInfo) { const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.CREATE_SIDEBAR, 'messageInfo should be messageTypes.CREATE_SIDEBAR!', ); const prefix = stringForUser(messageInfo.creator); const title = threadInfo.uiName; const sourceMessageAuthorPossessive = messageInfo.sourceMessageAuthor .isViewer ? 'your' : `${stringForUser(messageInfo.sourceMessageAuthor)}'s`; const body = `started a sidebar in response to ${sourceMessageAuthorPossessive} ` + `message "${messageInfo.initialThreadState.name ?? ''}"`; const merged = `${prefix} ${body}`; return { merged, body, title, prefix, }; }, generatesNotifs: true, userIDs(rawMessageInfo) { return rawMessageInfo.initialThreadState.memberIDs; }, threadIDs(rawMessageInfo) { const { parentThreadID } = rawMessageInfo.initialThreadState; return [parentThreadID]; }, }); diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js index ea5fabb10..c0fb00f31 100644 --- a/lib/shared/messages/multimedia-message-spec.js +++ b/lib/shared/messages/multimedia-message-spec.js @@ -1,305 +1,311 @@ // @flow import invariant from 'invariant'; import { contentStringForMediaArray, 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 type { + RawMessageInfo, + RawMultimediaMessageInfo, +} from '../../types/message-types'; import { messageTypes, type MultimediaMessageInfo, } from '../../types/message-types'; import type { ImagesMessageData, RawImagesMessageInfo, ImagesMessageInfo, } from '../../types/messages/images'; import type { MediaMessageData, MediaMessageInfo, RawMediaMessageInfo, } from '../../types/messages/media'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; import type { RelativeUserInfo } from '../../types/user-types'; import { createMediaMessageInfo, messagePreviewText, removeCreatorAsViewer, } from '../message-utils'; import { threadIsGroupChat } from '../thread-utils'; import { stringForUser } from '../user-utils'; import { hasMinCodeVersion } from '../version-utils'; import type { MessageSpec, MessageTitleParam, RawMessageInfoFromRowParams, } from './message-spec'; import { joinResult } from './utils'; export const multimediaMessageSpec: MessageSpec< MediaMessageData | ImagesMessageData, RawMediaMessageInfo | RawImagesMessageInfo, MediaMessageInfo | ImagesMessageInfo, > = Object.freeze({ messageContent(data: MediaMessageData | ImagesMessageData): string { const mediaIDs = data.media.map((media) => parseInt(media.id, 10)); return JSON.stringify(mediaIDs); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: MultimediaMessageInfo = (messageInfo: MultimediaMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); } return messagePreviewText(validMessageInfo, threadInfo); }, rawMessageInfoFromRow( row: Object, params: RawMessageInfoFromRowParams, ): RawMediaMessageInfo | RawImagesMessageInfo { 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: RawMediaMessageInfo | RawImagesMessageInfo, creator: RelativeUserInfo, ): ?(MediaMessageInfo | ImagesMessageInfo) { 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: MediaMessageData | ImagesMessageData, id: string, ): RawMediaMessageInfo | RawImagesMessageInfo { if (messageData.type === messageTypes.IMAGES) { return ({ ...messageData, id }: RawImagesMessageInfo); } else { return ({ ...messageData, id }: RawMediaMessageInfo); } }, shimUnsupportedMessageInfo( rawMessageInfo: RawMediaMessageInfo | RawImagesMessageInfo, platformDetails: ?PlatformDetails, ): RawMediaMessageInfo | RawImagesMessageInfo | RawUnsupportedMessageInfo { 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, }; } }, - unshimMessageInfo(unwrapped, messageInfo) { + unshimMessageInfo( + unwrapped: RawMediaMessageInfo | RawImagesMessageInfo, + messageInfo: RawMessageInfo, + ): ?RawMessageInfo { if (unwrapped.type === messageTypes.IMAGES) { return { ...unwrapped, media: unwrapped.media.map((media) => { if (media.dimensions) { return media; } const dimensions = preDimensionUploads[media.id]; invariant( dimensions, 'only four photos were uploaded before dimensions were calculated, ' + `and ${media.id} was not one of them`, ); return { ...media, dimensions }; }), }; } else if (unwrapped.type === messageTypes.MULTIMEDIA) { for (const { type } of unwrapped.media) { if (type !== 'photo' && type !== 'video') { return messageInfo; } } } return undefined; }, notificationTexts(messageInfos, threadInfo, params) { const media = []; for (const messageInfo of messageInfos) { invariant( messageInfo.type === messageTypes.IMAGES || messageInfo.type === messageTypes.MULTIMEDIA, 'messageInfo should be multimedia type!', ); for (const singleMedia of messageInfo.media) { media.push(singleMedia); } } const contentString = contentStringForMediaArray(media); const userString = stringForUser(messageInfos[0].creator); let body, merged; if (!threadInfo.name && !threadIsGroupChat(threadInfo)) { body = `sent you ${contentString}`; merged = body; } else { body = `sent ${contentString}`; const threadName = params.notifThreadName(threadInfo); merged = `${body} to ${threadName}`; } merged = `${userString} ${merged}`; return { merged, body, title: threadInfo.uiName, prefix: userString, }; }, notificationCollapseKey(rawMessageInfo) { // We use the legacy constant here to collapse both types into one return joinResult( messageTypes.IMAGES, rawMessageInfo.threadID, rawMessageInfo.creatorID, ); }, generatesNotifs: true, includedInRepliesCount: true, }); 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); } } // Four photos were uploaded before dimensions were calculated server-side, // and delivered to clients without dimensions in the MultimediaMessageInfo. const preDimensionUploads = { '156642': { width: 1440, height: 1080 }, '156649': { width: 720, height: 803 }, '156794': { width: 720, height: 803 }, '156877': { width: 574, height: 454 }, }; diff --git a/lib/shared/messages/sidebar-source-message-spec.js b/lib/shared/messages/sidebar-source-message-spec.js index 9b7baf4be..1224ed80b 100644 --- a/lib/shared/messages/sidebar-source-message-spec.js +++ b/lib/shared/messages/sidebar-source-message-spec.js @@ -1,139 +1,141 @@ // @flow import invariant from 'invariant'; import type { PlatformDetails } from '../../types/device-types'; import type { RawSidebarSourceMessageInfo, SidebarSourceMessageData, SidebarSourceMessageInfo, } from '../../types/message-types'; import { messageTypes } from '../../types/message-types'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; import type { RelativeUserInfo } from '../../types/user-types'; import { hasMinCodeVersion } from '../version-utils'; import type { CreateMessageInfoParams, MessageSpec, RawMessageInfoFromRowParams, } from './message-spec'; import { assertSingleMessageInfo } from './utils'; export const sidebarSourceMessageSpec: MessageSpec< SidebarSourceMessageData, RawSidebarSourceMessageInfo, SidebarSourceMessageInfo, > = Object.freeze({ messageContent(data: SidebarSourceMessageData): string { const sourceMessageID = data.sourceMessage?.id; invariant(sourceMessageID, 'Source message id should be set'); return JSON.stringify({ sourceMessageID, }); }, messageTitle() { invariant(false, 'Cannot call messageTitle on sidebarSourceMessageSpec'); }, rawMessageInfoFromRow( row: Object, params: RawMessageInfoFromRowParams, ): RawSidebarSourceMessageInfo { 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: RawSidebarSourceMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?SidebarSourceMessageInfo { 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: SidebarSourceMessageData, id: string, ): RawSidebarSourceMessageInfo { return { ...messageData, id }; }, shimUnsupportedMessageInfo( rawMessageInfo: RawSidebarSourceMessageInfo, platformDetails: ?PlatformDetails, ): RawSidebarSourceMessageInfo | RawUnsupportedMessageInfo { // 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', dontPrefixCreator: true, unsupportedMessageInfo: rawMessageInfo, }; }, - unshimMessageInfo(unwrapped) { + unshimMessageInfo( + unwrapped: RawSidebarSourceMessageInfo, + ): RawSidebarSourceMessageInfo { return unwrapped; }, notificationTexts(messageInfos, threadInfo, params) { const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.SIDEBAR_SOURCE, 'messageInfo should be messageTypes.SIDEBAR_SOURCE!', ); const sourceMessageInfo = messageInfo.sourceMessage; return params.notificationTexts([sourceMessageInfo], threadInfo); }, generatesNotifs: false, startsThread: true, }); diff --git a/lib/shared/messages/update-relationship-message-spec.js b/lib/shared/messages/update-relationship-message-spec.js index 9524ed7d8..ca3f8be02 100644 --- a/lib/shared/messages/update-relationship-message-spec.js +++ b/lib/shared/messages/update-relationship-message-spec.js @@ -1,162 +1,164 @@ // @flow import invariant from 'invariant'; import type { PlatformDetails } from '../../types/device-types'; import { messageTypes } from '../../types/message-types'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; import type { RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageData, UpdateRelationshipMessageInfo, } from '../../types/messages/update-relationship'; import type { RelativeUserInfo } from '../../types/user-types'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; import { hasMinCodeVersion } from '../version-utils'; import type { CreateMessageInfoParams, MessageSpec, MessageTitleParam, RobotextParams, } from './message-spec'; import { assertSingleMessageInfo } from './utils'; export const updateRelationshipMessageSpec: MessageSpec< UpdateRelationshipMessageData, RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageInfo, > = Object.freeze({ messageContent(data: UpdateRelationshipMessageData): string { return JSON.stringify({ operation: data.operation, targetID: data.targetID, }); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: UpdateRelationshipMessageInfo = (messageInfo: UpdateRelationshipMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); validMessageInfo = { ...validMessageInfo, target: { ...validMessageInfo.target, isViewer: false }, }; } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawUpdateRelationshipMessageInfo { 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: RawUpdateRelationshipMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?UpdateRelationshipMessageInfo { 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: UpdateRelationshipMessageData, id: string, ): RawUpdateRelationshipMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: UpdateRelationshipMessageInfo, creator: string, params: RobotextParams, ): string { 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: RawUpdateRelationshipMessageInfo, platformDetails: ?PlatformDetails, ): RawUpdateRelationshipMessageInfo | RawUnsupportedMessageInfo { 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, }; }, - unshimMessageInfo(unwrapped) { + unshimMessageInfo( + unwrapped: RawUpdateRelationshipMessageInfo, + ): RawUpdateRelationshipMessageInfo { return unwrapped; }, notificationTexts(messageInfos, threadInfo) { const messageInfo = assertSingleMessageInfo(messageInfos); const prefix = stringForUser(messageInfo.creator); const title = threadInfo.uiName; const body = messageInfo.operation === 'request_sent' ? 'sent you a friend request' : 'accepted your friend request'; const merged = `${prefix} ${body}`; return { merged, body, title, prefix, }; }, generatesNotifs: true, });