diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ed686a4d6..c8b0cceba 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1801,7 +1801,7 @@ }, "disappearingMessages": { "message": "Disappearing messages", - "description": "Conversation menu option to enable disappearing messages" + "description": "Conversation menu option to enable disappearing messages. Title of the settings section for Disappearing Messages" }, "disappearingMessagesDisabled": { "message": "Disappearing messages disabled", @@ -5414,5 +5414,63 @@ "CustomColorEditor__title": { "message": "Custom Color", "description": "Modal title for the custom color editor" + }, + "customDisappearingTimeOption": { + "message": "Custom time...", + "description": "Text for an option in Disappearing Messages menu and Conversation Details Disappearing Messages setting when no user value is available" + }, + "selectedCustomDisappearingTimeOption": { + "message": "Custom time", + "description": "Text for an option in Conversation Details Disappearing Messages setting when user previously selected custom time" + }, + "DisappearingTimeDialog__title": { + "message": "Custom Time", + "description": "Title for the custom disappearing message timeout dialog" + }, + "DisappearingTimeDialog__body": { + "message": "Choose a custom time for disappearing messages.", + "description": "Body for the custom disappearing message timeout dialog" + }, + "DisappearingTimeDialog__set": { + "message": "Set", + "description": "Text for the dialog button confirming the custom disappearing message timeout" + }, + "DisappearingTimeDialog__seconds": { + "message": "Seconds", + "description": "Name of the 'seconds' unit select for the custom disappearing message timeout dialog" + }, + "DisappearingTimeDialog__minutes": { + "message": "Minutes", + "description": "Name of the 'minutes' unit select for the custom disappearing message timeout dialog" + }, + "DisappearingTimeDialog__hours": { + "message": "Hours", + "description": "Name of the 'hours' unit select for the custom disappearing message timeout dialog" + }, + "DisappearingTimeDialog__days": { + "message": "Days", + "description": "Name of the 'days' unit select for the custom disappearing message timeout dialog" + }, + "DisappearingTimeDialog__weeks": { + "message": "Weeks", + "description": "Name of the 'weeks' unit select for the custom disappearing message timeout dialog" + }, + "settings__DisappearingMessages__footer": { + "message": "Set a default disappearing message timer for all new chats started by you.", + "description": "Footer for the Disappearing Messages settings section" + }, + "settings__DisappearingMessages__timer__label": { + "message": "Default timer for new chats", + "description": "Label for the Disapearring Messages default timer setting" + }, + "UniversalTimerNotification__text": { + "message": "The disappearing message time will be set to $timeValue$ when you message them.", + "description": "A message displayed when default disappearing message timeout is about to be applied", + "placeholders": { + "timeValue": { + "content": "$1", + "example": "1 week" + } + } } } diff --git a/js/modules/signal.js b/js/modules/signal.js index f5c6f411f..1cd930320 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -59,6 +59,9 @@ const { const { StagedLinkPreview, } = require('../../ts/components/conversation/StagedLinkPreview'); +const { + DisappearingTimeDialog, +} = require('../../ts/components/conversation/DisappearingTimeDialog'); // State const { createTimeline } = require('../../ts/state/roots/createTimeline'); @@ -346,6 +349,7 @@ exports.setup = (options = {}) => { ProgressModal, SafetyNumberChangeDialog, StagedLinkPreview, + DisappearingTimeDialog, Types: { Message: MediaGalleryMessage, }, diff --git a/js/settings_start.js b/js/settings_start.js index 579261c0b..9766c9cbc 100644 --- a/js/settings_start.js +++ b/js/settings_start.js @@ -48,6 +48,7 @@ const getInitialData = async () => ({ isPrimary: await window.isPrimary(), lastSyncTime: await window.getLastSyncTime(), + universalExpireTimer: await window.getUniversalExpireTimer(), }); window.initialRequest = getInitialData(); diff --git a/js/views/settings_view.js b/js/views/settings_view.js index 91ea99f7b..13617baf4 100644 --- a/js/views/settings_view.js +++ b/js/views/settings_view.js @@ -12,6 +12,12 @@ window.Whisper = window.Whisper || {}; const { Settings } = window.Signal.Types; + const { + DEFAULT_DURATIONS_IN_SECONDS, + DEFAULT_DURATIONS_SET, + format: formatExpirationTimer, + } = window.Signal.Util.expirationTimer; + const CheckboxView = Whisper.View.extend({ initialize(options) { this.name = options.name; @@ -70,6 +76,106 @@ }, }); + const DisappearingMessagesView = Whisper.View.extend({ + template: () => $('#disappearingMessagesSettings').html(), + initialize(options) { + this.timeDialog = null; + + this.value = options.value || 0; + + this.render(); + }, + + render_attributes() { + const isCustomValue = this.isCustomValue(); + + return { + title: i18n('disappearingMessages'), + timerValues: DEFAULT_DURATIONS_IN_SECONDS.map(seconds => { + const text = formatExpirationTimer(i18n, seconds, { + capitalizeOff: true, + }); + return { + selected: seconds === this.value ? 'selected' : undefined, + value: seconds, + text, + }; + }), + customSelected: isCustomValue ? 'selected' : undefined, + customText: i18n( + isCustomValue + ? 'selectedCustomDisappearingTimeOption' + : 'customDisappearingTimeOption' + ), + customInfo: isCustomValue + ? { + text: formatExpirationTimer(i18n, this.value), + } + : undefined, + timerLabel: i18n('settings__DisappearingMessages__timer__label'), + footer: i18n('settings__DisappearingMessages__footer'), + }; + }, + + events: { + change: 'change', + }, + + change(e) { + const value = parseInt(e.target.value, 10); + + if (value === -1) { + this.showDialog(); + return; + } + + this.updateValue(value); + window.log.info('disappearing-messages-timer changed to', this.value); + }, + + isCustomValue() { + return this.value && !DEFAULT_DURATIONS_SET.has(this.value); + }, + + showDialog() { + this.closeDialog(); + + this.timeDialog = new window.Whisper.ReactWrapperView({ + className: 'disappearing-time-dialog-wrapper', + Component: window.Signal.Components.DisappearingTimeDialog, + props: { + i18n, + initialValue: this.value, + onSubmit: newValue => { + this.updateValue(newValue); + this.closeDialog(); + + window.log.info( + 'disappearing-messages-timer changed to custom value', + this.value + ); + }, + onClose: () => { + this.closeDialog(); + }, + }, + }); + }, + + closeDialog() { + if (this.timeDialog) { + this.timeDialog.remove(); + } + this.timeDialog = null; + }, + + updateValue(newValue) { + this.value = newValue; + window.setUniversalExpireTimer(newValue); + this.render(); + }, + }); + const RadioButtonGroupView = Whisper.View.extend({ initialize(options) { this.name = options.name; @@ -202,6 +308,15 @@ value: window.initialData.mediaCameraPermissions, setFn: window.setMediaCameraPermissions, }); + + const disappearingMessagesView = new DisappearingMessagesView({ + value: window.initialData.universalExpireTimer, + name: 'disappearing-messages-setting', + }); + this.$('.disappearing-messages-setting').append( + disappearingMessagesView.el + ); + if (!window.initialData.isPrimary) { const syncView = new SyncView().render(); this.$('.sync-setting').append(syncView.el); diff --git a/main.js b/main.js index f54ade696..7f9e8a918 100644 --- a/main.js +++ b/main.js @@ -1697,6 +1697,8 @@ installSettingsGetter('is-primary'); installSettingsGetter('sync-request'); installSettingsGetter('sync-time'); installSettingsSetter('sync-time'); +installSettingsGetter('universal-expire-timer'); +installSettingsSetter('universal-expire-timer'); ipc.on('delete-all-data', () => { if (mainWindow && mainWindow.webContents) { diff --git a/preload.js b/preload.js index 40c86a079..5ee63856d 100644 --- a/preload.js +++ b/preload.js @@ -367,6 +367,8 @@ try { installGetter('sync-request', 'getSyncRequest'); installGetter('sync-time', 'getLastSyncTime'); installSetter('sync-time', 'setLastSyncTime'); + installGetter('universal-expire-timer', 'getUniversalExpireTimer'); + installSetter('universal-expire-timer', 'setUniversalExpireTimer'); ipc.on('delete-all-data', async () => { const { deleteAllData } = window.Events; diff --git a/protos/SignalStorage.proto b/protos/SignalStorage.proto index d54ed9aba..da2ee9310 100644 --- a/protos/SignalStorage.proto +++ b/protos/SignalStorage.proto @@ -129,5 +129,6 @@ message AccountRecord { optional PhoneNumberSharingMode phoneNumberSharingMode = 12; optional bool notDiscoverableByPhoneNumber = 13; repeated PinnedConversation pinnedConversations = 14; + optional uint32 universalExpireTimer = 17; optional bool primarySendsSms = 18; } diff --git a/settings.html b/settings.html index c08619b4c..aef93307c 100644 --- a/settings.html +++ b/settings.html @@ -1,4 +1,4 @@ - + @@ -38,6 +38,42 @@

+ + diff --git a/settings_preload.js b/settings_preload.js index 0708ce7ef..82bd87920 100644 --- a/settings_preload.js +++ b/settings_preload.js @@ -98,6 +98,8 @@ window.isPrimary = makeGetter('is-primary'); window.makeSyncRequest = makeGetter('sync-request'); window.getLastSyncTime = makeGetter('sync-time'); window.setLastSyncTime = makeSetter('sync-time'); +window.getUniversalExpireTimer = makeGetter('universal-expire-timer'); +window.setUniversalExpireTimer = makeSetter('universal-expire-timer'); window.deleteAllData = () => ipcRenderer.send('delete-all-data'); @@ -130,6 +132,9 @@ function makeSetter(name) { } window.Backbone = require('backbone'); +window.React = require('react'); +window.ReactDOM = require('react-dom'); + require('./ts/backbone/views/whisper_view'); require('./ts/backbone/views/toast_view'); require('./ts/logging/set_up_renderer_logging').initialize(); diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index c08f2e405..219513889 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -2461,6 +2461,19 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', margin-top: 1px; } +// Module: Universal Timer Notification + +.module-universal-timer-notification { + text-align: center; + + @include light-theme { + color: $color-gray-60; + } + @include dark-theme { + color: $color-gray-05; + } +} + .module-notification--with-click-handler { cursor: pointer; } @@ -3096,7 +3109,8 @@ button.module-conversation-details__action-button { margin-right: 12px; } - &__info { + &__info, + &__right-info { @include font-body-2; margin-top: 4px; @@ -3110,7 +3124,17 @@ button.module-conversation-details__action-button { } &__right { + position: relative; color: $color-gray-45; + min-width: 143px; + } + + &__right-info { + position: absolute; + + @include font-subtitle; + + padding-left: 14px; } &__actions { @@ -3170,60 +3194,6 @@ button.module-conversation-details__action-button { @include font-body-1-bold; } } - - &-select { - position: relative; - - select { - @include font-body-2; - -webkit-appearance: none; - border-radius: 4px; - border: 1px solid $color-gray-25; - cursor: pointer; - height: 40px; - min-width: 124px; - outline: 0; - padding: 10px; - padding-left: 12px; - padding-right: 32px; - text-overflow: ellipsis; - width: 100%; - - @include dark-theme { - background-color: $color-gray-90; - border-color: $color-gray-60; - color: $color-gray-05; - } - - &:focus { - border: 3px solid $color-ultramarine; - line-height: 14px; - padding-left: 10px; - } - } - - &::after { - border: 2px solid $color-gray-60; - border-radius: 2px; - border-right: 0; - border-top: 0; - content: ' '; - display: block; - height: 10px; - pointer-events: none; - position: absolute; - right: 15px; - top: 14px; - transform-origin: center; - transform: rotate(-45deg); - width: 10px; - z-index: 2; - - @include dark-theme { - border-color: $color-gray-15; - } - } - } } // Module: Message Detail diff --git a/stylesheets/_settings.scss b/stylesheets/_settings.scss index 31e1420e9..3acfebb59 100644 --- a/stylesheets/_settings.scss +++ b/stylesheets/_settings.scss @@ -7,6 +7,7 @@ &.modal { padding: 0; background-color: transparent; + z-index: 1; .content { margin: 0; @@ -77,4 +78,39 @@ @include font-body-2; color: $color-gray-60; } + + .disappearing-messages-setting { + &__timer { + display: flex; + flex-direction: row; + align-items: center; + + &__label { + flex-grow: 1; + margin-right: 20px; + } + + margin-bottom: 10px; + + &__right { + position: relative; + &__info { + position: absolute; + + @include font-subtitle; + + padding-left: 14px; + } + } + + &--with-info { + margin-bottom: 16px; + } + } + + &__footer { + @include font-body-2; + color: $color-gray-60; + } + } } diff --git a/stylesheets/components/ConversationHeader.scss b/stylesheets/components/ConversationHeader.scss index a67ddb491..bc14a9db3 100644 --- a/stylesheets/components/ConversationHeader.scss +++ b/stylesheets/components/ConversationHeader.scss @@ -2,6 +2,30 @@ // SPDX-License-Identifier: AGPL-3.0-only .module-ConversationHeader { + @mixin icon-element($icon, $margin-right: 4px) { + display: flex; + align-items: center; + user-select: none; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + &::before { + content: ''; + width: 13px; + height: 13px; + display: block; + margin-right: $margin-right; + + @include light-theme { + @include color-svg($icon, $color-gray-60); + } + @include dark-theme { + @include color-svg($icon, $color-gray-25); + } + } + } + --button-spacing: 24px; &.module-ConversationHeader--narrow { @@ -133,37 +157,13 @@ color: $color-gray-25; } - @mixin subtitle-element($icon) { - display: flex; - align-items: center; - user-select: none; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - - &::before { - content: ''; - width: 13px; - height: 13px; - display: block; - margin-right: 4px; - - @include light-theme { - @include color-svg($icon, $color-gray-60); - } - @include dark-theme { - @include color-svg($icon, $color-gray-25); - } - } - } - &__expiration { - @include subtitle-element('../images/icons/v2/timer-24.svg'); + @include icon-element('../images/icons/v2/timer-24.svg'); margin-right: 12px; } &__verified { - @include subtitle-element('../images/icons/v2/check-24.svg'); + @include icon-element('../images/icons/v2/check-24.svg'); } } } @@ -308,4 +308,13 @@ } } } + + &__disappearing-timer__item { + padding-left: 25px; + + &--active { + padding-left: 0px; + @include icon-element('../images/icons/v2/check-24.svg', 12px); + } + } } diff --git a/stylesheets/components/DisappearingTimeDialog.scss b/stylesheets/components/DisappearingTimeDialog.scss new file mode 100644 index 000000000..e8f2f5b35 --- /dev/null +++ b/stylesheets/components/DisappearingTimeDialog.scss @@ -0,0 +1,25 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.module-disappearing-time-dialog { + &__title.module-Modal__title { + margin-bottom: 2px; + } + + &__body p { + margin: 0 0 25px 0; + } + + &__time-boxes { + display: flex; + flex-direction: row; + + .module-select { + flex-grow: 1; + } + + &__units { + margin-left: 9px; + } + } +} diff --git a/stylesheets/components/Select.scss b/stylesheets/components/Select.scss new file mode 100644 index 000000000..4889ec9b2 --- /dev/null +++ b/stylesheets/components/Select.scss @@ -0,0 +1,55 @@ +.module-select { + position: relative; + + select { + @include font-body-2; + -webkit-appearance: none; + border-radius: 4px; + border: 1px solid $color-gray-25; + cursor: pointer; + height: 40px; + min-width: 124px; + outline: 0; + padding: 10px; + padding-left: 12px; + padding-right: 32px; + text-overflow: ellipsis; + width: 100%; + + @include dark-theme { + background-color: $color-gray-90; + border-color: $color-gray-60; + color: $color-gray-05; + } + + @include keyboard-mode { + &:focus { + border: 3px solid $color-ultramarine; + line-height: 14px; + padding-left: 10px; + } + } + } + + &::after { + border: 2px solid $color-gray-60; + border-radius: 2px; + border-right: 0; + border-top: 0; + content: ' '; + display: block; + height: 10px; + pointer-events: none; + position: absolute; + right: 15px; + top: 14px; + transform-origin: center; + transform: rotate(-45deg); + width: 10px; + z-index: 2; + + @include dark-theme { + border-color: $color-gray-15; + } + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 11769e0c3..ae0c055ae 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -41,6 +41,7 @@ @import './components/ContactSpoofingReviewDialogPerson.scss'; @import './components/ConversationHeader.scss'; @import './components/CustomColorEditor.scss'; +@import './components/DisappearingTimeDialog.scss'; @import './components/EditConversationAttributesModal.scss'; @import './components/ForwardMessageModal.scss'; @import './components/GradientDial.scss'; @@ -55,5 +56,6 @@ @import './components/SearchResultsLoadingFakeRow.scss'; @import './components/Slider.scss'; @import './components/Tabs.scss'; +@import './components/Select.scss'; @import './components/TimelineWarning.scss'; @import './components/TimelineWarnings.scss'; diff --git a/ts/background.ts b/ts/background.ts index 91a5c99ca..8de7d9658 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -34,6 +34,7 @@ import { RetryRequestType, } from './textsecure/MessageReceiver'; import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials'; +import * as universalExpireTimer from './util/universalExpireTimer'; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; @@ -513,6 +514,15 @@ export async function startApp(): Promise { getLastSyncTime: () => window.storage.get('synced_at'), setLastSyncTime: (value: number) => window.storage.put('synced_at', value), + getUniversalExpireTimer: (): number | undefined => { + return universalExpireTimer.get(); + }, + setUniversalExpireTimer: async ( + newValue: number | undefined + ): Promise => { + await universalExpireTimer.set(newValue); + window.Signal.Services.storageServiceUploadJob(); + }, addDarkOverlay: () => { if ($('.dark-overlay').length) { diff --git a/ts/components/ConfirmationDialog.tsx b/ts/components/ConfirmationDialog.tsx index 8a226c466..41ec37f44 100644 --- a/ts/components/ConfirmationDialog.tsx +++ b/ts/components/ConfirmationDialog.tsx @@ -14,6 +14,7 @@ export type ActionSpec = { }; export type OwnProps = { + readonly moduleClassName?: string; readonly actions?: Array; readonly cancelText?: string; readonly children?: React.ReactNode; @@ -22,6 +23,7 @@ export type OwnProps = { readonly onClose: () => unknown; readonly title?: string | React.ReactNode; readonly theme?: Theme; + readonly hasXButton?: boolean; }; export type Props = OwnProps; @@ -48,6 +50,7 @@ function getButtonVariant( export const ConfirmationDialog = React.memo( ({ + moduleClassName, actions = [], cancelText, children, @@ -56,6 +59,7 @@ export const ConfirmationDialog = React.memo( onClose, theme, title, + hasXButton, }: Props) => { const cancelAndClose = React.useCallback(() => { if (onCancel) { @@ -76,7 +80,14 @@ export const ConfirmationDialog = React.memo( const hasActions = Boolean(actions.length); return ( - + {children}