cleanup MessageSearchResults

pull/2142/head
Audric Ackermann 3 years ago
parent 792c23da87
commit 00d2bbc63d
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -1883,15 +1883,15 @@ function searchMessagesInConversation(query, conversationId, limit) {
const rows = globalInstance
.prepare(
`SELECT
messages.json,
snippet(messages_fts, -1, '<<left>>', '<<right>>', '...', 15) as snippet
FROM messages_fts
INNER JOIN ${MESSAGES_TABLE} on messages_fts.id = messages.id
${MESSAGES_TABLE}.json,
snippet(${MESSAGES_FTS_TABLE}, -1, '<<left>>', '<<right>>', '...', 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,

@ -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

@ -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 (
<div className="module-message-search-result__header__from">
{fromName} {window.i18n('to')}
<span className="module-mesages-search-result__header__group">
<span className="module-messages-search-result__header__group">
<ContactName pubkey={destination} shouldShowPubkey={false} />
</span>
</div>
@ -80,8 +78,7 @@ const From = (props: { source: string; destination: string }) => {
};
const AvatarItem = (props: { source: string }) => {
const { source } = props;
return <Avatar size={AvatarSize.S} pubkey={source} />;
return <Avatar size={AvatarSize.S} pubkey={props.source} />;
};
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 (
<div
key={`div-msg-searchresult-${id}`}
role="button"
onClick={async () => {
await openConversationToSpecificMessage({
onClick={() => {
void openConversationToSpecificMessage({
conversationKey: conversationId,
messageIdToNavigateTo: id,
});
}}
className={classNames('module-message-search-result')}
>
<AvatarItem source={effectiveSource} />
<AvatarItem source={source || me} />
<div className="module-message-search-result__text">
<div className="module-message-search-result__header">
<From source={effectiveSource} destination={effectiveDestination} />
<From source={source} destination={destination} />
<div className="module-message-search-result__header__timestamp">
<Timestamp timestamp={receivedAt} />
<Timestamp timestamp={serverTimestamp || timestamp || sent_at || received_at} />
</div>
</div>
<ResultBody>

@ -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<Array<any>> {
return conversations;
}
export async function searchMessages(query: string, limit: number): Promise<Array<Object>> {
const messages = await channels.searchMessages(query, limit);
export async function searchMessages(
query: string,
limit: number
): Promise<Array<MessageResultProps>> {
const messages = (await channels.searchMessages(query, limit)) as Array<MessageResultProps>;
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<Object> {
const messages = await channels.searchMessagesInConversation(query, conversationId, {
): Promise<Array<MessageAttributes>> {
const messages = (await channels.searchMessagesInConversation(query, conversationId, {
limit,
});
})) as Array<MessageAttributes>;
return messages;
}

@ -766,8 +766,9 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
quote: undefined,
groupInvitation: undefined,
dataExtractionNotification: undefined,
hasAttachments: false,
hasVisualMediaAttachments: false,
hasAttachments: 0,
hasFileAttachments: 0,
hasVisualMediaAttachments: 0,
attachments: undefined,
preview: undefined,
});

@ -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<string>;
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;

@ -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;
}

@ -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<void> {
// 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<void> {
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
);
});
}

@ -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<void> {
async function copyFromQuotedMessage(
msg: MessageModel,
quote?: SignalService.DataMessage.IQuote | null
): Promise<void> {
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;
}

@ -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,
''
);
});
}

@ -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<any>;
text: string;
attachments: Array<any> | null;
text: string | null;
referencedMessageNotFound: boolean;
}

@ -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 });

@ -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<string>;
contacts: Array<string>;
// TODO: ww typing
messages?: Array<any>;
messagesLookup?: any;
messages?: Array<MessageResultProps>;
};
// Actions
@ -30,8 +29,7 @@ type SearchResultsPayloadType = {
normalizedPhoneNumber?: string;
conversations: Array<string>;
contacts: Array<string>;
messages?: Array<Object>;
messages?: Array<MessageResultProps>;
};
type SearchResultsKickoffActionType = {
@ -76,25 +74,25 @@ export function search(query: string, options: SearchOptions): SearchResultsKick
async function doSearch(query: string, options: SearchOptions): Promise<SearchResultsPayloadType> {
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<any>,
filters: AdvancedSearchOptions,
contacts: Array<string>
) {
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<MessageResultProps>,
// filters: AdvancedSearchOptions,
// contacts: Array<string>
// ): Array<MessageResultProps> {
// 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<Array<MessageResultProps>> {
try {
const normalized = cleanSearchTerm(query);
return searchMessages(normalized, 1000);
@ -256,7 +254,6 @@ export const initialSearchState: SearchStateType = {
conversations: [],
contacts: [],
messages: [],
messagesLookup: {},
};
function getEmptyState(): SearchStateType {

@ -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);
});
});
});
});

@ -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);
});
});
});

@ -1,19 +0,0 @@
// IndexedDB doesnt 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;

@ -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;

@ -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<Message> => {
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,

Loading…
Cancel
Save