diff --git a/_locales/en/messages.json b/_locales/en/messages.json index af3f62d7d..064a5460a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -137,6 +137,13 @@ "typingIndicatorsSettingDescription": "See and share when messages are being typed (applies to all sessions).", "typingIndicatorsSettingTitle": "Typing Indicators", "zoomFactorSettingTitle": "Zoom Factor", + "pruneSettingTitle": "Prune Old Open Group Messages", + "pruneSettingDescription": "Prune messages older than 6 months from Open Groups on start", + "pruningOpengroupDialogTitle": "Open group pruning", + "pruningOpengroupDialogMessage": "Pruning old open group messages improves performance. Enable pruning for open group messages older than 6 months?", + "pruningOpengroupDialogSubMessage": "You can change this setting in the Session settings menu", + "enable": "Enable", + "keepDisabled": "Keep disabled", "notificationSettingsDialog": "When messages arrive, display notifications that reveal...", "disableNotifications": "Mute notifications", "nameAndMessage": "Name and content", diff --git a/preload.js b/preload.js index 7fafda6e7..abae0e161 100644 --- a/preload.js +++ b/preload.js @@ -86,6 +86,28 @@ window.getStartInTray = async () => { }); }; +window.getOpengroupPruning = async () => { + return new Promise(resolve => { + ipc.once('get-opengroup-pruning-response', (_event, value) => { + resolve(value); + }); + ipc.send('get-opengroup-pruning'); + }); +}; + +window.setOpengroupPruning = async opengroupPruning => + new Promise((resolve, reject) => { + ipc.once('set-opengroup-pruning-response', (_event, error) => { + if (error) { + reject(error); + return; + } + resolve(); + return; + }); + ipc.send('set-opengroup-pruning', opengroupPruning); + }); + window._ = require('lodash'); // We never do these in our code, so we'll prevent it everywhere diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 53f23bd74..2ace0f7db 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -551,7 +551,9 @@ label { &-main-message { font-size: $session-font-md; + line-height: 1.5; } + &-sub-message { text-align: center; margin-top: 20px; diff --git a/ts/components/dialog/SessionConfirm.tsx b/ts/components/dialog/SessionConfirm.tsx index b0b9534d0..805d1e042 100644 --- a/ts/components/dialog/SessionConfirm.tsx +++ b/ts/components/dialog/SessionConfirm.tsx @@ -127,6 +127,12 @@ export const SessionConfirm = (props: SessionConfirmDialogProps) => {
+ {!hideCancel && ( { dataTestId="session-confirm-cancel-button" /> )} -
); diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index da6b67108..dceb78031 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -30,7 +30,11 @@ import { cleanUpOldDecryptedMedias } from '../../session/crypto/DecryptedAttachm import { DURATION } from '../../session/constants'; import { conversationChanged, conversationRemoved } from '../../state/ducks/conversations'; -import { editProfileModal, onionPathModal } from '../../state/ducks/modalDialog'; +import { + editProfileModal, + onionPathModal, + updateConfirmModal, +} from '../../state/ducks/modalDialog'; import { uploadOurAvatar } from '../../interactions/conversationInteractions'; import { ModalContainer } from '../dialog/ModalContainer'; import { debounce, isEmpty, isString } from 'lodash'; @@ -54,6 +58,9 @@ import { getLatestDesktopReleaseFileToFsV2 } from '../../session/apis/file_serve import { ipcRenderer } from 'electron'; import { UserUtils } from '../../session/utils'; +import { Storage } from '../../util/storage'; +import { SettingsKey } from '../../data/settings-key'; + const Section = (props: { type: SectionType }) => { const ourNumber = useSelector(getOurNumber); const unreadMessageCount = useSelector(getUnreadMessageCount); @@ -251,6 +258,8 @@ const doAppStartUp = () => { void loadDefaultRooms(); debounce(triggerAvatarReUploadIfNeeded, 200); + + void askEnablingOpengroupPruningIfNeeded(); }; const CallContainer = () => { @@ -279,6 +288,36 @@ async function fetchReleaseFromFSAndUpdateMain() { } } +async function askEnablingOpengroupPruningIfNeeded() { + if (Storage.get(SettingsKey.settingsOpengroupPruning) === undefined) { + const setSettingsAndCloseDialog = async (valueToSetPruningTo: boolean) => { + window.setSettingValue(SettingsKey.settingsOpengroupPruning, valueToSetPruningTo); + await window.setOpengroupPruning(valueToSetPruningTo); + window.inboxStore?.dispatch(updateConfirmModal(null)); + }; + window.inboxStore?.dispatch( + updateConfirmModal({ + onClickOk: async () => { + await setSettingsAndCloseDialog(true); + }, + onClickClose: async () => { + await setSettingsAndCloseDialog(false); + }, + onClickCancel: async () => { + await setSettingsAndCloseDialog(false); + }, + title: window.i18n('pruningOpengroupDialogTitle'), + message: window.i18n('pruningOpengroupDialogMessage'), + messageSub: window.i18n('pruningOpengroupDialogSubMessage'), + okText: window.i18n('enable'), + cancelText: window.i18n('keepDisabled'), + }) + ); + return; + } + // otherwise nothing to do. the settings is already on or off, but as expected by the user +} + /** * ActionsPanel is the far left banner (not the left pane). * The panel with buttons to switch between the message/contact/settings/theme views diff --git a/ts/components/settings/section/CategoryAppearance.tsx b/ts/components/settings/section/CategoryAppearance.tsx index 39e1b237b..31a3fffd0 100644 --- a/ts/components/settings/section/CategoryAppearance.tsx +++ b/ts/components/settings/section/CategoryAppearance.tsx @@ -4,6 +4,7 @@ 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 { SettingsKey } from '../../../data/settings-key'; import { ToastUtils } from '../../../session/utils'; import { updateConfirmModal } from '../../../state/ducks/modalDialog'; import { toggleAudioAutoplay } from '../../../state/ducks/userConfig'; @@ -15,8 +16,8 @@ import { SessionSettingButtonItem, SessionToggleWithDescription } from '../Sessi import { ZoomingSessionSlider } from '../ZoomingSessionSlider'; async function toggleLinkPreviews() { - const newValue = !window.getSettingValue('link-preview-setting'); - window.setSettingValue('link-preview-setting', newValue); + const newValue = !window.getSettingValue(SettingsKey.settingsLinkPreview); + window.setSettingValue(SettingsKey.settingsLinkPreview, newValue); if (!newValue) { await createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: false }); } else { @@ -35,7 +36,7 @@ async function toggleStartInTray() { const newValue = !(await window.getStartInTray()); // make sure to write it here too, as this is the value used on the UI to mark the toggle as true/false - window.setSettingValue('start-in-tray-setting', newValue); + window.setSettingValue(SettingsKey.settingsStartInTray, newValue); await window.setStartInTray(newValue); if (!newValue) { ToastUtils.pushRestartNeeded(); @@ -45,11 +46,6 @@ async function toggleStartInTray() { } } -const settingsMenuBar = 'hide-menu-bar'; -const settingsSpellCheck = 'spell-check'; -const settingsLinkPreview = 'link-preview-setting'; -const settingsStartInTray = 'start-in-tray-setting'; - export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null }) => { const dispatch = useDispatch(); const forceUpdate = useUpdate(); @@ -57,17 +53,17 @@ export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null if (props.hasPassword !== null) { const isHideMenuBarActive = - window.getSettingValue(settingsMenuBar) === undefined + window.getSettingValue(SettingsKey.settingsMenuBar) === undefined ? true - : window.getSettingValue(settingsMenuBar); + : window.getSettingValue(SettingsKey.settingsMenuBar); const isSpellCheckActive = - window.getSettingValue(settingsSpellCheck) === undefined + window.getSettingValue(SettingsKey.settingsSpellCheck) === undefined ? true - : window.getSettingValue(settingsSpellCheck); + : window.getSettingValue(SettingsKey.settingsSpellCheck); - const isLinkPreviewsOn = Boolean(window.getSettingValue(settingsLinkPreview)); - const isStartInTrayActive = Boolean(window.getSettingValue(settingsStartInTray)); + const isLinkPreviewsOn = Boolean(window.getSettingValue(SettingsKey.settingsLinkPreview)); + const isStartInTrayActive = Boolean(window.getSettingValue(SettingsKey.settingsStartInTray)); return ( <> @@ -119,6 +115,7 @@ export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null description={window.i18n('audioMessageAutoplayDescription')} active={audioAutoPlay} /> + void; }) => { const forceUpdate = useUpdate(); const dispatch = useDispatch(); - + const isOpengroupPruningEnabled = Boolean( + window.getSettingValue(SettingsKey.settingsOpengroupPruning) + ); if (props.hasPassword !== null) { return ( <> @@ -117,6 +132,15 @@ export const SettingsCategoryPrivacy = (props: { description={window.i18n('hideRequestBannerDescription')} active={useSelector(getHideMessageRequestBanner)} /> + { + await toggleOpengroupPruning(); + forceUpdate(); + }} + title={window.i18n('pruneSettingTitle')} + description={window.i18n('pruneSettingDescription')} + active={isOpengroupPruningEnabled} + /> {!props.hasPassword && ( { sendResponse(localisedError || 'Invalid password'); } }); + ipc.on('start-in-tray-on-start', (event, newValue) => { try { userConfig.set('startInTray', newValue); @@ -954,6 +957,24 @@ ipc.on('get-start-in-tray', event => { } }); +ipc.on('get-opengroup-pruning', event => { + try { + const val = userConfig.get('opengroupPruning'); + event.sender.send('get-opengroup-pruning-response', val); + } catch (e) { + event.sender.send('get-opengroup-pruning-response', false); + } +}); + +ipc.on('set-opengroup-pruning', (event, newValue) => { + try { + userConfig.set('opengroupPruning', newValue); + event.sender.send('set-opengroup-pruning-response', null); + } catch (e) { + event.sender.send('set-opengroup-pruning-response', e); + } +}); + ipc.on('set-password', async (event, passPhrase, oldPhrase) => { const sendResponse = (response: string | undefined) => { event.sender.send('set-password-response', response); diff --git a/ts/node/sql.ts b/ts/node/sql.ts index 7ca85ba7e..0a0edb60c 100644 --- a/ts/node/sql.ts +++ b/ts/node/sql.ts @@ -1551,6 +1551,7 @@ async function initializeSql({ console.info('total message count before cleaning: ', getMessageCount()); console.info('total conversation count before cleaning: ', getConversationCount()); cleanUpOldOpengroups(); + cleanUpUnusedNodeForKeyEntries(); printDbStats(); @@ -3411,11 +3412,26 @@ function cleanUpOldOpengroups() { console.info('cleanUpOldOpengroups: ourNumber is not set'); return; } + const pruneSetting = getItemById('prune-setting')?.value; + + if (pruneSetting === undefined) { + console.info( + 'Prune settings is undefined, skipping cleanUpOldOpengroups but we will need to ask user' + ); + return; + } + + if (!pruneSetting) { + console.info('Prune setting not enabled, skipping cleanUpOldOpengroups'); + return; + } + const v2Convos = getAllOpenGroupV2Conversations(); if (!v2Convos || !v2Convos.length) { console.info('cleanUpOldOpengroups: v2Convos is empty'); return; } + // For each opengroups, if it has more than 1000 messages, we remove all the messages older than 2 months. // So this does not limit the size of opengroup history to 1000 messages but to 2 months. // This is the only way we can cleanup conversations objects from users which just sent messages a while ago and with whom we never interacted. @@ -3436,20 +3452,20 @@ function cleanUpOldOpengroups() { if (messagesInConvoBefore >= maxMessagePerOpengroupConvo) { const minute = 1000 * 60; const sixMonths = minute * 60 * 24 * 30 * 6; - const messagesTimestampToRemove = Date.now() - sixMonths; + const limitTimestamp = Date.now() - sixMonths; const countToRemove = assertGlobalInstance() .prepare( `SELECT count(*) from ${MESSAGES_TABLE} WHERE serverTimestamp <= $serverTimestamp AND conversationId = $conversationId;` ) - .get({ conversationId: convoId, serverTimestamp: Date.now() - sixMonths })['count(*)']; + .get({ conversationId: convoId, serverTimestamp: limitTimestamp })['count(*)']; const start = Date.now(); assertGlobalInstance() .prepare( ` - DELETE FROM ${MESSAGES_TABLE} WHERE serverTimestamp <= $serverTimestamp AND conversationId = $conversationId` + DELETE FROM ${MESSAGES_TABLE} WHERE serverTimestamp <= $serverTimestamp AND conversationId = $conversationId` ) - .run({ conversationId: convoId, serverTimestamp: messagesTimestampToRemove }); // delete messages older than sixMonths + .run({ conversationId: convoId, serverTimestamp: limitTimestamp }); // delete messages older than 6 months ago. const messagesInConvoAfter = getMessagesCountByConversation(convoId); console.info( @@ -3680,6 +3696,7 @@ export const sqlNode = { getPubkeysInPublicConversation, getAllGroupsInvolvingId, removeAllConversations, + cleanUpOldOpengroups, searchConversations, searchMessages, diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 412e6c60d..46e26d227 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -7,6 +7,7 @@ export type LocalizerKeys = | 'startedACall' | 'mainMenuWindow' | 'unblocked' + | 'keepDisabled' | 'userAddedToModerators' | 'to' | 'sent' @@ -14,7 +15,9 @@ export type LocalizerKeys = | 'closedGroupInviteFailMessage' | 'noContactsForGroup' | 'linkVisitWarningMessage' + | 'messageRequestAcceptedOurs' | 'anonymous' + | 'youHaveANewFriendRequest' | 'viewMenuZoomOut' | 'dialogClearAllDataDeletionFailedDesc' | 'timerOption_10_seconds_abbreviated' @@ -22,9 +25,11 @@ export type LocalizerKeys = | 'connectToServerFail' | 'disableNotifications' | 'publicChatExists' + | 'noMediaUntilApproved' | 'passwordViewTitle' | 'joinOpenGroupAfterInvitationConfirmationTitle' | 'notificationMostRecentFrom' + | 'messageRequestAccepted' | 'timerOption_5_minutes' | 'linkPreviewsConfirmMessage' | 'notificationMostRecent' @@ -44,7 +49,6 @@ export type LocalizerKeys = | 'viewMenuToggleDevTools' | 'fileSizeWarning' | 'openGroupURL' - | 'hideRequestBannerDescription' | 'hideMenuBarDescription' | 'pickClosedGroupMember' | 'ByUsingThisService...' @@ -54,6 +58,7 @@ export type LocalizerKeys = | 'typingAlt' | 'orJoinOneOfThese' | 'members' + | 'noMessageRequestsPending' | 'sendRecoveryPhraseMessage' | 'timerOption_1_hour' | 'youGotKickedFromGroup' @@ -68,8 +73,10 @@ export type LocalizerKeys = | 'addModerators' | 'sessionMessenger' | 'today' + | 'mustBeApproved' | 'appMenuHideOthers' | 'sendFailed' + | 'openMessageRequestInbox' | 'enterPassword' | 'enterSessionIDOfRecipient' | 'dialogClearAllDataDeletionFailedMultiple' @@ -81,6 +88,7 @@ export type LocalizerKeys = | 'requestsSubtitle' | 'closedGroupInviteSuccessTitle' | 'accept' + | 'hideBanner' | 'setPasswordTitle' | 'editMenuUndo' | 'pinConversation' @@ -94,6 +102,7 @@ export type LocalizerKeys = | 'autoUpdateNewVersionInstructions' | 'appMenuUnhide' | 'timerOption_30_minutes_abbreviated' + | 'pruneSettingDescription' | 'voiceMessage' | 'changePasswordTitle' | 'copyMessage' @@ -101,12 +110,11 @@ export type LocalizerKeys = | 'deleteJustForMe' | 'changeAccountPasswordTitle' | 'onionPathIndicatorDescription' - | 'timestamp_s' + | 'pruningOpengroupDialogSubMessage' | 'mediaPermissionsTitle' | 'replyingToMessage' | 'welcomeToYourSession' | 'editMenuCopy' - | 'timestamp_m' | 'leftTheGroup' | 'timerOption_30_minutes' | 'nameOnly' @@ -116,6 +124,7 @@ export type LocalizerKeys = | 'inviteContacts' | 'callMediaPermissionsTitle' | 'blocked' + | 'hideRequestBannerDescription' | 'noBlockedContacts' | 'leaveGroupConfirmation' | 'banUserAndDeleteAll' @@ -197,6 +206,7 @@ export type LocalizerKeys = | 'viewMenuResetZoom' | 'startInTrayDescription' | 'groupNamePlaceholder' + | 'messageRequestPending' | 'stagedPreviewThumbnail' | 'helpUsTranslateSession' | 'editMenuDeleteGroup' @@ -208,6 +218,7 @@ export type LocalizerKeys = | 'closedGroupInviteFailMessagePlural' | 'noAudioInputFound' | 'timerOption_10_seconds' + | 'openMessageRequestInboxDescription' | 'noteToSelf' | 'failedToAddAsModerator' | 'disabledDisappearingMessages' @@ -219,7 +230,6 @@ export type LocalizerKeys = | 'displayName' | 'invalidSessionId' | 'audioPermissionNeeded' - | 'timestamp_h' | 'add' | 'messageRequests' | 'show' @@ -228,6 +238,7 @@ export type LocalizerKeys = | 'goToSupportPage' | 'passwordsDoNotMatch' | 'createClosedGroupNamePrompt' + | 'pruningOpengroupDialogMessage' | 'audioMessageAutoplayDescription' | 'leaveAndRemoveForEveryone' | 'previewThumbnail' @@ -236,9 +247,9 @@ export type LocalizerKeys = | 'editMenuDeleteContact' | 'hideMenuBarTitle' | 'imageCaptionIconAlt' - | 'clearAll' | 'sendRecoveryPhraseTitle' | 'multipleJoinedTheGroup' + | 'messageRequestAcceptedOursNoName' | 'databaseError' | 'resend' | 'copiedToClipboard' @@ -248,6 +259,7 @@ export type LocalizerKeys = | 'unableToLoadAttachment' | 'cameraPermissionNeededTitle' | 'editMenuRedo' + | 'hideRequestBanner' | 'changeNicknameMessage' | 'close' | 'deleteMessageQuestion' @@ -258,6 +270,7 @@ export type LocalizerKeys = | 'getStarted' | 'unblockUser' | 'blockUser' + | 'clearAllConfirmationTitle' | 'trustThisContactDialogTitle' | 'received' | 'trimDatabaseConfirmationBody' @@ -276,6 +289,7 @@ export type LocalizerKeys = | 'timerOption_6_hours_abbreviated' | 'timerOption_1_week_abbreviated' | 'timerSetTo' + | 'enable' | 'notificationSubtitle' | 'youChangedTheTimer' | 'updatedTheGroup' @@ -288,8 +302,10 @@ export type LocalizerKeys = | 'noSearchResults' | 'changeNickname' | 'userUnbanned' + | 'respondingToRequestWarning' | 'error' | 'clearAllData' + | 'pruningOpengroupDialogTitle' | 'contactAvatarAlt' | 'disappearingMessages' | 'autoUpdateNewVersionTitle' @@ -328,6 +344,7 @@ export type LocalizerKeys = | 'markAllAsRead' | 'failedResolveOns' | 'showDebugLog' + | 'declineRequestMessage' | 'autoUpdateDownloadButtonLabel' | 'dialogClearAllDataDeletionFailedTitleQuestion' | 'autoUpdateDownloadInstructions' @@ -353,6 +370,7 @@ export type LocalizerKeys = | 'learnMore' | 'passwordCharacterError' | 'autoUpdateSettingTitle' + | 'documentsEmptyState' | 'deleteForEveryone' | 'createSessionID' | 'multipleLeftTheGroup' @@ -376,9 +394,11 @@ export type LocalizerKeys = | 'callMediaPermissionsDialogContent' | 'timerOption_1_day_abbreviated' | 'about' + | 'clearAllConfirmationBody' | 'ok' | 'multipleKickedFromTheGroup' | 'trimDatabase' + | 'clearAll' | 'recoveryPhraseSavePromptMain' | 'editMenuPaste' | 'areYouSureDeleteDeviceOnly' @@ -421,7 +441,7 @@ export type LocalizerKeys = | 'recoveryPhrase' | 'newMessages' | 'you' - | 'documentsEmptyState' + | 'pruneSettingTitle' | 'unbanUser' | 'notificationForConvo_mentions_only' | 'trustThisContactDialogDescription' @@ -429,22 +449,4 @@ export type LocalizerKeys = | 'searchFor...' | 'joinedTheGroup' | 'editGroupName' - | 'trimDatabase' - | 'trimDatabaseDescription' - | 'trimDatabaseConfirmationBody' - | 'respondingToRequestWarning' - | 'messageRequestPending' - | 'messageRequestAccepted' - | 'messageRequestAcceptedOurs' - | 'messageRequestAcceptedOursNoName' - | 'declineRequestMessage' - | 'openMessageRequestInbox' - | 'hideRequestBanner' - | 'noMessageRequestsPending' - | 'noMediaUntilApproved' - | 'mustBeApproved' - | 'youHaveANewFriendRequest' - | 'clearAllConfirmationTitle' - | 'clearAllConfirmationBody' - | 'hideBanner' | 'reportIssue'; diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index c662b86cb..a24d74732 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -164,6 +164,10 @@ async function createAccount(identityKeyPair: any) { // Enable typing indicators by default await Storage.put(SettingsKey.settingsTypingIndicator, false); + // opengroups pruning in ON by default on new accounts, but you can change that from the settings + await Storage.put(SettingsKey.settingsOpengroupPruning, true); + await window.setOpengroupPruning(true); + await setLocalPubKey(pubKeyString); } diff --git a/ts/window.d.ts b/ts/window.d.ts index df99dc20e..a9a8e82b0 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -91,6 +91,8 @@ declare global { confirmationDialog: any; setStartInTray: (val: boolean) => Promise; getStartInTray: () => Promise; + getOpengroupPruning: () => Promise; + setOpengroupPruning: (val: boolean) => Promise; closeAbout: () => void; getAutoUpdateEnabled: () => boolean; setAutoUpdateEnabled: (enabled: boolean) => void;