diff --git a/app/sql.js b/app/sql.js index a9f5b0290..7004da794 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1883,15 +1883,15 @@ function searchMessagesInConversation(query, conversationId, limit) { const rows = globalInstance .prepare( `SELECT - messages.json, - snippet(messages_fts, -1, '<>', '<>', '...', 15) as snippet - FROM messages_fts - INNER JOIN ${MESSAGES_TABLE} on messages_fts.id = messages.id + ${MESSAGES_TABLE}.json, + snippet(${MESSAGES_FTS_TABLE}, -1, '<>', '<>', '...', 15) as snippet + FROM ${MESSAGES_FTS_TABLE} + INNER JOIN ${MESSAGES_TABLE} on ${MESSAGES_FTS_TABLE}.id = ${MESSAGES_TABLE}.id WHERE - messages_fts match $query AND - messages.conversationId = $conversationId - ORDER BY messages.received_at DESC - LIMIT $limit;` + ${MESSAGES_FTS_TABLE} match $query AND + ${MESSAGES_TABLE}.conversationId = $conversationId + ORDER BY ${MESSAGES_TABLE}.serverTimestamp DESC, ${MESSAGES_TABLE}.serverId DESC, ${MESSAGES_TABLE}.sent_at DESC, ${MESSAGES_TABLE}.received_at DESC + LIMIT $limit;` ) .all({ query, diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 0723aec45..d5e7aeaae 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1475,8 +1475,11 @@ .module-message-search-result__header__name { font-weight: 300; } -.module-mesages-search-result__header__group { +.module-messages-search-result__header__group { font-weight: 300; + .module-contact-name { + display: initial; + } } // Module: Left Pane diff --git a/ts/components/search/MessageSearchResults.tsx b/ts/components/search/MessageSearchResults.tsx index 8456b36cb..78e205fc5 100644 --- a/ts/components/search/MessageSearchResults.tsx +++ b/ts/components/search/MessageSearchResults.tsx @@ -1,7 +1,6 @@ import React from 'react'; import classNames from 'classnames'; -import { MessageDirection } from '../../models/messageType'; import { getOurPubKeyStrFromCache } from '../../session/utils/User'; import { FindAndFormatContactType, @@ -12,10 +11,9 @@ import { Avatar, AvatarSize } from '../avatar/Avatar'; import { Timestamp } from '../conversation/Timestamp'; import { MessageBodyHighlight } from '../basic/MessageBodyHighlight'; import styled from 'styled-components'; - -type PropsHousekeeping = { - isSelected?: boolean; -}; +import { MessageAttributes } from '../../models/messageType'; +import { useIsPrivate } from '../../hooks/useParamSelector'; +import { UserUtils } from '../../session/utils'; export type PropsForSearchResults = { from: FindAndFormatContactType; @@ -30,7 +28,7 @@ export type PropsForSearchResults = { receivedAt?: number; }; -export type MessageResultProps = PropsForSearchResults & PropsHousekeeping; +export type MessageResultProps = MessageAttributes & { snippet: string }; const FromName = (props: { source: string; destination: string }) => { const { source, destination } = props; @@ -69,7 +67,7 @@ const From = (props: { source: string; destination: string }) => { return (
{fromName} {window.i18n('to')} - +
@@ -80,8 +78,7 @@ const From = (props: { source: string; destination: string }) => { }; const AvatarItem = (props: { source: string }) => { - const { source } = props; - return ; + return ; }; const ResultBody = styled.div` @@ -102,45 +99,57 @@ const ResultBody = styled.div` `; export const MessageSearchResult = (props: MessageResultProps) => { - const { id, conversationId, receivedAt, snippet, destination, source, direction } = props; - - // Some messages miss a source or destination. Doing checks to see if the fields can be derived from other sources. - // E.g. if the source is missing but the message is outgoing, the source will be our pubkey - const sourceOrDestinationDerivable = - (destination && direction === MessageDirection.outgoing) || - !destination || - !source || - (source && direction === MessageDirection.incoming); - - if (!sourceOrDestinationDerivable) { + const { + id, + conversationId, + received_at, + snippet, + source, + sent_at, + serverTimestamp, + timestamp, + direction, + } = props; + + /** destination is only used for search results (showing the `from:` and `to`) + * 1. for messages we sent or synced from another of our devices + * - the conversationId for a private convo + * - the conversationId for a closed group convo + * - the conversationId for an opengroup + * + * 2. for messages we received + * - our own pubkey for a private conversation + * - the conversationID for a closed group + * - the conversationID for a public group + */ + const me = UserUtils.getOurPubKeyStrFromCache(); + + const convoIsPrivate = useIsPrivate(conversationId); + const destination = + direction === 'incoming' ? conversationId : convoIsPrivate ? me : conversationId; + + if (!source && !destination) { return null; } - const effectiveSource = - !source && direction === MessageDirection.outgoing ? getOurPubKeyStrFromCache() : source; - const effectiveDestination = - !destination && direction === MessageDirection.incoming - ? getOurPubKeyStrFromCache() - : destination; - return (
{ - await openConversationToSpecificMessage({ + onClick={() => { + void openConversationToSpecificMessage({ conversationKey: conversationId, messageIdToNavigateTo: id, }); }} className={classNames('module-message-search-result')} > - +
- +
- +
diff --git a/ts/data/data.ts b/ts/data/data.ts index 8a2f63aa3..1e1f0a6b2 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -3,6 +3,7 @@ import { ipcRenderer } from 'electron'; // tslint:disable: no-require-imports no-var-requires one-variable-per-declaration no-void-expression import _ from 'lodash'; +import { MessageResultProps } from '../components/search/MessageSearchResults'; import { ConversationCollection, ConversationModel, @@ -587,9 +588,11 @@ export async function searchConversations(query: string): Promise> { return conversations; } -export async function searchMessages(query: string, limit: number): Promise> { - const messages = await channels.searchMessages(query, limit); - +export async function searchMessages( + query: string, + limit: number +): Promise> { + const messages = (await channels.searchMessages(query, limit)) as Array; return _.uniqWith(messages, (left: { id: string }, right: { id: string }) => { return left.id === right.id; }); @@ -602,10 +605,10 @@ export async function searchMessagesInConversation( query: string, conversationId: string, limit: number -): Promise { - const messages = await channels.searchMessagesInConversation(query, conversationId, { +): Promise> { + const messages = (await channels.searchMessagesInConversation(query, conversationId, { limit, - }); + })) as Array; return messages; } diff --git a/ts/models/message.ts b/ts/models/message.ts index 489ea22b5..26e96a89c 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -766,8 +766,9 @@ export class MessageModel extends Backbone.Model { quote: undefined, groupInvitation: undefined, dataExtractionNotification: undefined, - hasAttachments: false, - hasVisualMediaAttachments: false, + hasAttachments: 0, + hasFileAttachments: 0, + hasVisualMediaAttachments: 0, attachments: undefined, preview: undefined, }); diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index 2e1c55d93..63a78750d 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -30,9 +30,9 @@ export interface MessageAttributes { conversationId: string; errors?: any; flags?: number; - hasAttachments: boolean; - hasFileAttachments: boolean; - hasVisualMediaAttachments: boolean; + hasAttachments: 1 | 0; + hasFileAttachments: 1 | 0; + hasVisualMediaAttachments: 1 | 0; expirationTimerUpdate?: { expireTimer: number; source: string; @@ -86,11 +86,7 @@ export interface MessageAttributes { synced: boolean; sync: boolean; - /** - * This field is used for search only - */ - snippet?: any; - direction: any; + direction: MessageModelType; /** * This is used for when a user screenshots or saves an attachment you sent. @@ -176,7 +172,6 @@ export interface MessageAttributesOptionals { group?: any; timestamp?: number; status?: MessageDeliveryStatus; - dataMessage?: any; sent_to?: Array; sent?: boolean; serverId?: number; @@ -185,8 +180,7 @@ export interface MessageAttributesOptionals { sentSync?: boolean; synced?: boolean; sync?: boolean; - snippet?: any; - direction?: any; + direction?: MessageModelType; messageHash?: string; isDeleted?: boolean; callNotificationType?: CallNotificationType; diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 61d425df7..73b2a9c2d 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -360,7 +360,11 @@ export async function innerHandleContentMessage( content.dataMessage.profileKey = null; } perfStart(`handleDataMessage-${envelope.id}`); - await handleDataMessage(envelope, content.dataMessage, messageHash); + await handleDataMessage( + envelope, + content.dataMessage as SignalService.DataMessage, + messageHash + ); perfEnd(`handleDataMessage-${envelope.id}`, 'handleDataMessage'); return; } diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 36d26a156..d9f5b20f2 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -196,8 +196,6 @@ export async function processDecrypted( } if (decrypted.group) { - // decrypted.group.id = new TextDecoder('utf-8').decode(decrypted.group.id); - switch (decrypted.group.type) { case SignalService.GroupContext.Type.UPDATE: decrypted.body = ''; @@ -274,32 +272,32 @@ function isBodyEmpty(body: string) { */ export async function handleDataMessage( envelope: EnvelopePlus, - dataMessage: SignalService.IDataMessage, + rawDataMessage: SignalService.DataMessage, messageHash: string ): Promise { // we handle group updates from our other devices in handleClosedGroupControlMessage() - if (dataMessage.closedGroupControlMessage) { + if (rawDataMessage.closedGroupControlMessage) { await handleClosedGroupControlMessage( envelope, - dataMessage.closedGroupControlMessage as SignalService.DataMessage.ClosedGroupControlMessage + rawDataMessage.closedGroupControlMessage as SignalService.DataMessage.ClosedGroupControlMessage ); return; } - const message = await processDecrypted(envelope, dataMessage); - const source = dataMessage.syncTarget || envelope.source; + const message = await processDecrypted(envelope, rawDataMessage); + const source = rawDataMessage.syncTarget || envelope.source; const senderPubKey = envelope.senderIdentity || envelope.source; const isMe = UserUtils.isUsFromCache(senderPubKey); - const isSyncMessage = Boolean(dataMessage.syncTarget?.length); + const isSyncMessage = Boolean(rawDataMessage.syncTarget?.length); window?.log?.info(`Handle dataMessage from ${source} `); if (isSyncMessage && !isMe) { window?.log?.warn('Got a sync message from someone else than me. Dropping it.'); return removeFromCache(envelope); - } else if (isSyncMessage && dataMessage.syncTarget) { + } else if (isSyncMessage && rawDataMessage.syncTarget) { // override the envelope source - envelope.source = dataMessage.syncTarget; + envelope.source = rawDataMessage.syncTarget; } const senderConversation = await getConversationController().getOrCreateAndWait( @@ -328,47 +326,37 @@ export async function handleDataMessage( }; } + let groupId: string | null = null; + if (message.group?.id?.length) { + // remove the prefix from the source object so this is correct for all other + groupId = PubKey.removeTextSecurePrefixIfNeeded(toHex(message.group?.id)); + } + const confirm = () => removeFromCache(envelope); const data: MessageCreationData = { source: senderPubKey, destination: isMe ? message.syncTarget : envelope.source, - sourceDevice: 1, timestamp: _.toNumber(envelope.timestamp), receivedAt: envelope.receivedAt, - message, messageHash, isPublic: false, serverId: null, serverTimestamp: null, + groupId, }; - await handleMessageEvent(messageEventType, data, confirm); + await handleMessageEvent(messageEventType, data, message, confirm); } -type MessageDuplicateSearchType = { - body: string; - id: string; - timestamp: number; - serverId?: number; -}; - export type MessageId = { source: string; serverId?: number | null; serverTimestamp?: number | null; - sourceDevice: number; timestamp: number; - message: MessageDuplicateSearchType; }; -const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s - -export async function isMessageDuplicate({ - source, - timestamp, - message, - serverTimestamp, -}: MessageId) { + +export async function isMessageDuplicate({ source, timestamp, serverTimestamp }: MessageId) { // serverTimestamp is only used for opengroupv2 try { let result; @@ -392,33 +380,13 @@ export async function isMessageDuplicate({ sentAt: timestamp, }); - if (!result) { - return false; - } - const filteredResult = [result].filter((m: any) => m.attributes.body === message.body); - return filteredResult.some(m => isDuplicate(m, message, source)); + return Boolean(result); } catch (error) { window?.log?.error('isMessageDuplicate error:', toLogFormat(error)); return false; } } -export const isDuplicate = ( - m: MessageModel, - testedMessage: MessageDuplicateSearchType, - source: string -) => { - // The username in this case is the users pubKey - const sameUsername = m.attributes.source === source; - const sameText = m.attributes.body === testedMessage.body; - // Don't filter out messages that are too far apart from each other - const timestampsSimilar = - Math.abs(m.attributes.sent_at - testedMessage.timestamp) <= - PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES; - - return sameUsername && sameText && timestampsSimilar; -}; - async function handleProfileUpdate( profileKeyBuffer: Uint8Array, convoId: string, @@ -432,25 +400,24 @@ async function handleProfileUpdate( // Will do the save for us if needed await me.setProfileKey(profileKeyBuffer); } else { - const sender = await getConversationController().getOrCreateAndWait( + const senderConvo = await getConversationController().getOrCreateAndWait( convoId, ConversationTypeEnum.PRIVATE ); // Will do the save for us - await sender.setProfileKey(profileKeyBuffer); + await senderConvo.setProfileKey(profileKeyBuffer); } } export type MessageCreationData = { timestamp: number; receivedAt: number; - sourceDevice: number; // always 1 for Session source: string; - message: any; isPublic: boolean; serverId: number | null; serverTimestamp: number | null; + groupId: string | null; // Needed for synced outgoing messages expirationStartTimestamp?: any; // ??? @@ -463,24 +430,15 @@ export function initIncomingMessage(data: MessageCreationData): MessageModel { timestamp, isPublic, receivedAt, - sourceDevice, source, serverId, - message, serverTimestamp, messageHash, + groupId, } = data; - const messageGroupId = message?.group?.id; - const groupIdWithPrefix = messageGroupId && messageGroupId.length > 0 ? messageGroupId : null; - let groupId: string | undefined; - if (groupIdWithPrefix) { - groupId = PubKey.removeTextSecurePrefixIfNeeded(groupIdWithPrefix); - } - const messageData: any = { source, - sourceDevice, serverId, sent_at: timestamp, serverTimestamp, @@ -505,10 +463,9 @@ function createSentMessage(data: MessageCreationData): MessageModel { serverId, isPublic, receivedAt, - sourceDevice, expirationStartTimestamp, destination, - message, + groupId, messageHash, } = data; @@ -518,16 +475,8 @@ function createSentMessage(data: MessageCreationData): MessageModel { expirationStartTimestamp: Math.min(expirationStartTimestamp || data.timestamp || now, now), }; - const messageGroupId = message?.group?.id; - const groupIdWithPrefix = messageGroupId && messageGroupId.length > 0 ? messageGroupId : null; - let groupId: string | undefined; - if (groupIdWithPrefix) { - groupId = PubKey.removeTextSecurePrefixIfNeeded(groupIdWithPrefix); - } - const messageData = { source: UserUtils.getOurPubKeyStrFromCache(), - sourceDevice, serverTimestamp: serverTimestamp || undefined, serverId: serverId || undefined, sent_at: timestamp, @@ -553,22 +502,23 @@ export function createMessage(data: MessageCreationData, isIncoming: boolean): M // tslint:disable:cyclomatic-complexity max-func-body-length */ async function handleMessageEvent( messageEventType: 'sent' | 'message', - data: MessageCreationData, + messageCreationData: MessageCreationData, + rawDataMessage: SignalService.DataMessage, confirm: () => void ): Promise { const isIncoming = messageEventType === 'message'; - if (!data || !data.message) { + if (!messageCreationData || !rawDataMessage) { window?.log?.warn('Invalid data passed to handleMessageEvent.', event); confirm(); return; } - const { message, destination, messageHash } = data; + const { destination, messageHash } = messageCreationData; - let { source } = data; + let { source } = messageCreationData; - const isGroupMessage = Boolean(message.group); + const isGroupMessage = Boolean(rawDataMessage.group); const type = isGroupMessage ? ConversationTypeEnum.GROUP : ConversationTypeEnum.PRIVATE; @@ -578,11 +528,11 @@ async function handleMessageEvent( confirm(); return; } - if (message.profileKey?.length) { - await handleProfileUpdate(message.profileKey, conversationId, isIncoming); + if (rawDataMessage.profileKey?.length) { + await handleProfileUpdate(rawDataMessage.profileKey, conversationId, isIncoming); } - const msg = createMessage(data, isIncoming); + const msg = createMessage(messageCreationData, isIncoming); // if the message is `sent` (from secondary device) we have to set the sender manually... (at least for now) source = source || msg.get('source'); @@ -593,9 +543,11 @@ async function handleMessageEvent( // - group.id if it is a group message if (isGroupMessage) { // remove the prefix from the source object so this is correct for all other - message.group.id = PubKey.removeTextSecurePrefixIfNeeded(message.group.id); + (rawDataMessage as any).group.id = PubKey.removeTextSecurePrefixIfNeeded( + (rawDataMessage as any).group.id + ); - conversationId = message.group.id; + conversationId = (rawDataMessage as any).group.id; } if (!conversationId) { @@ -605,7 +557,7 @@ async function handleMessageEvent( // ========================================= - if (!isGroupMessage && source !== ourNumber) { + if (!rawDataMessage.group && source !== ourNumber) { // Ignore auth from our devices conversationId = source; } @@ -619,11 +571,19 @@ async function handleMessageEvent( } void conversation.queueJob(async () => { - if (await isMessageDuplicate(data)) { + if (await isMessageDuplicate(messageCreationData)) { window?.log?.info('Received duplicate message. Dropping it.'); confirm(); return; } - await handleMessageJob(msg, conversation, message, ourNumber, confirm, source, messageHash); + await handleMessageJob( + msg, + conversation, + rawDataMessage, + ourNumber, + confirm, + source, + messageHash + ); }); } diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index d9f7a5a43..c95d8e658 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -9,7 +9,7 @@ import { MessageModel } from '../models/message'; import { getMessageById, getMessagesBySentAt } from '../../ts/data/data'; import { MessageModelPropsWithoutConvoProps, messagesAdded } from '../state/ducks/conversations'; import { updateProfileOneAtATime } from './dataMessage'; -import Long from 'long'; +import { SignalService } from '../protobuf'; function contentTypeSupported(type: string): boolean { const Chrome = window.Signal.Util.GoogleChrome; @@ -17,15 +17,26 @@ function contentTypeSupported(type: string): boolean { } // tslint:disable-next-line: cyclomatic-complexity -async function copyFromQuotedMessage(msg: MessageModel, quote?: Quote): Promise { +async function copyFromQuotedMessage( + msg: MessageModel, + quote?: SignalService.DataMessage.IQuote | null +): Promise { if (!quote) { return; } - const { attachments, id: quoteId, author } = quote; - const firstAttachment = attachments[0]; - const id: number = Long.isLong(quoteId) ? quoteId.toNumber() : quoteId; + const quoteLocal: Quote = { + attachments: attachments || null, + author: author, + id: _.toNumber(quoteId), + text: null, + referencedMessageNotFound: false, + }; + + const firstAttachment = attachments?.[0] || undefined; + + const id: number = _.toNumber(quoteId); // We always look for the quote by sentAt timestamp, for opengroups, closed groups and session chats // this will return an array of sent message by id we have locally. @@ -38,18 +49,25 @@ async function copyFromQuotedMessage(msg: MessageModel, quote?: Quote): Promise< if (!found) { window?.log?.warn(`We did not found quoted message ${id}.`); - quote.referencedMessageNotFound = true; - msg.set({ quote }); + quoteLocal.referencedMessageNotFound = true; + msg.set({ quote: quoteLocal }); await msg.commit(); return; } window?.log?.info(`Found quoted message id: ${id}`); - quote.referencedMessageNotFound = false; + quoteLocal.referencedMessageNotFound = false; - quote.text = found.get('body') || ''; + quoteLocal.text = found.get('body') || ''; - if (!firstAttachment || !contentTypeSupported(firstAttachment.contentType)) { + // no attachments, just save the quote with the body + if ( + !firstAttachment || + !firstAttachment.contentType || + !contentTypeSupported(firstAttachment.contentType) + ) { + msg.set({ quote: quoteLocal }); + await msg.commit(); return; } @@ -81,6 +99,11 @@ async function copyFromQuotedMessage(msg: MessageModel, quote?: Quote): Promise< }; } } + quoteLocal.attachments = [firstAttachment]; + + msg.set({ quote: quoteLocal }); + await msg.commit(); + return; } function handleLinkPreviews(messageBody: string, messagePreview: any, message: MessageModel) { @@ -172,33 +195,28 @@ async function handleSyncedReceipts(message: MessageModel, conversation: Convers async function handleRegularMessage( conversation: ConversationModel, message: MessageModel, - initialMessage: any, + rawDataMessage: SignalService.DataMessage, source: string, ourNumber: string, messageHash: string ) { const type = message.get('type'); - await copyFromQuotedMessage(message, initialMessage.quote); - - const dataMessage = initialMessage; + await copyFromQuotedMessage(message, rawDataMessage.quote); const now = Date.now(); - if (dataMessage.openGroupInvitation) { - message.set({ groupInvitation: dataMessage.openGroupInvitation }); + if (rawDataMessage.openGroupInvitation) { + message.set({ groupInvitation: rawDataMessage.openGroupInvitation }); } - handleLinkPreviews(dataMessage.body, dataMessage.preview, message); + handleLinkPreviews(rawDataMessage.body, rawDataMessage.preview, message); const existingExpireTimer = conversation.get('expireTimer'); message.set({ - flags: dataMessage.flags, - hasAttachments: dataMessage.hasAttachments, - hasFileAttachments: dataMessage.hasFileAttachments, - hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments, - quote: dataMessage.quote, - attachments: dataMessage.attachments, - body: dataMessage.body, + flags: rawDataMessage.flags, + quote: rawDataMessage.quote, + attachments: rawDataMessage.attachments, + body: rawDataMessage.body, conversationId: conversation.id, decrypted_at: now, messageHash, @@ -245,16 +263,16 @@ async function handleRegularMessage( // Check if we need to update any profile names // the only profile we don't update with what is coming here is ours, // as our profile is shared accross our devices with a ConfigurationMessage - if (type === 'incoming' && dataMessage.profile) { + if (type === 'incoming' && rawDataMessage.profile) { void updateProfileOneAtATime( sendingDeviceConversation, - dataMessage.profile, - dataMessage.profileKey + rawDataMessage.profile, + rawDataMessage.profileKey ); } - if (dataMessage.profileKey) { - await processProfileKey(conversation, sendingDeviceConversation, dataMessage.profileKey); + if (rawDataMessage.profileKey) { + await processProfileKey(conversation, sendingDeviceConversation, rawDataMessage.profileKey); } // we just received a message from that user so we reset the typing indicator for this convo @@ -289,55 +307,53 @@ async function handleExpirationTimerUpdate( } export async function handleMessageJob( - message: MessageModel, + messageModel: MessageModel, conversation: ConversationModel, - initialMessage: any, + rawDataMessage: SignalService.DataMessage, ourNumber: string, confirm: () => void, source: string, messageHash: string ) { window?.log?.info( - `Starting handleDataMessage for message ${message.idForLogging()}, ${message.get( + `Starting handleDataMessage for message ${messageModel.idForLogging()}, ${messageModel.get( 'serverTimestamp' - ) || message.get('timestamp')} in conversation ${conversation.idForLogging()}` + ) || messageModel.get('timestamp')} in conversation ${conversation.idForLogging()}` ); try { - message.set({ flags: initialMessage.flags }); - if (message.isExpirationTimerUpdate()) { - const { expireTimer } = initialMessage; + messageModel.set({ flags: rawDataMessage.flags }); + if (messageModel.isExpirationTimerUpdate()) { + const { expireTimer } = rawDataMessage; const oldValue = conversation.get('expireTimer'); if (expireTimer === oldValue) { - if (confirm) { - confirm(); - } + confirm?.(); window?.log?.info( 'Dropping ExpireTimerUpdate message as we already have the same one set.' ); return; } - await handleExpirationTimerUpdate(conversation, message, source, expireTimer); + await handleExpirationTimerUpdate(conversation, messageModel, source, expireTimer); } else { await handleRegularMessage( conversation, - message, - initialMessage, + messageModel, + rawDataMessage, source, ourNumber, messageHash ); } - const id = await message.commit(); + const id = await messageModel.commit(); - message.set({ id }); + messageModel.set({ id }); // Note that this can save the message again, if jobs were queued. We need to // call it after we have an id for this message, because the jobs refer back // to their source message. - void queueAttachmentDownloads(message, conversation); + void queueAttachmentDownloads(messageModel, conversation); const unreadCount = await conversation.getUnreadCount(); conversation.set({ unreadCount }); @@ -349,37 +365,37 @@ export async function handleMessageJob( // We go to the database here because, between the message save above and // the previous line's trigger() call, we might have marked all messages // unread in the database. This message might already be read! - const fetched = await getMessageById(message.get('id')); + const fetched = await getMessageById(messageModel.get('id')); - const previousUnread = message.get('unread'); + const previousUnread = messageModel.get('unread'); // Important to update message with latest read state from database - message.merge(fetched); + messageModel.merge(fetched); - if (previousUnread !== message.get('unread')) { + if (previousUnread !== messageModel.get('unread')) { window?.log?.warn( 'Caught race condition on new message read state! ' + 'Manually starting timers.' ); // We call markRead() even though the message is already // marked read because we need to start expiration // timers, etc. - await message.markRead(Date.now()); + await messageModel.markRead(Date.now()); } } catch (error) { - window?.log?.warn('handleDataMessage: Message', message.idForLogging(), 'was deleted'); + window?.log?.warn('handleDataMessage: Message', messageModel.idForLogging(), 'was deleted'); } // this updates the redux store. // if the convo on which this message should become visible, // it will be shown to the user, and might as well be read right away - updatesToDispatch.set(message.id, { + updatesToDispatch.set(messageModel.id, { conversationKey: conversation.id, - messageModelProps: message.getMessageModelProps(), + messageModelProps: messageModel.getMessageModelProps(), }); throttledAllMessagesAddedDispatch(); - if (message.get('unread')) { - conversation.throttledNotify(message); + if (messageModel.get('unread')) { + conversation.throttledNotify(messageModel); } if (confirm) { @@ -387,7 +403,7 @@ export async function handleMessageJob( } } catch (error) { const errorForLog = error && error.stack ? error.stack : error; - window?.log?.error('handleDataMessage', message.idForLogging(), 'error:', errorForLog); + window?.log?.error('handleDataMessage', messageModel.idForLogging(), 'error:', errorForLog); throw error; } diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 075d413fd..56c413c13 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -285,7 +285,6 @@ export async function handleOpenGroupV2Message( window?.log?.error('Invalid decoded opengroup message: no dataMessage'); return; } - const dataMessage = idataMessage as SignalService.DataMessage; if (!getConversationController().get(conversationId)) { window?.log?.error('Received a message for an unknown convo. Skipping'); @@ -310,7 +309,6 @@ export async function handleOpenGroupV2Message( // for an opengroupv2 incoming message the serverTimestamp and the timestamp const messageCreationData: MessageCreationData = { isPublic: true, - sourceDevice: 1, serverId, serverTimestamp: sentTimestamp, receivedAt: Date.now(), @@ -318,9 +316,10 @@ export async function handleOpenGroupV2Message( timestamp: sentTimestamp, expirationStartTimestamp: undefined, source: sender, - message: dataMessage, + groupId: null, messageHash: '', // we do not care of a hash for an opengroup message }; + // WARNING this is important that the isMessageDuplicate is made in the conversation.queueJob const isDuplicate = await isMessageDuplicate({ ...messageCreationData }); @@ -334,6 +333,14 @@ export async function handleOpenGroupV2Message( const msg = createMessage(messageCreationData, !isMe); const ourNumber = UserUtils.getOurPubKeyStrFromCache(); - await handleMessageJob(msg, conversation, decoded?.dataMessage, ourNumber, noop, sender, ''); + await handleMessageJob( + msg, + conversation, + decoded?.dataMessage as SignalService.DataMessage, + ourNumber, + noop, + sender, + '' + ); }); } diff --git a/ts/receiver/types.ts b/ts/receiver/types.ts index dc85d463f..ee47df5b6 100644 --- a/ts/receiver/types.ts +++ b/ts/receiver/types.ts @@ -3,8 +3,8 @@ import { SignalService } from '../protobuf'; export interface Quote { id: number; // this is in fact a uint64 so we will have an issue author: string; - attachments: Array; - text: string; + attachments: Array | null; + text: string | null; referencedMessageNotFound: boolean; } diff --git a/ts/session/utils/AttachmentsDownload.ts b/ts/session/utils/AttachmentsDownload.ts index 6e643577d..0122b5fd4 100644 --- a/ts/session/utils/AttachmentsDownload.ts +++ b/ts/session/utils/AttachmentsDownload.ts @@ -13,6 +13,7 @@ import { import { MessageModel } from '../../models/message'; import { downloadAttachment, downloadAttachmentOpenGroupV2 } from '../../receiver/attachments'; import { initializeAttachmentLogic, processNewAttachment } from '../../types/MessageAttachment'; +import { getAttachmentMetadata } from '../../types/message/initializeAttachmentMetadata'; // this cause issues if we increment that value to > 1. const MAX_ATTACHMENT_JOB_PARALLELISM = 3; @@ -212,6 +213,14 @@ async function _runJob(job: any) { contentType: attachment.contentType, }); found = await getMessageById(messageId); + if (found) { + const { + hasAttachments, + hasVisualMediaAttachments, + hasFileAttachments, + } = await getAttachmentMetadata(found); + found.set({ hasAttachments, hasVisualMediaAttachments, hasFileAttachments }); + } _addAttachmentToMessage(found, upgradedAttachment, { type, index }); diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 98e3f5c64..76fcbd8a2 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -7,6 +7,7 @@ import { PubKey } from '../../session/types'; import { ConversationTypeEnum } from '../../models/conversation'; import _ from 'lodash'; import { getConversationController } from '../../session/conversations'; +import { MessageResultProps } from '../../components/search/MessageSearchResults'; // State @@ -19,9 +20,7 @@ export type SearchStateType = { conversations: Array; contacts: Array; - // TODO: ww typing - messages?: Array; - messagesLookup?: any; + messages?: Array; }; // Actions @@ -30,8 +29,7 @@ type SearchResultsPayloadType = { normalizedPhoneNumber?: string; conversations: Array; contacts: Array; - - messages?: Array; + messages?: Array; }; type SearchResultsKickoffActionType = { @@ -76,25 +74,25 @@ export function search(query: string, options: SearchOptions): SearchResultsKick async function doSearch(query: string, options: SearchOptions): Promise { const advancedSearchOptions = getAdvancedSearchOptionsFromQuery(query); const processedQuery = advancedSearchOptions.query; - const isAdvancedQuery = query !== processedQuery; + // const isAdvancedQuery = query !== processedQuery; const [discussions, messages] = await Promise.all([ queryConversationsAndContacts(processedQuery, options), queryMessages(processedQuery), ]); const { conversations, contacts } = discussions; - let filteredMessages = _.compact(messages); - if (isAdvancedQuery) { - const senderFilterQuery = - advancedSearchOptions.from && advancedSearchOptions.from.length > 0 - ? await queryConversationsAndContacts(advancedSearchOptions.from, options) - : undefined; - filteredMessages = advancedFilterMessages( - filteredMessages, - advancedSearchOptions, - senderFilterQuery?.contacts || [] - ); - } + const filteredMessages = _.compact(messages); + // if (isAdvancedQuery) { + // const senderFilterQuery = + // advancedSearchOptions.from && advancedSearchOptions.from.length > 0 + // ? await queryConversationsAndContacts(advancedSearchOptions.from, options) + // : undefined; + // filteredMessages = advancedFilterMessages( + // filteredMessages, + // advancedSearchOptions, + // senderFilterQuery?.contacts || [] + // ); + // } return { query, normalizedPhoneNumber: PubKey.normalize(query), @@ -120,35 +118,35 @@ export function updateSearchTerm(query: string): UpdateSearchTermActionType { // Helper functions for search -function advancedFilterMessages( - messages: Array, - filters: AdvancedSearchOptions, - contacts: Array -) { - let filteredMessages = messages; - if (filters.from && filters.from.length > 0) { - if (filters.from === '@me') { - filteredMessages = filteredMessages.filter(message => message.sent); - } else { - filteredMessages = []; - for (const contact of contacts) { - for (const message of messages) { - if (message.source === contact) { - filteredMessages.push(message); - } - } - } - } - } - if (filters.before > 0) { - filteredMessages = filteredMessages.filter(message => message.received_at < filters.before); - } - if (filters.after > 0) { - filteredMessages = filteredMessages.filter(message => message.received_at > filters.after); - } - - return filteredMessages; -} +// function advancedFilterMessages( +// messages: Array, +// filters: AdvancedSearchOptions, +// contacts: Array +// ): Array { +// let filteredMessages = messages; +// if (filters.from && filters.from.length > 0) { +// if (filters.from === '@me') { +// filteredMessages = filteredMessages.filter(message => message.sent); +// } else { +// filteredMessages = []; +// for (const contact of contacts) { +// for (const message of messages) { +// if (message.source === contact) { +// filteredMessages.push(message); +// } +// } +// } +// } +// } +// if (filters.before > 0) { +// filteredMessages = filteredMessages.filter(message => message.received_at < filters.before); +// } +// if (filters.after > 0) { +// filteredMessages = filteredMessages.filter(message => message.received_at > filters.after); +// } + +// return filteredMessages; +// } function getUnixMillisecondsTimestamp(timestamp: string): number { const timestampInt = parseInt(timestamp, 10); @@ -198,7 +196,7 @@ function getAdvancedSearchOptionsFromQuery(query: string): AdvancedSearchOptions return filters; } -async function queryMessages(query: string) { +async function queryMessages(query: string): Promise> { try { const normalized = cleanSearchTerm(query); return searchMessages(normalized, 1000); @@ -256,7 +254,6 @@ export const initialSearchState: SearchStateType = { conversations: [], contacts: [], messages: [], - messagesLookup: {}, }; function getEmptyState(): SearchStateType { diff --git a/ts/test/types/Conversation_test.ts b/ts/test/types/Conversation_test.ts deleted file mode 100644 index 884f66e13..000000000 --- a/ts/test/types/Conversation_test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { assert } from 'chai'; -import { LastMessageStatusType } from '../../state/ducks/conversations'; - -import * as Conversation from '../../types/Conversation'; -import { IncomingMessage } from '../../types/Message'; - -describe('Conversation', () => { - describe('createLastMessageUpdate', () => { - it('should reset last message if conversation has no messages', () => { - const input = {}; - const expected = { - lastMessage: '', - lastMessageStatus: undefined, - timestamp: undefined, - }; - - const actual = Conversation.createLastMessageUpdate(input); - assert.deepEqual(actual, expected); - }); - - context('for regular message', () => { - it('should update last message text and timestamp', () => { - const input = { - currentTimestamp: 555, - lastMessageStatus: 'read' as LastMessageStatusType, - lastMessage: { - type: 'outgoing', - conversationId: 'foo', - sent_at: 666, - timestamp: 666, - } as any, - lastMessageNotificationText: 'New outgoing message', - }; - const expected = { - lastMessage: 'New outgoing message', - lastMessageStatus: 'read' as LastMessageStatusType, - timestamp: 666, - }; - - const actual = Conversation.createLastMessageUpdate(input); - assert.deepEqual(actual, expected); - }); - }); - - context('for expire timer update from sync', () => { - it('should update message but not timestamp (to prevent bump to top)', () => { - const input = { - currentTimestamp: 555, - lastMessage: { - type: 'incoming', - conversationId: 'foo', - sent_at: 666, - timestamp: 666, - expirationTimerUpdate: { - expireTimer: 111, - fromSync: true, - source: '+12223334455', - }, - } as IncomingMessage, - lastMessageNotificationText: 'Last message before expired', - }; - const expected = { - lastMessage: 'Last message before expired', - lastMessageStatus: undefined, - timestamp: 555, - }; - - const actual = Conversation.createLastMessageUpdate(input); - assert.deepEqual(actual, expected); - }); - }); - }); -}); diff --git a/ts/test/types/message/initializeAttachmentMetadata_test.ts b/ts/test/types/message/initializeAttachmentMetadata_test.ts deleted file mode 100644 index 207012db4..000000000 --- a/ts/test/types/message/initializeAttachmentMetadata_test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { assert } from 'chai'; - -import * as Message from '../../../../ts/types/message/initializeAttachmentMetadata'; -import { IncomingMessage } from '../../../../ts/types/Message'; -import { SignalService } from '../../../../ts/protobuf'; -import * as MIME from '../../../../ts/types/MIME'; -// @ts-ignore -import { stringToArrayBuffer } from '../../../../js/modules/string_to_array_buffer'; - -describe('Message', () => { - describe('initializeAttachmentMetadata', () => { - it('should classify visual media attachments', async () => { - const input: IncomingMessage = { - type: 'incoming', - conversationId: 'foo', - id: '11111111-1111-1111-1111-111111111111', - timestamp: 1523317140899, - received_at: 1523317140899, - sent_at: 1523317140800, - attachments: [ - { - contentType: MIME.IMAGE_JPEG, - data: stringToArrayBuffer('foo'), - fileName: 'foo.jpg', - size: 1111, - }, - ], - }; - const expected: IncomingMessage = { - type: 'incoming', - conversationId: 'foo', - id: '11111111-1111-1111-1111-111111111111', - timestamp: 1523317140899, - received_at: 1523317140899, - sent_at: 1523317140800, - attachments: [ - { - contentType: MIME.IMAGE_JPEG, - data: stringToArrayBuffer('foo'), - fileName: 'foo.jpg', - size: 1111, - }, - ], - hasAttachments: 1, - hasVisualMediaAttachments: 1, - hasFileAttachments: undefined, - }; - - const actual = await Message.initializeAttachmentMetadata(input); - assert.deepEqual(actual, expected); - }); - - it('should classify file attachments', async () => { - const input: IncomingMessage = { - type: 'incoming', - conversationId: 'foo', - id: '11111111-1111-1111-1111-111111111111', - timestamp: 1523317140899, - received_at: 1523317140899, - sent_at: 1523317140800, - attachments: [ - { - contentType: MIME.APPLICATION_OCTET_STREAM, - data: stringToArrayBuffer('foo'), - fileName: 'foo.bin', - size: 1111, - }, - ], - }; - const expected: IncomingMessage = { - type: 'incoming', - conversationId: 'foo', - id: '11111111-1111-1111-1111-111111111111', - timestamp: 1523317140899, - received_at: 1523317140899, - sent_at: 1523317140800, - attachments: [ - { - contentType: MIME.APPLICATION_OCTET_STREAM, - data: stringToArrayBuffer('foo'), - fileName: 'foo.bin', - size: 1111, - }, - ], - hasAttachments: 1, - hasVisualMediaAttachments: undefined, - hasFileAttachments: 1, - }; - - const actual = await Message.initializeAttachmentMetadata(input); - assert.deepEqual(actual, expected); - }); - - it('should classify voice message attachments', async () => { - const input: IncomingMessage = { - type: 'incoming', - conversationId: 'foo', - id: '11111111-1111-1111-1111-111111111111', - timestamp: 1523317140899, - received_at: 1523317140899, - sent_at: 1523317140800, - attachments: [ - { - contentType: MIME.AUDIO_AAC, - flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, - data: stringToArrayBuffer('foo'), - fileName: 'Voice Message.aac', - size: 1111, - }, - ], - }; - const expected: IncomingMessage = { - type: 'incoming', - conversationId: 'foo', - id: '11111111-1111-1111-1111-111111111111', - timestamp: 1523317140899, - received_at: 1523317140899, - sent_at: 1523317140800, - attachments: [ - { - contentType: MIME.AUDIO_AAC, - flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, - data: stringToArrayBuffer('foo'), - fileName: 'Voice Message.aac', - size: 1111, - }, - ], - hasAttachments: 1, - hasVisualMediaAttachments: undefined, - hasFileAttachments: undefined, - }; - - const actual = await Message.initializeAttachmentMetadata(input); - assert.deepEqual(actual, expected); - }); - }); -}); diff --git a/ts/types/IndexedDB.ts b/ts/types/IndexedDB.ts deleted file mode 100644 index 687606de3..000000000 --- a/ts/types/IndexedDB.ts +++ /dev/null @@ -1,19 +0,0 @@ -// IndexedDB doesn’t support boolean indexes so we map `true` to 1 and `false` -// to `0`, i.e. `IndexableBoolean`. -// N.B. Using `undefined` allows excluding an entry from an index. Useful -// when index size is a consideration or one only needs to query for `true`, -// i.e. `IndexablePresence`. -export type IndexableBoolean = IndexableFalse | IndexableTrue; -export type IndexablePresence = undefined | IndexableTrue; - -type IndexableFalse = 0; -type IndexableTrue = 1; - -export const INDEXABLE_FALSE: IndexableFalse = 0; -export const INDEXABLE_TRUE: IndexableTrue = 1; - -export const toIndexableBoolean = (value: boolean): IndexableBoolean => - value ? INDEXABLE_TRUE : INDEXABLE_FALSE; - -export const toIndexablePresence = (value: boolean): IndexablePresence => - value ? INDEXABLE_TRUE : undefined; diff --git a/ts/types/Message.ts b/ts/types/Message.ts index 487833518..a22bac537 100644 --- a/ts/types/Message.ts +++ b/ts/types/Message.ts @@ -1,5 +1,4 @@ import { Attachment } from './Attachment'; -import { IndexableBoolean, IndexablePresence } from './IndexedDB'; export type Message = UserMessage; export type UserMessage = IncomingMessage; @@ -21,7 +20,6 @@ export type IncomingMessage = Readonly< source?: string; sourceDevice?: number; } & SharedMessageProperties & - MessageSchemaVersion5 & ExpirationTimerUpdate >; @@ -41,14 +39,6 @@ type ExpirationTimerUpdate = Partial< }> >; -type MessageSchemaVersion5 = Partial< - Readonly<{ - hasAttachments: IndexableBoolean; - hasVisualMediaAttachments: IndexablePresence; - hasFileAttachments: IndexablePresence; - }> ->; - export type LokiProfile = { displayName: string; avatarPointer?: string; diff --git a/ts/types/message/initializeAttachmentMetadata.ts b/ts/types/message/initializeAttachmentMetadata.ts index 4ca7c9e61..176dbb148 100644 --- a/ts/types/message/initializeAttachmentMetadata.ts +++ b/ts/types/message/initializeAttachmentMetadata.ts @@ -1,23 +1,25 @@ +import { MessageModel } from '../../models/message'; import * as Attachment from '../Attachment'; -import * as IndexedDB from '../IndexedDB'; -import { Message, UserMessage } from '../Message'; const hasAttachment = (predicate: (value: Attachment.Attachment) => boolean) => ( - message: UserMessage -): IndexedDB.IndexablePresence => - IndexedDB.toIndexablePresence(message.attachments.some(predicate)); + message: MessageModel +): boolean => Boolean((message.get('attachments') || []).some(predicate)); const hasFileAttachment = hasAttachment(Attachment.isFile); const hasVisualMediaAttachment = hasAttachment(Attachment.isVisualMedia); -export const initializeAttachmentMetadata = async (message: Message): Promise => { - const hasAttachments = IndexedDB.toIndexableBoolean(message.attachments.length > 0); - - const hasFileAttachments = hasFileAttachment(message); - const hasVisualMediaAttachments = hasVisualMediaAttachment(message); +export const getAttachmentMetadata = async ( + message: MessageModel +): Promise<{ + hasAttachments: 1 | 0; + hasFileAttachments: 1 | 0; + hasVisualMediaAttachments: 1 | 0; +}> => { + const hasAttachments = Boolean(message.get('attachments').length) ? 1 : 0; + const hasFileAttachments = hasFileAttachment(message) ? 1 : 0; + const hasVisualMediaAttachments = hasVisualMediaAttachment(message) ? 1 : 0; return { - ...message, hasAttachments, hasFileAttachments, hasVisualMediaAttachments,