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 const rows = globalInstance
.prepare( .prepare(
`SELECT `SELECT
messages.json, ${MESSAGES_TABLE}.json,
snippet(messages_fts, -1, '<<left>>', '<<right>>', '...', 15) as snippet snippet(${MESSAGES_FTS_TABLE}, -1, '<<left>>', '<<right>>', '...', 15) as snippet
FROM messages_fts FROM ${MESSAGES_FTS_TABLE}
INNER JOIN ${MESSAGES_TABLE} on messages_fts.id = messages.id INNER JOIN ${MESSAGES_TABLE} on ${MESSAGES_FTS_TABLE}.id = ${MESSAGES_TABLE}.id
WHERE WHERE
messages_fts match $query AND ${MESSAGES_FTS_TABLE} match $query AND
messages.conversationId = $conversationId ${MESSAGES_TABLE}.conversationId = $conversationId
ORDER BY messages.received_at DESC ORDER BY ${MESSAGES_TABLE}.serverTimestamp DESC, ${MESSAGES_TABLE}.serverId DESC, ${MESSAGES_TABLE}.sent_at DESC, ${MESSAGES_TABLE}.received_at DESC
LIMIT $limit;` LIMIT $limit;`
) )
.all({ .all({
query, query,

@ -1475,8 +1475,11 @@
.module-message-search-result__header__name { .module-message-search-result__header__name {
font-weight: 300; font-weight: 300;
} }
.module-mesages-search-result__header__group { .module-messages-search-result__header__group {
font-weight: 300; font-weight: 300;
.module-contact-name {
display: initial;
}
} }
// Module: Left Pane // Module: Left Pane

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { MessageDirection } from '../../models/messageType';
import { getOurPubKeyStrFromCache } from '../../session/utils/User'; import { getOurPubKeyStrFromCache } from '../../session/utils/User';
import { import {
FindAndFormatContactType, FindAndFormatContactType,
@ -12,10 +11,9 @@ import { Avatar, AvatarSize } from '../avatar/Avatar';
import { Timestamp } from '../conversation/Timestamp'; import { Timestamp } from '../conversation/Timestamp';
import { MessageBodyHighlight } from '../basic/MessageBodyHighlight'; import { MessageBodyHighlight } from '../basic/MessageBodyHighlight';
import styled from 'styled-components'; import styled from 'styled-components';
import { MessageAttributes } from '../../models/messageType';
type PropsHousekeeping = { import { useIsPrivate } from '../../hooks/useParamSelector';
isSelected?: boolean; import { UserUtils } from '../../session/utils';
};
export type PropsForSearchResults = { export type PropsForSearchResults = {
from: FindAndFormatContactType; from: FindAndFormatContactType;
@ -30,7 +28,7 @@ export type PropsForSearchResults = {
receivedAt?: number; receivedAt?: number;
}; };
export type MessageResultProps = PropsForSearchResults & PropsHousekeeping; export type MessageResultProps = MessageAttributes & { snippet: string };
const FromName = (props: { source: string; destination: string }) => { const FromName = (props: { source: string; destination: string }) => {
const { source, destination } = props; const { source, destination } = props;
@ -69,7 +67,7 @@ const From = (props: { source: string; destination: string }) => {
return ( return (
<div className="module-message-search-result__header__from"> <div className="module-message-search-result__header__from">
{fromName} {window.i18n('to')} {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} /> <ContactName pubkey={destination} shouldShowPubkey={false} />
</span> </span>
</div> </div>
@ -80,8 +78,7 @@ const From = (props: { source: string; destination: string }) => {
}; };
const AvatarItem = (props: { source: string }) => { const AvatarItem = (props: { source: string }) => {
const { source } = props; return <Avatar size={AvatarSize.S} pubkey={props.source} />;
return <Avatar size={AvatarSize.S} pubkey={source} />;
}; };
const ResultBody = styled.div` const ResultBody = styled.div`
@ -102,45 +99,57 @@ const ResultBody = styled.div`
`; `;
export const MessageSearchResult = (props: MessageResultProps) => { export const MessageSearchResult = (props: MessageResultProps) => {
const { id, conversationId, receivedAt, snippet, destination, source, direction } = props; const {
id,
// Some messages miss a source or destination. Doing checks to see if the fields can be derived from other sources. conversationId,
// E.g. if the source is missing but the message is outgoing, the source will be our pubkey received_at,
const sourceOrDestinationDerivable = snippet,
(destination && direction === MessageDirection.outgoing) || source,
!destination || sent_at,
!source || serverTimestamp,
(source && direction === MessageDirection.incoming); timestamp,
direction,
if (!sourceOrDestinationDerivable) { } = 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; return null;
} }
const effectiveSource =
!source && direction === MessageDirection.outgoing ? getOurPubKeyStrFromCache() : source;
const effectiveDestination =
!destination && direction === MessageDirection.incoming
? getOurPubKeyStrFromCache()
: destination;
return ( return (
<div <div
key={`div-msg-searchresult-${id}`} key={`div-msg-searchresult-${id}`}
role="button" role="button"
onClick={async () => { onClick={() => {
await openConversationToSpecificMessage({ void openConversationToSpecificMessage({
conversationKey: conversationId, conversationKey: conversationId,
messageIdToNavigateTo: id, messageIdToNavigateTo: id,
}); });
}} }}
className={classNames('module-message-search-result')} 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__text">
<div className="module-message-search-result__header"> <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"> <div className="module-message-search-result__header__timestamp">
<Timestamp timestamp={receivedAt} /> <Timestamp timestamp={serverTimestamp || timestamp || sent_at || received_at} />
</div> </div>
</div> </div>
<ResultBody> <ResultBody>

@ -3,6 +3,7 @@ import { ipcRenderer } from 'electron';
// tslint:disable: no-require-imports no-var-requires one-variable-per-declaration no-void-expression // tslint:disable: no-require-imports no-var-requires one-variable-per-declaration no-void-expression
import _ from 'lodash'; import _ from 'lodash';
import { MessageResultProps } from '../components/search/MessageSearchResults';
import { import {
ConversationCollection, ConversationCollection,
ConversationModel, ConversationModel,
@ -587,9 +588,11 @@ export async function searchConversations(query: string): Promise<Array<any>> {
return conversations; return conversations;
} }
export async function searchMessages(query: string, limit: number): Promise<Array<Object>> { export async function searchMessages(
const messages = await channels.searchMessages(query, limit); 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 _.uniqWith(messages, (left: { id: string }, right: { id: string }) => {
return left.id === right.id; return left.id === right.id;
}); });
@ -602,10 +605,10 @@ export async function searchMessagesInConversation(
query: string, query: string,
conversationId: string, conversationId: string,
limit: number limit: number
): Promise<Object> { ): Promise<Array<MessageAttributes>> {
const messages = await channels.searchMessagesInConversation(query, conversationId, { const messages = (await channels.searchMessagesInConversation(query, conversationId, {
limit, limit,
}); })) as Array<MessageAttributes>;
return messages; return messages;
} }

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

@ -30,9 +30,9 @@ export interface MessageAttributes {
conversationId: string; conversationId: string;
errors?: any; errors?: any;
flags?: number; flags?: number;
hasAttachments: boolean; hasAttachments: 1 | 0;
hasFileAttachments: boolean; hasFileAttachments: 1 | 0;
hasVisualMediaAttachments: boolean; hasVisualMediaAttachments: 1 | 0;
expirationTimerUpdate?: { expirationTimerUpdate?: {
expireTimer: number; expireTimer: number;
source: string; source: string;
@ -86,11 +86,7 @@ export interface MessageAttributes {
synced: boolean; synced: boolean;
sync: boolean; sync: boolean;
/** direction: MessageModelType;
* This field is used for search only
*/
snippet?: any;
direction: any;
/** /**
* This is used for when a user screenshots or saves an attachment you sent. * This is used for when a user screenshots or saves an attachment you sent.
@ -176,7 +172,6 @@ export interface MessageAttributesOptionals {
group?: any; group?: any;
timestamp?: number; timestamp?: number;
status?: MessageDeliveryStatus; status?: MessageDeliveryStatus;
dataMessage?: any;
sent_to?: Array<string>; sent_to?: Array<string>;
sent?: boolean; sent?: boolean;
serverId?: number; serverId?: number;
@ -185,8 +180,7 @@ export interface MessageAttributesOptionals {
sentSync?: boolean; sentSync?: boolean;
synced?: boolean; synced?: boolean;
sync?: boolean; sync?: boolean;
snippet?: any; direction?: MessageModelType;
direction?: any;
messageHash?: string; messageHash?: string;
isDeleted?: boolean; isDeleted?: boolean;
callNotificationType?: CallNotificationType; callNotificationType?: CallNotificationType;

@ -360,7 +360,11 @@ export async function innerHandleContentMessage(
content.dataMessage.profileKey = null; content.dataMessage.profileKey = null;
} }
perfStart(`handleDataMessage-${envelope.id}`); perfStart(`handleDataMessage-${envelope.id}`);
await handleDataMessage(envelope, content.dataMessage, messageHash); await handleDataMessage(
envelope,
content.dataMessage as SignalService.DataMessage,
messageHash
);
perfEnd(`handleDataMessage-${envelope.id}`, 'handleDataMessage'); perfEnd(`handleDataMessage-${envelope.id}`, 'handleDataMessage');
return; return;
} }

@ -196,8 +196,6 @@ export async function processDecrypted(
} }
if (decrypted.group) { if (decrypted.group) {
// decrypted.group.id = new TextDecoder('utf-8').decode(decrypted.group.id);
switch (decrypted.group.type) { switch (decrypted.group.type) {
case SignalService.GroupContext.Type.UPDATE: case SignalService.GroupContext.Type.UPDATE:
decrypted.body = ''; decrypted.body = '';
@ -274,32 +272,32 @@ function isBodyEmpty(body: string) {
*/ */
export async function handleDataMessage( export async function handleDataMessage(
envelope: EnvelopePlus, envelope: EnvelopePlus,
dataMessage: SignalService.IDataMessage, rawDataMessage: SignalService.DataMessage,
messageHash: string messageHash: string
): Promise<void> { ): Promise<void> {
// we handle group updates from our other devices in handleClosedGroupControlMessage() // we handle group updates from our other devices in handleClosedGroupControlMessage()
if (dataMessage.closedGroupControlMessage) { if (rawDataMessage.closedGroupControlMessage) {
await handleClosedGroupControlMessage( await handleClosedGroupControlMessage(
envelope, envelope,
dataMessage.closedGroupControlMessage as SignalService.DataMessage.ClosedGroupControlMessage rawDataMessage.closedGroupControlMessage as SignalService.DataMessage.ClosedGroupControlMessage
); );
return; return;
} }
const message = await processDecrypted(envelope, dataMessage); const message = await processDecrypted(envelope, rawDataMessage);
const source = dataMessage.syncTarget || envelope.source; const source = rawDataMessage.syncTarget || envelope.source;
const senderPubKey = envelope.senderIdentity || envelope.source; const senderPubKey = envelope.senderIdentity || envelope.source;
const isMe = UserUtils.isUsFromCache(senderPubKey); const isMe = UserUtils.isUsFromCache(senderPubKey);
const isSyncMessage = Boolean(dataMessage.syncTarget?.length); const isSyncMessage = Boolean(rawDataMessage.syncTarget?.length);
window?.log?.info(`Handle dataMessage from ${source} `); window?.log?.info(`Handle dataMessage from ${source} `);
if (isSyncMessage && !isMe) { if (isSyncMessage && !isMe) {
window?.log?.warn('Got a sync message from someone else than me. Dropping it.'); window?.log?.warn('Got a sync message from someone else than me. Dropping it.');
return removeFromCache(envelope); return removeFromCache(envelope);
} else if (isSyncMessage && dataMessage.syncTarget) { } else if (isSyncMessage && rawDataMessage.syncTarget) {
// override the envelope source // override the envelope source
envelope.source = dataMessage.syncTarget; envelope.source = rawDataMessage.syncTarget;
} }
const senderConversation = await getConversationController().getOrCreateAndWait( 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 confirm = () => removeFromCache(envelope);
const data: MessageCreationData = { const data: MessageCreationData = {
source: senderPubKey, source: senderPubKey,
destination: isMe ? message.syncTarget : envelope.source, destination: isMe ? message.syncTarget : envelope.source,
sourceDevice: 1,
timestamp: _.toNumber(envelope.timestamp), timestamp: _.toNumber(envelope.timestamp),
receivedAt: envelope.receivedAt, receivedAt: envelope.receivedAt,
message,
messageHash, messageHash,
isPublic: false, isPublic: false,
serverId: null, serverId: null,
serverTimestamp: 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 = { export type MessageId = {
source: string; source: string;
serverId?: number | null; serverId?: number | null;
serverTimestamp?: number | null; serverTimestamp?: number | null;
sourceDevice: number;
timestamp: number; timestamp: number;
message: MessageDuplicateSearchType;
}; };
const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
export async function isMessageDuplicate({ source, timestamp, serverTimestamp }: MessageId) {
export async function isMessageDuplicate({
source,
timestamp,
message,
serverTimestamp,
}: MessageId) {
// serverTimestamp is only used for opengroupv2 // serverTimestamp is only used for opengroupv2
try { try {
let result; let result;
@ -392,33 +380,13 @@ export async function isMessageDuplicate({
sentAt: timestamp, sentAt: timestamp,
}); });
if (!result) { return Boolean(result);
return false;
}
const filteredResult = [result].filter((m: any) => m.attributes.body === message.body);
return filteredResult.some(m => isDuplicate(m, message, source));
} catch (error) { } catch (error) {
window?.log?.error('isMessageDuplicate error:', toLogFormat(error)); window?.log?.error('isMessageDuplicate error:', toLogFormat(error));
return false; 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( async function handleProfileUpdate(
profileKeyBuffer: Uint8Array, profileKeyBuffer: Uint8Array,
convoId: string, convoId: string,
@ -432,25 +400,24 @@ async function handleProfileUpdate(
// Will do the save for us if needed // Will do the save for us if needed
await me.setProfileKey(profileKeyBuffer); await me.setProfileKey(profileKeyBuffer);
} else { } else {
const sender = await getConversationController().getOrCreateAndWait( const senderConvo = await getConversationController().getOrCreateAndWait(
convoId, convoId,
ConversationTypeEnum.PRIVATE ConversationTypeEnum.PRIVATE
); );
// Will do the save for us // Will do the save for us
await sender.setProfileKey(profileKeyBuffer); await senderConvo.setProfileKey(profileKeyBuffer);
} }
} }
export type MessageCreationData = { export type MessageCreationData = {
timestamp: number; timestamp: number;
receivedAt: number; receivedAt: number;
sourceDevice: number; // always 1 for Session
source: string; source: string;
message: any;
isPublic: boolean; isPublic: boolean;
serverId: number | null; serverId: number | null;
serverTimestamp: number | null; serverTimestamp: number | null;
groupId: string | null;
// Needed for synced outgoing messages // Needed for synced outgoing messages
expirationStartTimestamp?: any; // ??? expirationStartTimestamp?: any; // ???
@ -463,24 +430,15 @@ export function initIncomingMessage(data: MessageCreationData): MessageModel {
timestamp, timestamp,
isPublic, isPublic,
receivedAt, receivedAt,
sourceDevice,
source, source,
serverId, serverId,
message,
serverTimestamp, serverTimestamp,
messageHash, messageHash,
groupId,
} = data; } = 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 = { const messageData: any = {
source, source,
sourceDevice,
serverId, serverId,
sent_at: timestamp, sent_at: timestamp,
serverTimestamp, serverTimestamp,
@ -505,10 +463,9 @@ function createSentMessage(data: MessageCreationData): MessageModel {
serverId, serverId,
isPublic, isPublic,
receivedAt, receivedAt,
sourceDevice,
expirationStartTimestamp, expirationStartTimestamp,
destination, destination,
message, groupId,
messageHash, messageHash,
} = data; } = data;
@ -518,16 +475,8 @@ function createSentMessage(data: MessageCreationData): MessageModel {
expirationStartTimestamp: Math.min(expirationStartTimestamp || data.timestamp || now, now), 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 = { const messageData = {
source: UserUtils.getOurPubKeyStrFromCache(), source: UserUtils.getOurPubKeyStrFromCache(),
sourceDevice,
serverTimestamp: serverTimestamp || undefined, serverTimestamp: serverTimestamp || undefined,
serverId: serverId || undefined, serverId: serverId || undefined,
sent_at: timestamp, sent_at: timestamp,
@ -553,22 +502,23 @@ export function createMessage(data: MessageCreationData, isIncoming: boolean): M
// tslint:disable:cyclomatic-complexity max-func-body-length */ // tslint:disable:cyclomatic-complexity max-func-body-length */
async function handleMessageEvent( async function handleMessageEvent(
messageEventType: 'sent' | 'message', messageEventType: 'sent' | 'message',
data: MessageCreationData, messageCreationData: MessageCreationData,
rawDataMessage: SignalService.DataMessage,
confirm: () => void confirm: () => void
): Promise<void> { ): Promise<void> {
const isIncoming = messageEventType === 'message'; const isIncoming = messageEventType === 'message';
if (!data || !data.message) { if (!messageCreationData || !rawDataMessage) {
window?.log?.warn('Invalid data passed to handleMessageEvent.', event); window?.log?.warn('Invalid data passed to handleMessageEvent.', event);
confirm(); confirm();
return; 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; const type = isGroupMessage ? ConversationTypeEnum.GROUP : ConversationTypeEnum.PRIVATE;
@ -578,11 +528,11 @@ async function handleMessageEvent(
confirm(); confirm();
return; return;
} }
if (message.profileKey?.length) { if (rawDataMessage.profileKey?.length) {
await handleProfileUpdate(message.profileKey, conversationId, isIncoming); 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) // if the message is `sent` (from secondary device) we have to set the sender manually... (at least for now)
source = source || msg.get('source'); source = source || msg.get('source');
@ -593,9 +543,11 @@ async function handleMessageEvent(
// - group.id if it is a group message // - group.id if it is a group message
if (isGroupMessage) { if (isGroupMessage) {
// remove the prefix from the source object so this is correct for all other // 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) { if (!conversationId) {
@ -605,7 +557,7 @@ async function handleMessageEvent(
// ========================================= // =========================================
if (!isGroupMessage && source !== ourNumber) { if (!rawDataMessage.group && source !== ourNumber) {
// Ignore auth from our devices // Ignore auth from our devices
conversationId = source; conversationId = source;
} }
@ -619,11 +571,19 @@ async function handleMessageEvent(
} }
void conversation.queueJob(async () => { void conversation.queueJob(async () => {
if (await isMessageDuplicate(data)) { if (await isMessageDuplicate(messageCreationData)) {
window?.log?.info('Received duplicate message. Dropping it.'); window?.log?.info('Received duplicate message. Dropping it.');
confirm(); confirm();
return; 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 { getMessageById, getMessagesBySentAt } from '../../ts/data/data';
import { MessageModelPropsWithoutConvoProps, messagesAdded } from '../state/ducks/conversations'; import { MessageModelPropsWithoutConvoProps, messagesAdded } from '../state/ducks/conversations';
import { updateProfileOneAtATime } from './dataMessage'; import { updateProfileOneAtATime } from './dataMessage';
import Long from 'long'; import { SignalService } from '../protobuf';
function contentTypeSupported(type: string): boolean { function contentTypeSupported(type: string): boolean {
const Chrome = window.Signal.Util.GoogleChrome; const Chrome = window.Signal.Util.GoogleChrome;
@ -17,15 +17,26 @@ function contentTypeSupported(type: string): boolean {
} }
// tslint:disable-next-line: cyclomatic-complexity // 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) { if (!quote) {
return; return;
} }
const { attachments, id: quoteId, author } = quote; 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 // 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. // 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) { if (!found) {
window?.log?.warn(`We did not found quoted message ${id}.`); window?.log?.warn(`We did not found quoted message ${id}.`);
quote.referencedMessageNotFound = true; quoteLocal.referencedMessageNotFound = true;
msg.set({ quote }); msg.set({ quote: quoteLocal });
await msg.commit(); await msg.commit();
return; return;
} }
window?.log?.info(`Found quoted message id: ${id}`); 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; 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) { function handleLinkPreviews(messageBody: string, messagePreview: any, message: MessageModel) {
@ -172,33 +195,28 @@ async function handleSyncedReceipts(message: MessageModel, conversation: Convers
async function handleRegularMessage( async function handleRegularMessage(
conversation: ConversationModel, conversation: ConversationModel,
message: MessageModel, message: MessageModel,
initialMessage: any, rawDataMessage: SignalService.DataMessage,
source: string, source: string,
ourNumber: string, ourNumber: string,
messageHash: string messageHash: string
) { ) {
const type = message.get('type'); const type = message.get('type');
await copyFromQuotedMessage(message, initialMessage.quote); await copyFromQuotedMessage(message, rawDataMessage.quote);
const dataMessage = initialMessage;
const now = Date.now(); const now = Date.now();
if (dataMessage.openGroupInvitation) { if (rawDataMessage.openGroupInvitation) {
message.set({ groupInvitation: dataMessage.openGroupInvitation }); message.set({ groupInvitation: rawDataMessage.openGroupInvitation });
} }
handleLinkPreviews(dataMessage.body, dataMessage.preview, message); handleLinkPreviews(rawDataMessage.body, rawDataMessage.preview, message);
const existingExpireTimer = conversation.get('expireTimer'); const existingExpireTimer = conversation.get('expireTimer');
message.set({ message.set({
flags: dataMessage.flags, flags: rawDataMessage.flags,
hasAttachments: dataMessage.hasAttachments, quote: rawDataMessage.quote,
hasFileAttachments: dataMessage.hasFileAttachments, attachments: rawDataMessage.attachments,
hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments, body: rawDataMessage.body,
quote: dataMessage.quote,
attachments: dataMessage.attachments,
body: dataMessage.body,
conversationId: conversation.id, conversationId: conversation.id,
decrypted_at: now, decrypted_at: now,
messageHash, messageHash,
@ -245,16 +263,16 @@ async function handleRegularMessage(
// Check if we need to update any profile names // Check if we need to update any profile names
// the only profile we don't update with what is coming here is ours, // 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 // as our profile is shared accross our devices with a ConfigurationMessage
if (type === 'incoming' && dataMessage.profile) { if (type === 'incoming' && rawDataMessage.profile) {
void updateProfileOneAtATime( void updateProfileOneAtATime(
sendingDeviceConversation, sendingDeviceConversation,
dataMessage.profile, rawDataMessage.profile,
dataMessage.profileKey rawDataMessage.profileKey
); );
} }
if (dataMessage.profileKey) { if (rawDataMessage.profileKey) {
await processProfileKey(conversation, sendingDeviceConversation, dataMessage.profileKey); await processProfileKey(conversation, sendingDeviceConversation, rawDataMessage.profileKey);
} }
// we just received a message from that user so we reset the typing indicator for this convo // 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( export async function handleMessageJob(
message: MessageModel, messageModel: MessageModel,
conversation: ConversationModel, conversation: ConversationModel,
initialMessage: any, rawDataMessage: SignalService.DataMessage,
ourNumber: string, ourNumber: string,
confirm: () => void, confirm: () => void,
source: string, source: string,
messageHash: string messageHash: string
) { ) {
window?.log?.info( window?.log?.info(
`Starting handleDataMessage for message ${message.idForLogging()}, ${message.get( `Starting handleDataMessage for message ${messageModel.idForLogging()}, ${messageModel.get(
'serverTimestamp' 'serverTimestamp'
) || message.get('timestamp')} in conversation ${conversation.idForLogging()}` ) || messageModel.get('timestamp')} in conversation ${conversation.idForLogging()}`
); );
try { try {
message.set({ flags: initialMessage.flags }); messageModel.set({ flags: rawDataMessage.flags });
if (message.isExpirationTimerUpdate()) { if (messageModel.isExpirationTimerUpdate()) {
const { expireTimer } = initialMessage; const { expireTimer } = rawDataMessage;
const oldValue = conversation.get('expireTimer'); const oldValue = conversation.get('expireTimer');
if (expireTimer === oldValue) { if (expireTimer === oldValue) {
if (confirm) { confirm?.();
confirm();
}
window?.log?.info( window?.log?.info(
'Dropping ExpireTimerUpdate message as we already have the same one set.' 'Dropping ExpireTimerUpdate message as we already have the same one set.'
); );
return; return;
} }
await handleExpirationTimerUpdate(conversation, message, source, expireTimer); await handleExpirationTimerUpdate(conversation, messageModel, source, expireTimer);
} else { } else {
await handleRegularMessage( await handleRegularMessage(
conversation, conversation,
message, messageModel,
initialMessage, rawDataMessage,
source, source,
ourNumber, ourNumber,
messageHash 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 // 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 // call it after we have an id for this message, because the jobs refer back
// to their source message. // to their source message.
void queueAttachmentDownloads(message, conversation); void queueAttachmentDownloads(messageModel, conversation);
const unreadCount = await conversation.getUnreadCount(); const unreadCount = await conversation.getUnreadCount();
conversation.set({ unreadCount }); conversation.set({ unreadCount });
@ -349,37 +365,37 @@ export async function handleMessageJob(
// We go to the database here because, between the message save above and // 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 // the previous line's trigger() call, we might have marked all messages
// unread in the database. This message might already be read! // 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 // 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( window?.log?.warn(
'Caught race condition on new message read state! ' + 'Manually starting timers.' 'Caught race condition on new message read state! ' + 'Manually starting timers.'
); );
// We call markRead() even though the message is already // We call markRead() even though the message is already
// marked read because we need to start expiration // marked read because we need to start expiration
// timers, etc. // timers, etc.
await message.markRead(Date.now()); await messageModel.markRead(Date.now());
} }
} catch (error) { } 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. // this updates the redux store.
// if the convo on which this message should become visible, // 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 // 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, conversationKey: conversation.id,
messageModelProps: message.getMessageModelProps(), messageModelProps: messageModel.getMessageModelProps(),
}); });
throttledAllMessagesAddedDispatch(); throttledAllMessagesAddedDispatch();
if (message.get('unread')) { if (messageModel.get('unread')) {
conversation.throttledNotify(message); conversation.throttledNotify(messageModel);
} }
if (confirm) { if (confirm) {
@ -387,7 +403,7 @@ export async function handleMessageJob(
} }
} catch (error) { } catch (error) {
const errorForLog = error && error.stack ? error.stack : 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; throw error;
} }

@ -285,7 +285,6 @@ export async function handleOpenGroupV2Message(
window?.log?.error('Invalid decoded opengroup message: no dataMessage'); window?.log?.error('Invalid decoded opengroup message: no dataMessage');
return; return;
} }
const dataMessage = idataMessage as SignalService.DataMessage;
if (!getConversationController().get(conversationId)) { if (!getConversationController().get(conversationId)) {
window?.log?.error('Received a message for an unknown convo. Skipping'); 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 // for an opengroupv2 incoming message the serverTimestamp and the timestamp
const messageCreationData: MessageCreationData = { const messageCreationData: MessageCreationData = {
isPublic: true, isPublic: true,
sourceDevice: 1,
serverId, serverId,
serverTimestamp: sentTimestamp, serverTimestamp: sentTimestamp,
receivedAt: Date.now(), receivedAt: Date.now(),
@ -318,9 +316,10 @@ export async function handleOpenGroupV2Message(
timestamp: sentTimestamp, timestamp: sentTimestamp,
expirationStartTimestamp: undefined, expirationStartTimestamp: undefined,
source: sender, source: sender,
message: dataMessage, groupId: null,
messageHash: '', // we do not care of a hash for an opengroup message 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 // WARNING this is important that the isMessageDuplicate is made in the conversation.queueJob
const isDuplicate = await isMessageDuplicate({ ...messageCreationData }); const isDuplicate = await isMessageDuplicate({ ...messageCreationData });
@ -334,6 +333,14 @@ export async function handleOpenGroupV2Message(
const msg = createMessage(messageCreationData, !isMe); const msg = createMessage(messageCreationData, !isMe);
const ourNumber = UserUtils.getOurPubKeyStrFromCache(); 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 { export interface Quote {
id: number; // this is in fact a uint64 so we will have an issue id: number; // this is in fact a uint64 so we will have an issue
author: string; author: string;
attachments: Array<any>; attachments: Array<any> | null;
text: string; text: string | null;
referencedMessageNotFound: boolean; referencedMessageNotFound: boolean;
} }

@ -13,6 +13,7 @@ import {
import { MessageModel } from '../../models/message'; import { MessageModel } from '../../models/message';
import { downloadAttachment, downloadAttachmentOpenGroupV2 } from '../../receiver/attachments'; import { downloadAttachment, downloadAttachmentOpenGroupV2 } from '../../receiver/attachments';
import { initializeAttachmentLogic, processNewAttachment } from '../../types/MessageAttachment'; import { initializeAttachmentLogic, processNewAttachment } from '../../types/MessageAttachment';
import { getAttachmentMetadata } from '../../types/message/initializeAttachmentMetadata';
// this cause issues if we increment that value to > 1. // this cause issues if we increment that value to > 1.
const MAX_ATTACHMENT_JOB_PARALLELISM = 3; const MAX_ATTACHMENT_JOB_PARALLELISM = 3;
@ -212,6 +213,14 @@ async function _runJob(job: any) {
contentType: attachment.contentType, contentType: attachment.contentType,
}); });
found = await getMessageById(messageId); found = await getMessageById(messageId);
if (found) {
const {
hasAttachments,
hasVisualMediaAttachments,
hasFileAttachments,
} = await getAttachmentMetadata(found);
found.set({ hasAttachments, hasVisualMediaAttachments, hasFileAttachments });
}
_addAttachmentToMessage(found, upgradedAttachment, { type, index }); _addAttachmentToMessage(found, upgradedAttachment, { type, index });

@ -7,6 +7,7 @@ import { PubKey } from '../../session/types';
import { ConversationTypeEnum } from '../../models/conversation'; import { ConversationTypeEnum } from '../../models/conversation';
import _ from 'lodash'; import _ from 'lodash';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
import { MessageResultProps } from '../../components/search/MessageSearchResults';
// State // State
@ -19,9 +20,7 @@ export type SearchStateType = {
conversations: Array<string>; conversations: Array<string>;
contacts: Array<string>; contacts: Array<string>;
// TODO: ww typing messages?: Array<MessageResultProps>;
messages?: Array<any>;
messagesLookup?: any;
}; };
// Actions // Actions
@ -30,8 +29,7 @@ type SearchResultsPayloadType = {
normalizedPhoneNumber?: string; normalizedPhoneNumber?: string;
conversations: Array<string>; conversations: Array<string>;
contacts: Array<string>; contacts: Array<string>;
messages?: Array<MessageResultProps>;
messages?: Array<Object>;
}; };
type SearchResultsKickoffActionType = { type SearchResultsKickoffActionType = {
@ -76,25 +74,25 @@ export function search(query: string, options: SearchOptions): SearchResultsKick
async function doSearch(query: string, options: SearchOptions): Promise<SearchResultsPayloadType> { async function doSearch(query: string, options: SearchOptions): Promise<SearchResultsPayloadType> {
const advancedSearchOptions = getAdvancedSearchOptionsFromQuery(query); const advancedSearchOptions = getAdvancedSearchOptionsFromQuery(query);
const processedQuery = advancedSearchOptions.query; const processedQuery = advancedSearchOptions.query;
const isAdvancedQuery = query !== processedQuery; // const isAdvancedQuery = query !== processedQuery;
const [discussions, messages] = await Promise.all([ const [discussions, messages] = await Promise.all([
queryConversationsAndContacts(processedQuery, options), queryConversationsAndContacts(processedQuery, options),
queryMessages(processedQuery), queryMessages(processedQuery),
]); ]);
const { conversations, contacts } = discussions; const { conversations, contacts } = discussions;
let filteredMessages = _.compact(messages); const filteredMessages = _.compact(messages);
if (isAdvancedQuery) { // if (isAdvancedQuery) {
const senderFilterQuery = // const senderFilterQuery =
advancedSearchOptions.from && advancedSearchOptions.from.length > 0 // advancedSearchOptions.from && advancedSearchOptions.from.length > 0
? await queryConversationsAndContacts(advancedSearchOptions.from, options) // ? await queryConversationsAndContacts(advancedSearchOptions.from, options)
: undefined; // : undefined;
filteredMessages = advancedFilterMessages( // filteredMessages = advancedFilterMessages(
filteredMessages, // filteredMessages,
advancedSearchOptions, // advancedSearchOptions,
senderFilterQuery?.contacts || [] // senderFilterQuery?.contacts || []
); // );
} // }
return { return {
query, query,
normalizedPhoneNumber: PubKey.normalize(query), normalizedPhoneNumber: PubKey.normalize(query),
@ -120,35 +118,35 @@ export function updateSearchTerm(query: string): UpdateSearchTermActionType {
// Helper functions for search // Helper functions for search
function advancedFilterMessages( // function advancedFilterMessages(
messages: Array<any>, // messages: Array<MessageResultProps>,
filters: AdvancedSearchOptions, // filters: AdvancedSearchOptions,
contacts: Array<string> // contacts: Array<string>
) { // ): Array<MessageResultProps> {
let filteredMessages = messages; // let filteredMessages = messages;
if (filters.from && filters.from.length > 0) { // if (filters.from && filters.from.length > 0) {
if (filters.from === '@me') { // if (filters.from === '@me') {
filteredMessages = filteredMessages.filter(message => message.sent); // filteredMessages = filteredMessages.filter(message => message.sent);
} else { // } else {
filteredMessages = []; // filteredMessages = [];
for (const contact of contacts) { // for (const contact of contacts) {
for (const message of messages) { // for (const message of messages) {
if (message.source === contact) { // if (message.source === contact) {
filteredMessages.push(message); // filteredMessages.push(message);
} // }
} // }
} // }
} // }
} // }
if (filters.before > 0) { // if (filters.before > 0) {
filteredMessages = filteredMessages.filter(message => message.received_at < filters.before); // filteredMessages = filteredMessages.filter(message => message.received_at < filters.before);
} // }
if (filters.after > 0) { // if (filters.after > 0) {
filteredMessages = filteredMessages.filter(message => message.received_at > filters.after); // filteredMessages = filteredMessages.filter(message => message.received_at > filters.after);
} // }
return filteredMessages; // return filteredMessages;
} // }
function getUnixMillisecondsTimestamp(timestamp: string): number { function getUnixMillisecondsTimestamp(timestamp: string): number {
const timestampInt = parseInt(timestamp, 10); const timestampInt = parseInt(timestamp, 10);
@ -198,7 +196,7 @@ function getAdvancedSearchOptionsFromQuery(query: string): AdvancedSearchOptions
return filters; return filters;
} }
async function queryMessages(query: string) { async function queryMessages(query: string): Promise<Array<MessageResultProps>> {
try { try {
const normalized = cleanSearchTerm(query); const normalized = cleanSearchTerm(query);
return searchMessages(normalized, 1000); return searchMessages(normalized, 1000);
@ -256,7 +254,6 @@ export const initialSearchState: SearchStateType = {
conversations: [], conversations: [],
contacts: [], contacts: [],
messages: [], messages: [],
messagesLookup: {},
}; };
function getEmptyState(): SearchStateType { 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 { Attachment } from './Attachment';
import { IndexableBoolean, IndexablePresence } from './IndexedDB';
export type Message = UserMessage; export type Message = UserMessage;
export type UserMessage = IncomingMessage; export type UserMessage = IncomingMessage;
@ -21,7 +20,6 @@ export type IncomingMessage = Readonly<
source?: string; source?: string;
sourceDevice?: number; sourceDevice?: number;
} & SharedMessageProperties & } & SharedMessageProperties &
MessageSchemaVersion5 &
ExpirationTimerUpdate ExpirationTimerUpdate
>; >;
@ -41,14 +39,6 @@ type ExpirationTimerUpdate = Partial<
}> }>
>; >;
type MessageSchemaVersion5 = Partial<
Readonly<{
hasAttachments: IndexableBoolean;
hasVisualMediaAttachments: IndexablePresence;
hasFileAttachments: IndexablePresence;
}>
>;
export type LokiProfile = { export type LokiProfile = {
displayName: string; displayName: string;
avatarPointer?: string; avatarPointer?: string;

@ -1,23 +1,25 @@
import { MessageModel } from '../../models/message';
import * as Attachment from '../Attachment'; import * as Attachment from '../Attachment';
import * as IndexedDB from '../IndexedDB';
import { Message, UserMessage } from '../Message';
const hasAttachment = (predicate: (value: Attachment.Attachment) => boolean) => ( const hasAttachment = (predicate: (value: Attachment.Attachment) => boolean) => (
message: UserMessage message: MessageModel
): IndexedDB.IndexablePresence => ): boolean => Boolean((message.get('attachments') || []).some(predicate));
IndexedDB.toIndexablePresence(message.attachments.some(predicate));
const hasFileAttachment = hasAttachment(Attachment.isFile); const hasFileAttachment = hasAttachment(Attachment.isFile);
const hasVisualMediaAttachment = hasAttachment(Attachment.isVisualMedia); const hasVisualMediaAttachment = hasAttachment(Attachment.isVisualMedia);
export const initializeAttachmentMetadata = async (message: Message): Promise<Message> => { export const getAttachmentMetadata = async (
const hasAttachments = IndexedDB.toIndexableBoolean(message.attachments.length > 0); message: MessageModel
): Promise<{
const hasFileAttachments = hasFileAttachment(message); hasAttachments: 1 | 0;
const hasVisualMediaAttachments = hasVisualMediaAttachment(message); 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 { return {
...message,
hasAttachments, hasAttachments,
hasFileAttachments, hasFileAttachments,
hasVisualMediaAttachments, hasVisualMediaAttachments,

Loading…
Cancel
Save