diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 45580ed3f..916bfda27 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -460,5 +460,7 @@ "callMediaPermissionsDialogContent": "The current implementation of voice/video calls will expose your IP address to the Oxen Foundation servers and the calling/called user.", "menuCall": "Call", "startedACall": "You called $name$", - "answeredACall": "Call with $name$" + "answeredACall": "Call with $name$", + "trimDatabase": "Trim Database", + "trimDatabaseDescription": "Reduces your message database size to your last 10,000 messages." } diff --git a/app/sql.js b/app/sql.js index d4b20e0db..b45e9a9be 100644 --- a/app/sql.js +++ b/app/sql.js @@ -73,6 +73,8 @@ module.exports = { getMessagesByConversation, getFirstUnreadMessageIdInConversation, hasConversationOutgoingMessage, + trimMessages, + fillWithTestData, getUnprocessedCount, getAllUnprocessed, @@ -836,6 +838,8 @@ const LOKI_SCHEMA_VERSIONS = [ updateToLokiSchemaVersion15, updateToLokiSchemaVersion16, updateToLokiSchemaVersion17, + updateToLokiSchemaVersion18, + updateToLokiSchemaVersion19, ]; function updateToLokiSchemaVersion1(currentVersion, db) { @@ -1251,6 +1255,86 @@ function updateToLokiSchemaVersion17(currentVersion, db) { console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); } +function updateToLokiSchemaVersion18(currentVersion, db) { + const targetVersion = 18; + if (currentVersion >= targetVersion) { + return; + } + console.log(`updateToLokiSchemaVersion${targetVersion}: starting...`); + + // Dropping all pre-existing schema relating to message searching. + // Recreating the full text search and related triggers + db.transaction(() => { + db.exec(` + DROP TRIGGER IF EXISTS messages_on_insert; + DROP TRIGGER IF EXISTS messages_on_delete; + DROP TRIGGER IF EXISTS messages_on_update; + DROP TABLE IF EXISTS ${MESSAGES_FTS_TABLE}; + `); + + writeLokiSchemaVersion(targetVersion, db); + })(); + + db.transaction(() => { + db.exec(` + -- Then we create our full-text search table and populate it + CREATE VIRTUAL TABLE ${MESSAGES_FTS_TABLE} + USING fts5(id UNINDEXED, body); + INSERT INTO ${MESSAGES_FTS_TABLE}(id, body) + SELECT id, body FROM ${MESSAGES_TABLE}; + -- Then we set up triggers to keep the full-text search table up to date + CREATE TRIGGER messages_on_insert AFTER INSERT ON ${MESSAGES_TABLE} BEGIN + INSERT INTO ${MESSAGES_FTS_TABLE} ( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + CREATE TRIGGER messages_on_delete AFTER DELETE ON ${MESSAGES_TABLE} BEGIN + DELETE FROM ${MESSAGES_FTS_TABLE} WHERE id = old.id; + END; + CREATE TRIGGER messages_on_update AFTER UPDATE ON ${MESSAGES_TABLE} BEGIN + DELETE FROM ${MESSAGES_FTS_TABLE} WHERE id = old.id; + INSERT INTO ${MESSAGES_FTS_TABLE}( + id, + body + ) VALUES ( + new.id, + new.body + ); + END; + `); + + writeLokiSchemaVersion(targetVersion, db); + })(); + console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); +} + +function updateToLokiSchemaVersion19(currentVersion, db) { + const targetVersion = 19; + if (currentVersion >= targetVersion) { + return; + } + console.log(`updateToLokiSchemaVersion${targetVersion}: starting...`); + + db.transaction(() => { + db.exec(` + DROP INDEX messages_schemaVersion; + ALTER TABLE ${MESSAGES_TABLE} DROP COLUMN schemaVersion; + `); + // this is way to slow for now... + // db.exec(` + // UPDATE ${MESSAGES_TABLE} SET + // json = json_remove(json, '$.schemaVersion') + // `); + writeLokiSchemaVersion(targetVersion, db); + })(); + + console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); +} + function writeLokiSchemaVersion(newVersion, db) { db.prepare( `INSERT INTO loki_schema( @@ -1841,7 +1925,6 @@ function saveMessage(data) { serverTimestamp, // eslint-disable-next-line camelcase received_at, - schemaVersion, sent, // eslint-disable-next-line camelcase sent_at, @@ -1876,7 +1959,6 @@ function saveMessage(data) { hasFileAttachments, hasVisualMediaAttachments, received_at, - schemaVersion, sent, sent_at, source, @@ -1901,7 +1983,6 @@ function saveMessage(data) { hasFileAttachments, hasVisualMediaAttachments, received_at, - schemaVersion, sent, sent_at, source, @@ -1922,7 +2003,6 @@ function saveMessage(data) { $hasFileAttachments, $hasVisualMediaAttachments, $received_at, - $schemaVersion, $sent, $sent_at, $source, @@ -2220,6 +2300,34 @@ function getFirstUnreadMessageIdInConversation(conversationId) { return rows[0].id; } +/** + * Deletes all but the 10,000 last received messages. + */ +function trimMessages() { + globalInstance + .prepare( + ` + DELETE FROM ${MESSAGES_TABLE} + WHERE id NOT IN ( + SELECT id FROM ${MESSAGES_TABLE} + ORDER BY received_at DESC + LIMIT 10000 + ); + ` + ) + .run(); + + const rows = globalInstance + .prepare( + `SELECT * FROM ${MESSAGES_TABLE} + ORDER BY received_at DESC;` + ) + .all(); + + return rows; + // return map(rows, row => jsonToObject(row.json)); +} + function getMessagesBySentAt(sentAt) { const rows = globalInstance .prepare( @@ -2921,3 +3029,139 @@ function removeOneOpenGroupV1Message() { return toRemoveCount - 1; } + +/** + * Only using this for development. Populate conversation and message tables. + * @param {*} numConvosToAdd + * @param {*} numMsgsToAdd + */ +function fillWithTestData(numConvosToAdd, numMsgsToAdd) { + const convoBeforeCount = globalInstance + .prepare(`SELECT count(*) from ${CONVERSATIONS_TABLE};`) + .get()['count(*)']; + + const lipsum = + // eslint:disable-next-line max-line-length + `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis ac ornare lorem, + non suscipit purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Suspendisse cursus aliquet velit a dignissim. Integer at nisi sed velit consequat + dictum. Phasellus congue tellus ante. Ut rutrum hendrerit dapibus. Fusce + luctus, ante nec interdum molestie, purus urna volutpat turpis, eget mattis + lectus velit at velit. Praesent vel tellus turpis. Praesent eget purus at + nisl blandit pharetra. Cras dapibus sem vitae rutrum dapibus. Vivamus vitae mi + ante. Donec aliquam porta nibh, vel scelerisque orci condimentum sed. + Proin in mattis ipsum, ac euismod sem. Donec malesuada sem nisl, at + vehicula ante efficitur sed. Curabitur in sapien eros. Morbi tempor ante ut + metus scelerisque condimentum. Integer sit amet tempus nulla. Vivamus + imperdiet dui ac luctus vulputate. Sed a accumsan risus. Nulla facilisi. + Nulla mauris dui, luctus in sagittis at, sodales id mauris. Integer efficitur + viverra ex, ut dignissim eros tincidunt placerat. Sed facilisis gravida + mauris in luctus . Fusce dapibus, est vitae tincidunt eleifend, justo + odio porta dui, sed ultrices mi arcu vitae ante. Mauris ut libero + erat. Nam ut mi quis ante tincidunt facilisis sit amet id enim. + Vestibulum in molestie mi. In ac felis est. Vestibulum vel blandit ex. Morbi vitae + viverra augue . Ut turpis quam, cursus quis ex a, convallis + ullamcorper purus. Nam eget libero arcu. Integer fermentum enim nunc, non consequat urna + fermentum condimentum. Nulla vitae malesuada est. Donec imperdiet tortor interdum + malesuada feugiat. Integer pulvinar dui ex, eget tristique arcu mattis at. Nam eu neque + eget mauris varius suscipit. Quisque ac enim vitae mauris laoreet congue nec sed + justo. Curabitur fermentum quam eget est tincidunt, at faucibus lacus maximus. Donec + auctor enim dolor, faucibus egestas diam consectetur sed. Donec eget rutrum arcu, at + tempus mi. Fusce quis volutpat sapien. In aliquet fringilla purus. Ut eu nunc non + augue lacinia ultrices at eget tortor. Maecenas pulvinar odio sit amet purus + elementum, a vehicula lorem maximus. Pellentesque eu lorem magna. Vestibulum ut facilisis + lorem. Proin et enim cursus, vulputate neque sit amet, posuere enim. Praesent + faucibus tellus vel mi tincidunt, nec malesuada nibh malesuada. In laoreet sapien vitae + aliquet sollicitudin. + `; + + const msgBeforeCount = globalInstance.prepare(`SELECT count(*) from ${MESSAGES_TABLE};`).get()[ + 'count(*)' + ]; + + console.warn('==== fillWithTestData ===='); + console.warn({ + convoBeforeCount, + msgBeforeCount, + convoToAdd: numConvosToAdd, + msgToAdd: numMsgsToAdd, + }); + + const convosIdsAdded = []; + // eslint-disable-next-line no-plusplus + for (let index = 0; index < numConvosToAdd; index++) { + const activeAt = Date.now() - index; + const id = Date.now() - 1000 * index; + const convoObjToAdd = { + active_at: activeAt, + members: [], + profileName: `${activeAt}`, + name: `${activeAt}`, + id: `05${id}`, + type: 'group', + }; + convosIdsAdded.push(id); + try { + saveConversation(convoObjToAdd); + // eslint-disable-next-line no-empty + } catch (e) {} + } + console.warn('convosIdsAdded', convosIdsAdded); + // eslint-disable-next-line no-plusplus + for (let index = 0; index < numMsgsToAdd; index++) { + const activeAt = Date.now() - index; + const id = Date.now() - 1000 * index; + + const lipsumStartIdx = Math.floor(Math.random() * lipsum.length); + const lipsumLength = Math.floor(Math.random() * lipsum.length - lipsumStartIdx); + const fakeBodyText = lipsum.substring(lipsumStartIdx, lipsumStartIdx + lipsumLength); + + const convoId = convosIdsAdded[Math.floor(Math.random() * convosIdsAdded.length)]; + const msgObjToAdd = { + // body: `fake body ${activeAt}`, + body: `fakeMsgIdx-spongebob-${index} ${fakeBodyText} ${activeAt}`, + conversationId: `${convoId}`, + // eslint-disable-next-line camelcase + expires_at: 0, + hasAttachments: 0, + hasFileAttachments: 0, + hasVisualMediaAttachments: 0, + id: `${id}`, + serverId: 0, + serverTimestamp: 0, + // eslint-disable-next-line camelcase + received_at: Date.now(), + sent: 0, + // eslint-disable-next-line camelcase + sent_at: Date.now(), + source: `${convoId}`, + sourceDevice: 1, + type: '%', + unread: 1, + expireTimer: 0, + expirationStartTimestamp: 0, + }; + + if (convoId % 10 === 0) { + console.info('uyo , convoId ', { index, convoId }); + } + + try { + saveMessage(msgObjToAdd); + // eslint-disable-next-line no-empty + } catch (e) { + console.warn(e); + } + } + + const convoAfterCount = globalInstance + .prepare(`SELECT count(*) from ${CONVERSATIONS_TABLE};`) + .get()['count(*)']; + + const msgAfterCount = globalInstance.prepare(`SELECT count(*) from ${MESSAGES_TABLE};`).get()[ + 'count(*)' + ]; + + console.warn({ convoAfterCount, msgAfterCount }); + return convosIdsAdded; +} diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 95ae355e8..b30249321 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1547,7 +1547,7 @@ white-space: nowrap; text-overflow: ellipsis; - color: $color-gray-90; + color: var(--color-text-subtle); } .module-message-search-result__header__timestamp { diff --git a/ts/components/basic/MessageBodyHighlist.tsx b/ts/components/basic/MessageBodyHighlist.tsx new file mode 100644 index 000000000..520cff140 --- /dev/null +++ b/ts/components/basic/MessageBodyHighlist.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { RenderTextCallbackType } from '../../types/Util'; +import { SizeClassType } from '../../util/emoji'; +import { AddNewLines } from '../conversation/AddNewLines'; +import { Emojify } from '../conversation/Emojify'; +import { MessageBody } from '../conversation/message/message-content/MessageBody'; + +interface Props { + text: string; +} + +const renderNewLines: RenderTextCallbackType = ({ text, key }) => ( + +); + +const renderEmoji = ({ + text, + key, + sizeClass, + renderNonEmoji, +}: { + text: string; + key: number; + sizeClass?: SizeClassType; + renderNonEmoji: RenderTextCallbackType; +}) => ; + +export class MessageBodyHighlight extends React.Component { + public render() { + const { text } = this.props; + const results: Array = []; + const FIND_BEGIN_END = /<>(.+?)<>/g; + + let match = FIND_BEGIN_END.exec(text); + let last = 0; + let count = 1; + + if (!match) { + return ; + } + + const sizeClass = ''; + + while (match) { + if (last < match.index) { + const beforeText = text.slice(last, match.index); + results.push( + renderEmoji({ + text: beforeText, + sizeClass, + key: count++, + renderNonEmoji: renderNewLines, + }) + ); + } + + const [, toHighlight] = match; + results.push( + + {renderEmoji({ + text: toHighlight, + sizeClass, + key: count++, + renderNonEmoji: renderNewLines, + })} + + ); + + // @ts-ignore + last = FIND_BEGIN_END.lastIndex; + match = FIND_BEGIN_END.exec(text); + } + + if (last < text.length) { + results.push( + renderEmoji({ + text: text.slice(last), + sizeClass, + key: count++, + renderNonEmoji: renderNewLines, + }) + ); + } + + return results; + } +} diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index f906a91cf..1a58b0666 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -2,6 +2,7 @@ import React from 'react'; import classNames from 'classnames'; import { Emojify } from './Emojify'; +import { useConversationUsername } from '../../hooks/useParamSelector'; type Props = { pubkey: string; @@ -17,13 +18,15 @@ export const ContactName = (props: Props) => { const { pubkey, name, profileName, module, boldProfileName, compact, shouldShowPubkey } = props; const prefix = module ? module : 'module-contact-name'; - const shouldShowProfile = Boolean(profileName || name); + const convoName = useConversationUsername(pubkey); + + const shouldShowProfile = Boolean(convoName || profileName || name); const styles = (boldProfileName ? { fontWeight: 'bold', } : {}) as React.CSSProperties; - const textProfile = profileName || name || window.i18n('anonymous'); + const textProfile = profileName || name || convoName || window.i18n('anonymous'); const profileElement = shouldShowProfile ? ( diff --git a/ts/components/search/MessageSearchResults.tsx b/ts/components/search/MessageSearchResults.tsx new file mode 100644 index 000000000..4f52ee20e --- /dev/null +++ b/ts/components/search/MessageSearchResults.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { MessageDirection } from '../../models/messageType'; +import { getOurPubKeyStrFromCache } from '../../session/utils/User'; +import { + FindAndFormatContactType, + openConversationWithMessages, +} from '../../state/ducks/conversations'; +import { ContactName } from '../conversation/ContactName'; +import { Avatar, AvatarSize } from '../avatar/Avatar'; +import { Timestamp } from '../conversation/Timestamp'; +import { MessageBodyHighlight } from '../basic/MessageBodyHighlist'; + +type PropsHousekeeping = { + isSelected?: boolean; +}; + +export type PropsForSearchResults = { + from: FindAndFormatContactType; + to: FindAndFormatContactType; + id: string; + conversationId: string; + destination: string; + source: string; + + direction?: string; + snippet?: string; //not sure about the type of snippet + receivedAt?: number; +}; + +type Props = PropsForSearchResults & PropsHousekeeping; + +const FromName = (props: { source: string; destination: string }) => { + const { source, destination } = props; + + const isNoteToSelf = destination === getOurPubKeyStrFromCache() && source === destination; + + if (isNoteToSelf) { + return ( + + {window.i18n('noteToSelf')} + + ); + } + + if (source === getOurPubKeyStrFromCache()) { + return {window.i18n('you')}; + } + + return ( + // tslint:disable: use-simple-attributes + + ); +}; + +const From = (props: { source: string; destination: string }) => { + const { source, destination } = props; + const fromName = ; + + const ourKey = getOurPubKeyStrFromCache(); + + // TODO: ww maybe add useConversationUsername hook within contact name + if (destination !== ourKey) { + return ( +
+ {fromName} {window.i18n('to')} + + + +
+ ); + } + + return
{fromName}
; +}; + +const AvatarItem = (props: { source: string }) => { + const { source } = props; + return ; +}; + +export const MessageSearchResult = (props: Props) => { + const { + isSelected, + id, + conversationId, + receivedAt, + snippet, + destination, + source, + direction, + } = props; + + const sourceOrDestinationDerivable = + (destination && direction === MessageDirection.outgoing) || + !destination || + !source || + (source && direction === MessageDirection.incoming); + + if (!sourceOrDestinationDerivable) { + return null; + } + + const effectiveSource = + !source && direction === MessageDirection.outgoing ? getOurPubKeyStrFromCache() : source; + const effectiveDestination = + !destination && direction === MessageDirection.incoming + ? getOurPubKeyStrFromCache() + : destination; + + return ( +
{ + await openConversationWithMessages({ + conversationKey: conversationId, + messageId: id, + }); + }} + className={classNames( + 'module-message-search-result', + isSelected ? 'module-message-search-result--is-selected' : null + )} + > + +
+
+ +
+ +
+
+
+ +
+
+
+ ); +}; diff --git a/ts/components/search/SearchResults.tsx b/ts/components/search/SearchResults.tsx index 2d81e98dc..88d96c35e 100644 --- a/ts/components/search/SearchResults.tsx +++ b/ts/components/search/SearchResults.tsx @@ -3,10 +3,13 @@ import { ConversationListItemProps, MemoConversationListItemWithDetails, } from '../leftpane/conversation-list-item/ConversationListItem'; +import { MessageSearchResult } from './MessageSearchResults'; export type SearchResultsProps = { contacts: Array; conversations: Array; + // TODO: ww add proper typing + messages: Array; hideMessagesHeader: boolean; searchTerm: string; }; @@ -23,11 +26,12 @@ const ContactsItem = (props: { header: string; items: Array { - const { conversations, contacts, searchTerm } = props; + const { conversations, contacts, messages, searchTerm, hideMessagesHeader } = props; const haveConversations = conversations && conversations.length; const haveContacts = contacts && contacts.length; - const noResults = !haveConversations && !haveContacts; + const haveMessages = messages && messages.length; + const noResults = !haveConversations && !haveContacts && !haveMessages; return (
@@ -50,7 +54,7 @@ export const SearchResults = (props: SearchResultsProps) => { ) : null} - {/* {haveMessages ? ( + {haveMessages ? (
{hideMessagesHeader ? null : (
@@ -58,10 +62,10 @@ export const SearchResults = (props: SearchResultsProps) => {
)} {messages.map(message => ( - + ))}
- ) : null} */} + ) : null}
); }; diff --git a/ts/components/settings/section/CategoryAppearance.tsx b/ts/components/settings/section/CategoryAppearance.tsx index c21ef9161..8bac7a476 100644 --- a/ts/components/settings/section/CategoryAppearance.tsx +++ b/ts/components/settings/section/CategoryAppearance.tsx @@ -3,7 +3,15 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; // tslint:disable-next-line: no-submodule-imports import useUpdate from 'react-use/lib/useUpdate'; -import { createOrUpdateItem, hasLinkPreviewPopupBeenDisplayed } from '../../../data/data'; +import { + createOrUpdateItem, + fillWithTestData, + fillWithTestData2, + // fillWithTestData2, + getMessageCount, + hasLinkPreviewPopupBeenDisplayed, + trimMessages, +} from '../../../data/data'; import { ToastUtils } from '../../../session/utils'; import { updateConfirmModal } from '../../../state/ducks/modalDialog'; import { toggleAudioAutoplay } from '../../../state/ducks/userConfig'; @@ -131,6 +139,30 @@ export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null buttonColor={SessionButtonColor.Primary} buttonText={window.i18n('translation')} /> + { + console.warn('trim the database to last 10k messages'); + + const msgCount = await getMessageCount(); + const deleteAmount = Math.max(msgCount - 10000, 0); + + dispatch( + updateConfirmModal({ + onClickOk: () => { + void trimMessages(); + }, + onClickClose: () => { + updateConfirmModal(null); + }, + message: `Are you sure you want to delete your ${deleteAmount} oldest received messages?`, + }) + ); + }} + buttonColor={SessionButtonColor.Primary} + buttonText={window.i18n('trimDatabase')} + /> { ipcRenderer.send('show-debug-log'); @@ -138,6 +170,20 @@ export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null buttonColor={SessionButtonColor.Primary} buttonText={window.i18n('showDebugLog')} /> + { + await fillWithTestData(100, 2000000); + }} + buttonColor={SessionButtonColor.Primary} + buttonText={'Spam fill DB'} + /> + { + await fillWithTestData2(100, 1000); + }} + buttonColor={SessionButtonColor.Primary} + buttonText={'Spam fill DB using cached'} + /> ); } diff --git a/ts/data/data.ts b/ts/data/data.ts index e5cc48e59..3b8b8c293 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -3,10 +3,15 @@ import { ipcRenderer } from 'electron'; // tslint:disable: no-require-imports no-var-requires one-variable-per-declaration no-void-expression import _ from 'lodash'; -import { ConversationCollection, ConversationModel } from '../models/conversation'; +import { + ConversationCollection, + ConversationModel, + ConversationTypeEnum, +} from '../models/conversation'; import { MessageCollection, MessageModel } from '../models/message'; -import { MessageAttributes } from '../models/messageType'; +import { MessageAttributes, MessageDirection } from '../models/messageType'; import { HexKeyPair } from '../receiver/keypairs'; +import { getConversationController } from '../session/conversations'; import { getSodium } from '../session/crypto'; import { PubKey } from '../session/types'; import { fromArrayBufferToBase64, fromBase64ToArrayBuffer } from '../session/utils/String'; @@ -109,6 +114,7 @@ const channelsToMake = { removeAllMessagesInConversation, + getMessageCount, getMessageBySender, getMessageBySenderAndServerTimestamp, getMessageBySenderAndTimestamp, @@ -123,6 +129,7 @@ const channelsToMake = { hasConversationOutgoingMessage, getSeenMessagesByHashList, getLastHashBySnode, + trimMessages, getUnprocessedCount, getAllUnprocessed, @@ -156,6 +163,9 @@ const channelsToMake = { removeAllClosedGroupEncryptionKeyPairs, removeOneOpenGroupV1Message, + // dev performance testing + fillWithTestData, + // open group v2 ...channelstoMakeOpenGroupV2, }; @@ -191,8 +201,11 @@ export function init() { }); } -// When IPC arguments are prepared for the cross-process send, they are JSON.stringified. -// We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates). +/** + * When IPC arguments are prepared for the cross-process send, they are JSON.stringified. + * We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates). + * @param data - data to be cleaned + */ function _cleanData(data: any): any { const keys = Object.keys(data); @@ -575,6 +588,7 @@ export async function searchConversations(query: string): Promise> { export async function searchMessages(query: string, { limit }: any = {}): Promise> { const messages = await channels.searchMessages(query, { limit }); + console.warn('searched message', messages); return messages; } @@ -758,6 +772,13 @@ export async function getMessagesByConversation( return new MessageCollection(messages); } +/** + * @returns Returns count of all messages in the database + */ +export async function getMessageCount() { + return channels.getMessageCount(); +} + export async function getFirstUnreadMessageIdInConversation( conversationId: string ): Promise { @@ -801,6 +822,12 @@ export async function removeAllMessagesInConversation(conversationId: string): P } while (messages.length > 0); } +export async function trimMessages(): Promise { + const count = await channels.trimMessages(); + console.warn({ count }); + return; +} + export async function getMessagesBySentAt(sentAt: number): Promise { const messages = await channels.getMessagesBySentAt(sentAt); return new MessageCollection(messages); @@ -964,3 +991,51 @@ export async function updateSnodePoolOnDb(snodesAsJsonString: string): Promise { return channels.removeOneOpenGroupV1Message(); } + +/** + * Generates fake conversations and distributes messages amongst the conversations randomly + * @param numConvosToAdd Amount of fake conversations to generate + * @param numMsgsToAdd Number of fake messages to generate + */ +export async function fillWithTestData( + numConvosToAdd: number, + numMsgsToAdd: number +): Promise { + if (!channels.fillWithTestData) { + return; + } + const ids = await channels.fillWithTestData(numConvosToAdd, numMsgsToAdd); + ids.map((id: string) => { + const convo = getConversationController().get(id); + const convoMsg = 'x'; + convo.set('lastMessage', convoMsg); + }); +} + +export const fillWithTestData2 = async (convs: number, msgs: number) => { + const newConvos = []; + for (let convsAddedCount = 0; convsAddedCount < convs; convsAddedCount++) { + const convoId = `${Date.now()} + ${convsAddedCount}`; + const newConvo = await getConversationController().getOrCreateAndWait( + convoId, + ConversationTypeEnum.PRIVATE + ); + newConvos.push(newConvo); + } + + for (let msgsAddedCount = 0; msgsAddedCount < msgs; msgsAddedCount++) { + if (msgsAddedCount % 100 === 0) { + console.warn(msgsAddedCount); + } + // tslint:disable: insecure-random + const convoToChoose = newConvos[Math.floor(Math.random() * newConvos.length)]; + await convoToChoose.addSingleMessage({ + source: convoToChoose.id, + type: MessageDirection.outgoing, + conversationId: convoToChoose.id, + body: `spongebob ${new Date().toString()}`, + // tslint:disable: insecure-random + direction: Math.random() > 0.5 ? 'outgoing' : 'incoming', + }); + } +}; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 0f478b3e3..2d8d4df43 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -23,6 +23,7 @@ import { toHex } from '../session/utils/String'; import { actions as conversationActions, conversationChanged, + conversationsChanged, LastMessageStatusType, MessageModelPropsWithoutConvoProps, ReduxConversationType, @@ -204,10 +205,7 @@ export class ConversationModel extends Backbone.Model { trailing: true, leading: true, }); - this.triggerUIRefresh = _.throttle(this.triggerUIRefresh, 1000, { - trailing: true, - leading: true, - }); + this.throttledNotify = _.debounce(this.notify, 500, { maxWait: 5000, trailing: true }); //start right away the function is called, and wait 1sec before calling it again const markReadDebounced = _.debounce(this.markReadBouncy, 1000, { @@ -915,14 +913,8 @@ export class ConversationModel extends Backbone.Model { } public triggerUIRefresh() { - window.inboxStore?.dispatch( - conversationChanged({ - id: this.id, - data: { - ...this.getConversationModelProps(), - }, - }) - ); + updatesToDispatch.set(this.id, this.getConversationModelProps()); + trotthledAllConversationsDispatch(); } public async commit() { @@ -1253,26 +1245,6 @@ export class ConversationModel extends Backbone.Model { } } - public async upgradeMessages(messages: any) { - // tslint:disable-next-line: one-variable-per-declaration - for (let max = messages.length, i = 0; i < max; i += 1) { - const message = messages.at(i); - const { attributes } = message; - const { schemaVersion } = attributes; - - if (schemaVersion < window.Signal.Types.Message.VERSION_NEEDED_FOR_DISPLAY) { - // Yep, we really do want to wait for each of these - // eslint-disable-next-line no-await-in-loop - const { upgradeMessageSchema } = window.Signal.Migrations; - - const upgradedMessage = await upgradeMessageSchema(attributes); - message.set(upgradedMessage); - // eslint-disable-next-line no-await-in-loop - await upgradedMessage.commit(); - } - } - } - public hasMember(pubkey: string) { return _.includes(this.get('members'), pubkey); } @@ -1665,6 +1637,18 @@ export class ConversationModel extends Backbone.Model { } } +const trotthledAllConversationsDispatch = _.throttle(() => { + if (updatesToDispatch.size === 0) { + return; + } + console.warn('updatesToDispatch.size ', updatesToDispatch.size); + window.inboxStore?.dispatch(conversationsChanged([...updatesToDispatch.values()])); + + updatesToDispatch.clear(); +}, 500); + +const updatesToDispatch: Map = new Map(); + export class ConversationCollection extends Backbone.Collection { constructor(models?: Array) { super(models); diff --git a/ts/models/message.ts b/ts/models/message.ts index 6d2b36037..a4c03bb5d 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -75,13 +75,6 @@ export class MessageModel extends Backbone.Model { const filledAttrs = fillMessageAttributesWithDefaults(attributes); super(filledAttrs); - this.set( - window.Signal.Types.Message.initializeSchemaVersion({ - message: filledAttrs, - logger: window.log, - }) - ); - if (!this.attributes.id) { throw new Error('A message always needs to have an id.'); } diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index b2455de9b..d11d59c37 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -33,7 +33,6 @@ export interface MessageAttributes { hasAttachments: boolean; hasFileAttachments: boolean; hasVisualMediaAttachments: boolean; - schemaVersion: number; expirationTimerUpdate?: { expireTimer: number; source: string; @@ -118,6 +117,11 @@ export interface DataExtractionNotificationMsg { referencedAttachmentTimestamp: number; // the attachment timestamp he screenshot } +export enum MessageDirection { + outgoing = 'outgoing', + incoming = 'incoming', +} + export type PropsForDataExtractionNotification = DataExtractionNotificationMsg & { name: string; messageId: string; @@ -158,7 +162,6 @@ export interface MessageAttributesOptionals { hasAttachments?: boolean; hasFileAttachments?: boolean; hasVisualMediaAttachments?: boolean; - schemaVersion?: number; expirationTimerUpdate?: { expireTimer: number; source: string; @@ -199,7 +202,6 @@ export const fillMessageAttributesWithDefaults = ( const defaulted = _.defaults(optAttributes, { expireTimer: 0, // disabled id: uuidv4(), - schemaVersion: window.Signal.Types.Message.CURRENT_SCHEMA_VERSION, unread: 0, // if nothing is set, this message is considered read }); // this is just to cleanup a bit the db. delivered and delivered_to were removed, so everytime we load a message diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index f3915ddb2..f18a2b871 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -18,9 +18,6 @@ function contentTypeSupported(type: string): boolean { // tslint:disable-next-line: cyclomatic-complexity async function copyFromQuotedMessage(msg: MessageModel, quote?: Quote): Promise { - const { upgradeMessageSchema } = window.Signal.Migrations; - const { Message: TypedMessage, Errors } = window.Signal.Types; - if (!quote) { return; } @@ -217,7 +214,6 @@ async function handleRegularMessage( hasFileAttachments: dataMessage.hasFileAttachments, hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments, quote: dataMessage.quote, - schemaVersion: dataMessage.schemaVersion, attachments: dataMessage.attachments, body: dataMessage.body, conversationId: conversation.id, diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index d19a83231..b1b2521f9 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -567,23 +567,25 @@ const conversationsSlice = createSlice({ }> ) { const { payload } = action; - const { id, data } = payload; - const { conversationLookup, selectedConversation } = state; + return applyConversationChanged(state, payload); + }, + conversationsChanged( + state: ConversationsStateType, + action: PayloadAction> + ) { + const { payload } = action; - const existing = conversationLookup[id]; - // In the change case we only modify the lookup if we already had that conversation - if (!existing) { - return state; + let updatedState = state; + if (payload.length) { + payload.forEach(convoProps => { + updatedState = applyConversationChanged(updatedState, { + id: convoProps.id, + data: convoProps, + }); + }); } - return { - ...state, - selectedConversation, - conversationLookup: { - ...conversationLookup, - [id]: data, - }, - }; + return updatedState; }, conversationRemoved(state: ConversationsStateType, action: PayloadAction) { @@ -784,12 +786,36 @@ const conversationsSlice = createSlice({ }, }); +function applyConversationChanged( + state: ConversationsStateType, + payload: { id: string; data: ReduxConversationType } +) { + const { id, data } = payload; + const { conversationLookup, selectedConversation } = state; + + const existing = conversationLookup[id]; + // In the change case we only modify the lookup if we already had that conversation + if (!existing) { + return state; + } + + return { + ...state, + selectedConversation, + conversationLookup: { + ...conversationLookup, + [id]: data, + }, + }; +} + // destructures export const { actions, reducer } = conversationsSlice; export const { // conversation and messages list conversationAdded, conversationChanged, + conversationsChanged, conversationRemoved, removeAllConversations, messageExpired, diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 38ab06d4d..6347aa9bd 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -5,6 +5,7 @@ import { searchConversations, searchMessages } from '../../../ts/data/data'; import { ReduxConversationType } from './conversations'; import { PubKey } from '../../session/types'; import { ConversationTypeEnum } from '../../models/conversation'; +import _ from 'lodash'; // State @@ -16,6 +17,10 @@ export type SearchStateType = { // For conversations we store just the id, and pull conversation props in the selector conversations: Array; contacts: Array; + + // TODO: ww typing + messages?: Array; + messagesLookup?: any; }; // Actions @@ -24,6 +29,9 @@ type SearchResultsPayloadType = { normalizedPhoneNumber?: string; conversations: Array; contacts: Array; + + // TODO: ww typing + messages?: Array; }; type SearchResultsKickoffActionType = { @@ -75,7 +83,7 @@ async function doSearch(query: string, options: SearchOptions): Promise message !== undefined); + let filteredMessages = _.compact(messages); if (isAdvancedQuery) { let senderFilter: Array = []; @@ -88,12 +96,12 @@ async function doSearch(query: string, options: SearchOptions): Promise }); export const getSearchResults = createSelector( - [getSearch, getConversationLookup, getSelectedConversationKey], - (state: SearchStateType, lookup: ConversationLookupType, selectedConversation?: string) => { + [getSearch, getConversationLookup, getSelectedConversationKey, getSelectedMessage], + ( + searchState: SearchStateType, + lookup: ConversationLookupType, + selectedConversation?: string, + selectedMessage?: string + ) => { + console.warn({ state: searchState }); return { contacts: compact( - state.contacts.map(id => { + searchState.contacts.map(id => { const value = lookup[id]; if (value && id === selectedConversation) { @@ -41,7 +47,7 @@ export const getSearchResults = createSelector( }) ), conversations: compact( - state.conversations.map(id => { + searchState.conversations.map(id => { const value = lookup[id]; // Don't return anything when activeAt is unset (i.e. no current conversations with this user) @@ -60,9 +66,21 @@ export const getSearchResults = createSelector( return value; }) ), + messages: compact( + searchState.messages?.map(message => { + if (message.id === selectedMessage) { + return { + ...message, + isSelected: true, + }; + } + + return message; + }) + ), hideMessagesHeader: false, - searchTerm: state.query, + searchTerm: searchState.query, }; } ); diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index 73d921944..b1e3f0459 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -268,15 +268,6 @@ export type Attachment = { contentType?: MIME.MIMEType; size?: number; data: ArrayBuffer; - - // // Omit unused / deprecated keys: - // schemaVersion?: number; - // id?: string; - // width?: number; - // height?: number; - // thumbnail?: ArrayBuffer; - // key?: ArrayBuffer; - // digest?: ArrayBuffer; } & Partial; interface AttachmentSchemaVersion3 { diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index e254fe6dd..da5b86cbc 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -462,4 +462,6 @@ export type LocalizerKeys = | 'searchFor...' | 'joinedTheGroup' | 'editGroupName' + | 'trimDatabase' + | 'trimDatabaseDescription' | 'reportIssue';