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}