Universal Disappearing Messages
This commit is contained in:
parent
c63871d71b
commit
19f8042cd3
50 changed files with 1224 additions and 191 deletions
|
@ -1801,7 +1801,7 @@
|
||||||
},
|
},
|
||||||
"disappearingMessages": {
|
"disappearingMessages": {
|
||||||
"message": "Disappearing messages",
|
"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": {
|
"disappearingMessagesDisabled": {
|
||||||
"message": "Disappearing messages disabled",
|
"message": "Disappearing messages disabled",
|
||||||
|
@ -5414,5 +5414,63 @@
|
||||||
"CustomColorEditor__title": {
|
"CustomColorEditor__title": {
|
||||||
"message": "Custom Color",
|
"message": "Custom Color",
|
||||||
"description": "Modal title for the custom color editor"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,9 @@ const {
|
||||||
const {
|
const {
|
||||||
StagedLinkPreview,
|
StagedLinkPreview,
|
||||||
} = require('../../ts/components/conversation/StagedLinkPreview');
|
} = require('../../ts/components/conversation/StagedLinkPreview');
|
||||||
|
const {
|
||||||
|
DisappearingTimeDialog,
|
||||||
|
} = require('../../ts/components/conversation/DisappearingTimeDialog');
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
||||||
|
@ -346,6 +349,7 @@ exports.setup = (options = {}) => {
|
||||||
ProgressModal,
|
ProgressModal,
|
||||||
SafetyNumberChangeDialog,
|
SafetyNumberChangeDialog,
|
||||||
StagedLinkPreview,
|
StagedLinkPreview,
|
||||||
|
DisappearingTimeDialog,
|
||||||
Types: {
|
Types: {
|
||||||
Message: MediaGalleryMessage,
|
Message: MediaGalleryMessage,
|
||||||
},
|
},
|
||||||
|
|
|
@ -48,6 +48,7 @@ const getInitialData = async () => ({
|
||||||
|
|
||||||
isPrimary: await window.isPrimary(),
|
isPrimary: await window.isPrimary(),
|
||||||
lastSyncTime: await window.getLastSyncTime(),
|
lastSyncTime: await window.getLastSyncTime(),
|
||||||
|
universalExpireTimer: await window.getUniversalExpireTimer(),
|
||||||
});
|
});
|
||||||
|
|
||||||
window.initialRequest = getInitialData();
|
window.initialRequest = getInitialData();
|
||||||
|
|
|
@ -12,6 +12,12 @@
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
const { Settings } = window.Signal.Types;
|
const { Settings } = window.Signal.Types;
|
||||||
|
|
||||||
|
const {
|
||||||
|
DEFAULT_DURATIONS_IN_SECONDS,
|
||||||
|
DEFAULT_DURATIONS_SET,
|
||||||
|
format: formatExpirationTimer,
|
||||||
|
} = window.Signal.Util.expirationTimer;
|
||||||
|
|
||||||
const CheckboxView = Whisper.View.extend({
|
const CheckboxView = Whisper.View.extend({
|
||||||
initialize(options) {
|
initialize(options) {
|
||||||
this.name = options.name;
|
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({
|
const RadioButtonGroupView = Whisper.View.extend({
|
||||||
initialize(options) {
|
initialize(options) {
|
||||||
this.name = options.name;
|
this.name = options.name;
|
||||||
|
@ -202,6 +308,15 @@
|
||||||
value: window.initialData.mediaCameraPermissions,
|
value: window.initialData.mediaCameraPermissions,
|
||||||
setFn: window.setMediaCameraPermissions,
|
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) {
|
if (!window.initialData.isPrimary) {
|
||||||
const syncView = new SyncView().render();
|
const syncView = new SyncView().render();
|
||||||
this.$('.sync-setting').append(syncView.el);
|
this.$('.sync-setting').append(syncView.el);
|
||||||
|
|
2
main.js
2
main.js
|
@ -1697,6 +1697,8 @@ installSettingsGetter('is-primary');
|
||||||
installSettingsGetter('sync-request');
|
installSettingsGetter('sync-request');
|
||||||
installSettingsGetter('sync-time');
|
installSettingsGetter('sync-time');
|
||||||
installSettingsSetter('sync-time');
|
installSettingsSetter('sync-time');
|
||||||
|
installSettingsGetter('universal-expire-timer');
|
||||||
|
installSettingsSetter('universal-expire-timer');
|
||||||
|
|
||||||
ipc.on('delete-all-data', () => {
|
ipc.on('delete-all-data', () => {
|
||||||
if (mainWindow && mainWindow.webContents) {
|
if (mainWindow && mainWindow.webContents) {
|
||||||
|
|
|
@ -367,6 +367,8 @@ try {
|
||||||
installGetter('sync-request', 'getSyncRequest');
|
installGetter('sync-request', 'getSyncRequest');
|
||||||
installGetter('sync-time', 'getLastSyncTime');
|
installGetter('sync-time', 'getLastSyncTime');
|
||||||
installSetter('sync-time', 'setLastSyncTime');
|
installSetter('sync-time', 'setLastSyncTime');
|
||||||
|
installGetter('universal-expire-timer', 'getUniversalExpireTimer');
|
||||||
|
installSetter('universal-expire-timer', 'setUniversalExpireTimer');
|
||||||
|
|
||||||
ipc.on('delete-all-data', async () => {
|
ipc.on('delete-all-data', async () => {
|
||||||
const { deleteAllData } = window.Events;
|
const { deleteAllData } = window.Events;
|
||||||
|
|
|
@ -129,5 +129,6 @@ message AccountRecord {
|
||||||
optional PhoneNumberSharingMode phoneNumberSharingMode = 12;
|
optional PhoneNumberSharingMode phoneNumberSharingMode = 12;
|
||||||
optional bool notDiscoverableByPhoneNumber = 13;
|
optional bool notDiscoverableByPhoneNumber = 13;
|
||||||
repeated PinnedConversation pinnedConversations = 14;
|
repeated PinnedConversation pinnedConversations = 14;
|
||||||
|
optional uint32 universalExpireTimer = 17;
|
||||||
optional bool primarySendsSms = 18;
|
optional bool primarySendsSms = 18;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- Copyright 2018-2020 Signal Messenger, LLC -->
|
<!-- Copyright 2018-2021 Signal Messenger, LLC -->
|
||||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
|
@ -38,6 +38,42 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
<script type='text/x-tmpl-mustache' id='disappearingMessagesSettings'>
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
<div class="disappearing-messages-setting__timer {{#customInfo}}disappearing-messages-setting__timer--with-info{{/customInfo}}">
|
||||||
|
<label
|
||||||
|
class="disappearing-messages-setting__timer__label"
|
||||||
|
for='disappearing-messages-timer'
|
||||||
|
>
|
||||||
|
{{ timerLabel }}
|
||||||
|
</label>
|
||||||
|
<div class="disappearing-messages-setting__timer__right">
|
||||||
|
<div class="module-select">
|
||||||
|
<select
|
||||||
|
name='disappearing-messages-timer'
|
||||||
|
id='disappearing-messages-timer'
|
||||||
|
>
|
||||||
|
{{#timerValues}}
|
||||||
|
<option value="{{value}}" {{selected}}>
|
||||||
|
{{ text }}
|
||||||
|
</option>
|
||||||
|
{{/timerValues}}
|
||||||
|
<option value="-1" {{customSelected}}>
|
||||||
|
{{customText}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{{#customInfo}}
|
||||||
|
<div class="disappearing-messages-setting__timer__right__info">
|
||||||
|
{{text}}
|
||||||
|
</div>
|
||||||
|
{{/customInfo}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='disappearing-messages-setting__footer'>
|
||||||
|
{{ footer }}
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
<script type='text/x-tmpl-mustache' id='settings'>
|
<script type='text/x-tmpl-mustache' id='settings'>
|
||||||
<div class='content'>
|
<div class='content'>
|
||||||
<a class='x close' alt='close settings' href='#'></a>
|
<a class='x close' alt='close settings' href='#'></a>
|
||||||
|
@ -163,6 +199,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class='sync-setting'></div>
|
<div class='sync-setting'></div>
|
||||||
<hr>
|
<hr>
|
||||||
|
<div class='disappearing-messages-setting'>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
<div class='clear-data-settings'>
|
<div class='clear-data-settings'>
|
||||||
<h3>{{ clearDataHeader }}</h3>
|
<h3>{{ clearDataHeader }}</h3>
|
||||||
<div>
|
<div>
|
||||||
|
@ -174,6 +213,7 @@
|
||||||
</script>
|
</script>
|
||||||
<script type='text/javascript' src='js/components.js'></script>
|
<script type='text/javascript' src='js/components.js'></script>
|
||||||
<script type='text/javascript' src='ts/backboneJquery.js'></script>
|
<script type='text/javascript' src='ts/backboneJquery.js'></script>
|
||||||
|
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/settings_view.js'></script>
|
<script type='text/javascript' src='js/views/settings_view.js'></script>
|
||||||
<script type='text/javascript' src='js/settings_start.js'></script>
|
<script type='text/javascript' src='js/settings_start.js'></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -98,6 +98,8 @@ window.isPrimary = makeGetter('is-primary');
|
||||||
window.makeSyncRequest = makeGetter('sync-request');
|
window.makeSyncRequest = makeGetter('sync-request');
|
||||||
window.getLastSyncTime = makeGetter('sync-time');
|
window.getLastSyncTime = makeGetter('sync-time');
|
||||||
window.setLastSyncTime = makeSetter('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');
|
window.deleteAllData = () => ipcRenderer.send('delete-all-data');
|
||||||
|
|
||||||
|
@ -130,6 +132,9 @@ function makeSetter(name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Backbone = require('backbone');
|
window.Backbone = require('backbone');
|
||||||
|
window.React = require('react');
|
||||||
|
window.ReactDOM = require('react-dom');
|
||||||
|
|
||||||
require('./ts/backbone/views/whisper_view');
|
require('./ts/backbone/views/whisper_view');
|
||||||
require('./ts/backbone/views/toast_view');
|
require('./ts/backbone/views/toast_view');
|
||||||
require('./ts/logging/set_up_renderer_logging').initialize();
|
require('./ts/logging/set_up_renderer_logging').initialize();
|
||||||
|
|
|
@ -2461,6 +2461,19 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
margin-top: 1px;
|
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 {
|
.module-notification--with-click-handler {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -3096,7 +3109,8 @@ button.module-conversation-details__action-button {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info,
|
||||||
|
&__right-info {
|
||||||
@include font-body-2;
|
@include font-body-2;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
|
||||||
|
@ -3110,7 +3124,17 @@ button.module-conversation-details__action-button {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__right {
|
&__right {
|
||||||
|
position: relative;
|
||||||
color: $color-gray-45;
|
color: $color-gray-45;
|
||||||
|
min-width: 143px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__right-info {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
@include font-subtitle;
|
||||||
|
|
||||||
|
padding-left: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
|
@ -3170,60 +3194,6 @@ button.module-conversation-details__action-button {
|
||||||
@include font-body-1-bold;
|
@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
|
// Module: Message Detail
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
&.modal {
|
&.modal {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -77,4 +78,39 @@
|
||||||
@include font-body-2;
|
@include font-body-2;
|
||||||
color: $color-gray-60;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,30 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
.module-ConversationHeader {
|
.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;
|
--button-spacing: 24px;
|
||||||
|
|
||||||
&.module-ConversationHeader--narrow {
|
&.module-ConversationHeader--narrow {
|
||||||
|
@ -133,37 +157,13 @@
|
||||||
color: $color-gray-25;
|
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 {
|
&__expiration {
|
||||||
@include subtitle-element('../images/icons/v2/timer-24.svg');
|
@include icon-element('../images/icons/v2/timer-24.svg');
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__verified {
|
&__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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
25
stylesheets/components/DisappearingTimeDialog.scss
Normal file
25
stylesheets/components/DisappearingTimeDialog.scss
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
stylesheets/components/Select.scss
Normal file
55
stylesheets/components/Select.scss
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,7 @@
|
||||||
@import './components/ContactSpoofingReviewDialogPerson.scss';
|
@import './components/ContactSpoofingReviewDialogPerson.scss';
|
||||||
@import './components/ConversationHeader.scss';
|
@import './components/ConversationHeader.scss';
|
||||||
@import './components/CustomColorEditor.scss';
|
@import './components/CustomColorEditor.scss';
|
||||||
|
@import './components/DisappearingTimeDialog.scss';
|
||||||
@import './components/EditConversationAttributesModal.scss';
|
@import './components/EditConversationAttributesModal.scss';
|
||||||
@import './components/ForwardMessageModal.scss';
|
@import './components/ForwardMessageModal.scss';
|
||||||
@import './components/GradientDial.scss';
|
@import './components/GradientDial.scss';
|
||||||
|
@ -55,5 +56,6 @@
|
||||||
@import './components/SearchResultsLoadingFakeRow.scss';
|
@import './components/SearchResultsLoadingFakeRow.scss';
|
||||||
@import './components/Slider.scss';
|
@import './components/Slider.scss';
|
||||||
@import './components/Tabs.scss';
|
@import './components/Tabs.scss';
|
||||||
|
@import './components/Select.scss';
|
||||||
@import './components/TimelineWarning.scss';
|
@import './components/TimelineWarning.scss';
|
||||||
@import './components/TimelineWarnings.scss';
|
@import './components/TimelineWarnings.scss';
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {
|
||||||
RetryRequestType,
|
RetryRequestType,
|
||||||
} from './textsecure/MessageReceiver';
|
} from './textsecure/MessageReceiver';
|
||||||
import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials';
|
import { connectToServerWithStoredCredentials } from './util/connectToServerWithStoredCredentials';
|
||||||
|
import * as universalExpireTimer from './util/universalExpireTimer';
|
||||||
|
|
||||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||||
|
|
||||||
|
@ -513,6 +514,15 @@ export async function startApp(): Promise<void> {
|
||||||
getLastSyncTime: () => window.storage.get('synced_at'),
|
getLastSyncTime: () => window.storage.get('synced_at'),
|
||||||
setLastSyncTime: (value: number) =>
|
setLastSyncTime: (value: number) =>
|
||||||
window.storage.put('synced_at', value),
|
window.storage.put('synced_at', value),
|
||||||
|
getUniversalExpireTimer: (): number | undefined => {
|
||||||
|
return universalExpireTimer.get();
|
||||||
|
},
|
||||||
|
setUniversalExpireTimer: async (
|
||||||
|
newValue: number | undefined
|
||||||
|
): Promise<void> => {
|
||||||
|
await universalExpireTimer.set(newValue);
|
||||||
|
window.Signal.Services.storageServiceUploadJob();
|
||||||
|
},
|
||||||
|
|
||||||
addDarkOverlay: () => {
|
addDarkOverlay: () => {
|
||||||
if ($('.dark-overlay').length) {
|
if ($('.dark-overlay').length) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ export type ActionSpec = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
|
readonly moduleClassName?: string;
|
||||||
readonly actions?: Array<ActionSpec>;
|
readonly actions?: Array<ActionSpec>;
|
||||||
readonly cancelText?: string;
|
readonly cancelText?: string;
|
||||||
readonly children?: React.ReactNode;
|
readonly children?: React.ReactNode;
|
||||||
|
@ -22,6 +23,7 @@ export type OwnProps = {
|
||||||
readonly onClose: () => unknown;
|
readonly onClose: () => unknown;
|
||||||
readonly title?: string | React.ReactNode;
|
readonly title?: string | React.ReactNode;
|
||||||
readonly theme?: Theme;
|
readonly theme?: Theme;
|
||||||
|
readonly hasXButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = OwnProps;
|
export type Props = OwnProps;
|
||||||
|
@ -48,6 +50,7 @@ function getButtonVariant(
|
||||||
|
|
||||||
export const ConfirmationDialog = React.memo(
|
export const ConfirmationDialog = React.memo(
|
||||||
({
|
({
|
||||||
|
moduleClassName,
|
||||||
actions = [],
|
actions = [],
|
||||||
cancelText,
|
cancelText,
|
||||||
children,
|
children,
|
||||||
|
@ -56,6 +59,7 @@ export const ConfirmationDialog = React.memo(
|
||||||
onClose,
|
onClose,
|
||||||
theme,
|
theme,
|
||||||
title,
|
title,
|
||||||
|
hasXButton,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const cancelAndClose = React.useCallback(() => {
|
const cancelAndClose = React.useCallback(() => {
|
||||||
if (onCancel) {
|
if (onCancel) {
|
||||||
|
@ -76,7 +80,14 @@ export const ConfirmationDialog = React.memo(
|
||||||
const hasActions = Boolean(actions.length);
|
const hasActions = Boolean(actions.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal i18n={i18n} onClose={cancelAndClose} title={title} theme={theme}>
|
<Modal
|
||||||
|
moduleClassName={moduleClassName}
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={cancelAndClose}
|
||||||
|
title={title}
|
||||||
|
theme={theme}
|
||||||
|
hasXButton={hasXButton}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
<Modal.ButtonFooter>
|
<Modal.ButtonFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|
31
ts/components/Select.stories.tsx
Normal file
31
ts/components/Select.stories.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Select } from './Select';
|
||||||
|
|
||||||
|
const story = storiesOf('Components/Select', module);
|
||||||
|
|
||||||
|
story.add('Normal', () => {
|
||||||
|
const [value, setValue] = useState(0);
|
||||||
|
|
||||||
|
const onChange = action('onChange');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 1, text: '1' },
|
||||||
|
{ value: 2, text: '2' },
|
||||||
|
{ value: 3, text: '3' },
|
||||||
|
]}
|
||||||
|
value={value}
|
||||||
|
onChange={newValue => {
|
||||||
|
onChange(newValue);
|
||||||
|
setValue(parseInt(newValue, 10));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
39
ts/components/Select.tsx
Normal file
39
ts/components/Select.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export type Option = Readonly<{
|
||||||
|
text: string;
|
||||||
|
value: string | number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type PropsType = Readonly<{
|
||||||
|
moduleClassName?: string;
|
||||||
|
options: ReadonlyArray<Option>;
|
||||||
|
onChange(value: string): void;
|
||||||
|
value: string | number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export function Select(props: PropsType): JSX.Element {
|
||||||
|
const { moduleClassName, value, options, onChange } = props;
|
||||||
|
|
||||||
|
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
onChange(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(['module-select', moduleClassName])}>
|
||||||
|
<select value={value} onChange={onSelectChange}>
|
||||||
|
{options.map(({ text, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<option value={optionValue} key={optionValue} aria-label={text}>
|
||||||
|
{text}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -107,7 +107,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
name: 'Joyrey 🔥 Leppey',
|
name: 'Joyrey 🔥 Leppey',
|
||||||
phoneNumber: '(202) 555-0002',
|
phoneNumber: '(202) 555-0002',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
id: '2',
|
id: '3',
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -119,7 +119,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
phoneNumber: '(202) 555-0003',
|
phoneNumber: '(202) 555-0003',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
id: '3',
|
id: '4',
|
||||||
title: '🔥Flames🔥',
|
title: '🔥Flames🔥',
|
||||||
profileName: '🔥Flames🔥',
|
profileName: '🔥Flames🔥',
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
|
@ -132,7 +132,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
title: '(202) 555-0011',
|
title: '(202) 555-0011',
|
||||||
phoneNumber: '(202) 555-0011',
|
phoneNumber: '(202) 555-0011',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
id: '11',
|
id: '5',
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -145,7 +145,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
phoneNumber: '(202) 555-0004',
|
phoneNumber: '(202) 555-0004',
|
||||||
title: '(202) 555-0004',
|
title: '(202) 555-0004',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
id: '4',
|
id: '6',
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -157,7 +157,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
title: '(202) 555-0005',
|
title: '(202) 555-0005',
|
||||||
phoneNumber: '(202) 555-0005',
|
phoneNumber: '(202) 555-0005',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
id: '5',
|
id: '7',
|
||||||
expireTimer: 10,
|
expireTimer: 10,
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
},
|
},
|
||||||
|
@ -170,10 +170,11 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
title: '(202) 555-0005',
|
title: '(202) 555-0005',
|
||||||
phoneNumber: '(202) 555-0005',
|
phoneNumber: '(202) 555-0005',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
id: '5',
|
id: '8',
|
||||||
expireTimer: 60,
|
expireTimer: 300,
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
|
canChangeTimer: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -184,7 +185,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
title: '(202) 555-0006',
|
title: '(202) 555-0006',
|
||||||
phoneNumber: '(202) 555-0006',
|
phoneNumber: '(202) 555-0006',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
id: '6',
|
id: '9',
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
muteExpiresAt: new Date('3000-10-18T11:11:11Z').valueOf(),
|
muteExpiresAt: new Date('3000-10-18T11:11:11Z').valueOf(),
|
||||||
},
|
},
|
||||||
|
@ -197,7 +198,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
title: '(202) 555-0006',
|
title: '(202) 555-0006',
|
||||||
phoneNumber: '(202) 555-0006',
|
phoneNumber: '(202) 555-0006',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
id: '6',
|
id: '10',
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
isSMSOnly: true,
|
isSMSOnly: true,
|
||||||
},
|
},
|
||||||
|
@ -217,7 +218,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
title: 'Typescript support group',
|
title: 'Typescript support group',
|
||||||
name: 'Typescript support group',
|
name: 'Typescript support group',
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
id: '1',
|
id: '11',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
expireTimer: 10,
|
expireTimer: 10,
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
|
@ -232,7 +233,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
title: 'Typescript support group',
|
title: 'Typescript support group',
|
||||||
name: 'Typescript support group',
|
name: 'Typescript support group',
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
id: '2',
|
id: '12',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
left: true,
|
left: true,
|
||||||
expireTimer: 10,
|
expireTimer: 10,
|
||||||
|
@ -248,7 +249,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
title: 'Typescript support group',
|
title: 'Typescript support group',
|
||||||
name: 'Typescript support group',
|
name: 'Typescript support group',
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
id: '1',
|
id: '13',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
expireTimer: 10,
|
expireTimer: 10,
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
|
@ -263,7 +264,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
title: 'Way too many messages',
|
title: 'Way too many messages',
|
||||||
name: 'Way too many messages',
|
name: 'Way too many messages',
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
id: '1',
|
id: '14',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
expireTimer: 10,
|
expireTimer: 10,
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
|
@ -284,7 +285,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
title: '(202) 555-0007',
|
title: '(202) 555-0007',
|
||||||
phoneNumber: '(202) 555-0007',
|
phoneNumber: '(202) 555-0007',
|
||||||
id: '7',
|
id: '15',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: true,
|
isMe: true,
|
||||||
acceptedMessageRequest: true,
|
acceptedMessageRequest: true,
|
||||||
|
@ -304,7 +305,7 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
title: '(202) 555-0007',
|
title: '(202) 555-0007',
|
||||||
phoneNumber: '(202) 555-0007',
|
phoneNumber: '(202) 555-0007',
|
||||||
id: '7',
|
id: '16',
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
acceptedMessageRequest: false,
|
acceptedMessageRequest: false,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from 'react-contextmenu';
|
} from 'react-contextmenu';
|
||||||
|
|
||||||
import { Emojify } from './Emojify';
|
import { Emojify } from './Emojify';
|
||||||
|
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
|
||||||
import { Avatar, AvatarSize } from '../Avatar';
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
import { InContactsIcon } from '../InContactsIcon';
|
import { InContactsIcon } from '../InContactsIcon';
|
||||||
|
|
||||||
|
@ -92,10 +93,18 @@ export type PropsType = PropsDataType &
|
||||||
PropsActionsType &
|
PropsActionsType &
|
||||||
PropsHousekeepingType;
|
PropsHousekeepingType;
|
||||||
|
|
||||||
|
enum ModalState {
|
||||||
|
NothingOpen,
|
||||||
|
CustomDisappearingTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
type StateType = {
|
type StateType = {
|
||||||
isNarrow: boolean;
|
isNarrow: boolean;
|
||||||
|
modalState: ModalState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TIMER_ITEM_CLASS = 'module-ConversationHeader__disappearing-timer__item';
|
||||||
|
|
||||||
export class ConversationHeader extends React.Component<PropsType, StateType> {
|
export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
private showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
private showMenuBound: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
|
||||||
|
@ -106,7 +115,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
public constructor(props: PropsType) {
|
public constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = { isNarrow: false };
|
this.state = { isNarrow: false, modalState: ModalState.NothingOpen };
|
||||||
|
|
||||||
this.menuTriggerRef = React.createRef();
|
this.menuTriggerRef = React.createRef();
|
||||||
this.showMenuBound = this.showMenu.bind(this);
|
this.showMenuBound = this.showMenu.bind(this);
|
||||||
|
@ -355,6 +364,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
i18n,
|
i18n,
|
||||||
acceptedMessageRequest,
|
acceptedMessageRequest,
|
||||||
canChangeTimer,
|
canChangeTimer,
|
||||||
|
expireTimer,
|
||||||
isArchived,
|
isArchived,
|
||||||
isMe,
|
isMe,
|
||||||
isPinned,
|
isPinned,
|
||||||
|
@ -427,23 +437,60 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
|
|
||||||
const hasGV2AdminEnabled = isGroup && groupVersion === 2;
|
const hasGV2AdminEnabled = isGroup && groupVersion === 2;
|
||||||
|
|
||||||
|
const isActiveExpireTimer = (value: number): boolean => {
|
||||||
|
if (!expireTimer) {
|
||||||
|
return value === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom time...
|
||||||
|
if (value === -1) {
|
||||||
|
return !expirationTimer.DEFAULT_DURATIONS_SET.has(expireTimer);
|
||||||
|
}
|
||||||
|
return value === expireTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const expireDurations: ReadonlyArray<ReactNode> = [
|
||||||
|
...expirationTimer.DEFAULT_DURATIONS_IN_SECONDS,
|
||||||
|
-1,
|
||||||
|
].map((seconds: number) => {
|
||||||
|
let text: string;
|
||||||
|
|
||||||
|
if (seconds === -1) {
|
||||||
|
text = i18n('customDisappearingTimeOption');
|
||||||
|
} else {
|
||||||
|
text = expirationTimer.format(i18n, seconds, {
|
||||||
|
capitalizeOff: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDurationClick = () => {
|
||||||
|
if (seconds === -1) {
|
||||||
|
this.setState({
|
||||||
|
modalState: ModalState.CustomDisappearingTimeout,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onSetDisappearingMessages(seconds);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem key={seconds} onClick={onDurationClick}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
TIMER_ITEM_CLASS,
|
||||||
|
isActiveExpireTimer(seconds) && `${TIMER_ITEM_CLASS}--active`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu id={triggerId}>
|
<ContextMenu id={triggerId}>
|
||||||
{disableTimerChanges ? null : (
|
{disableTimerChanges ? null : (
|
||||||
<SubMenu title={disappearingTitle}>
|
<SubMenu title={disappearingTitle}>{expireDurations}</SubMenu>
|
||||||
{expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(
|
|
||||||
(seconds: number) => (
|
|
||||||
<MenuItem
|
|
||||||
key={seconds}
|
|
||||||
onClick={() => {
|
|
||||||
onSetDisappearingMessages(seconds);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{expirationTimer.format(i18n, seconds)}
|
|
||||||
</MenuItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SubMenu>
|
|
||||||
)}
|
)}
|
||||||
<SubMenu title={muteTitle}>
|
<SubMenu title={muteTitle}>
|
||||||
{muteOptions.map(item => (
|
{muteOptions.map(item => (
|
||||||
|
@ -578,36 +625,64 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
const { id, isSMSOnly } = this.props;
|
const {
|
||||||
const { isNarrow } = this.state;
|
id,
|
||||||
|
isSMSOnly,
|
||||||
|
i18n,
|
||||||
|
onSetDisappearingMessages,
|
||||||
|
expireTimer,
|
||||||
|
} = this.props;
|
||||||
|
const { isNarrow, modalState } = this.state;
|
||||||
const triggerId = `conversation-${id}`;
|
const triggerId = `conversation-${id}`;
|
||||||
|
|
||||||
|
let modalNode: ReactNode;
|
||||||
|
if (modalState === ModalState.NothingOpen) {
|
||||||
|
modalNode = undefined;
|
||||||
|
} else if (modalState === ModalState.CustomDisappearingTimeout) {
|
||||||
|
modalNode = (
|
||||||
|
<DisappearingTimeDialog
|
||||||
|
i18n={i18n}
|
||||||
|
initialValue={expireTimer}
|
||||||
|
onSubmit={value => {
|
||||||
|
this.setState({ modalState: ModalState.NothingOpen });
|
||||||
|
onSetDisappearingMessages(value);
|
||||||
|
}}
|
||||||
|
onClose={() => this.setState({ modalState: ModalState.NothingOpen })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(modalState);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Measure
|
<>
|
||||||
bounds
|
{modalNode}
|
||||||
onResize={({ bounds }) => {
|
<Measure
|
||||||
if (!bounds || !bounds.width) {
|
bounds
|
||||||
return;
|
onResize={({ bounds }) => {
|
||||||
}
|
if (!bounds || !bounds.width) {
|
||||||
this.setState({ isNarrow: bounds.width < 500 });
|
return;
|
||||||
}}
|
}
|
||||||
>
|
this.setState({ isNarrow: bounds.width < 500 });
|
||||||
{({ measureRef }) => (
|
}}
|
||||||
<div
|
>
|
||||||
className={classNames('module-ConversationHeader', {
|
{({ measureRef }) => (
|
||||||
'module-ConversationHeader--narrow': isNarrow,
|
<div
|
||||||
})}
|
className={classNames('module-ConversationHeader', {
|
||||||
ref={measureRef}
|
'module-ConversationHeader--narrow': isNarrow,
|
||||||
>
|
})}
|
||||||
{this.renderBackButton()}
|
ref={measureRef}
|
||||||
{this.renderHeader()}
|
>
|
||||||
{!isSMSOnly && this.renderOutgoingCallButtons()}
|
{this.renderBackButton()}
|
||||||
{this.renderSearchButton()}
|
{this.renderHeader()}
|
||||||
{this.renderMoreButton(triggerId)}
|
{!isSMSOnly && this.renderOutgoingCallButtons()}
|
||||||
{this.renderMenu(triggerId)}
|
{this.renderSearchButton()}
|
||||||
</div>
|
{this.renderMoreButton(triggerId)}
|
||||||
)}
|
{this.renderMenu(triggerId)}
|
||||||
</Measure>
|
</div>
|
||||||
|
)}
|
||||||
|
</Measure>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { EXPIRE_TIMERS } from '../../test-both/util/expireTimers';
|
||||||
|
|
||||||
|
const story = storiesOf('Components/DisappearingTimeDialog', module);
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
EXPIRE_TIMERS.forEach(({ value, label }) => {
|
||||||
|
story.add(`Initial value: ${label}`, () => {
|
||||||
|
return (
|
||||||
|
<DisappearingTimeDialog
|
||||||
|
i18n={i18n}
|
||||||
|
initialValue={value}
|
||||||
|
onSubmit={action('onSubmit')}
|
||||||
|
onClose={action('onClose')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
124
ts/components/conversation/DisappearingTimeDialog.tsx
Normal file
124
ts/components/conversation/DisappearingTimeDialog.tsx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
|
import { Select } from '../Select';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
import { Theme } from '../../util/theme';
|
||||||
|
|
||||||
|
const CSS_MODULE = 'module-disappearing-time-dialog';
|
||||||
|
|
||||||
|
const DEFAULT_VALUE = 60;
|
||||||
|
|
||||||
|
export type PropsType = Readonly<{
|
||||||
|
i18n: LocalizerType;
|
||||||
|
theme?: Theme;
|
||||||
|
initialValue?: number;
|
||||||
|
onSubmit: (value: number) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const UNITS = ['seconds', 'minutes', 'hours', 'days', 'weeks'];
|
||||||
|
|
||||||
|
const UNIT_TO_MS = new Map<string, number>([
|
||||||
|
['seconds', 1],
|
||||||
|
['minutes', 60],
|
||||||
|
['hours', 60 * 60],
|
||||||
|
['days', 24 * 60 * 60],
|
||||||
|
['weeks', 7 * 24 * 60 * 60],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const RANGES = new Map<string, [number, number]>([
|
||||||
|
['seconds', [1, 60]],
|
||||||
|
['minutes', [1, 60]],
|
||||||
|
['hours', [1, 24]],
|
||||||
|
['days', [1, 7]],
|
||||||
|
['weeks', [1, 5]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function DisappearingTimeDialog(props: PropsType): JSX.Element {
|
||||||
|
const {
|
||||||
|
i18n,
|
||||||
|
theme,
|
||||||
|
initialValue = DEFAULT_VALUE,
|
||||||
|
onSubmit,
|
||||||
|
onClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
let initialUnit = 'seconds';
|
||||||
|
let initialUnitValue = 1;
|
||||||
|
for (const unit of UNITS) {
|
||||||
|
const ms = UNIT_TO_MS.get(unit) || 1;
|
||||||
|
|
||||||
|
if (initialValue < ms) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialUnit = unit;
|
||||||
|
initialUnitValue = Math.floor(initialValue / ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [unitValue, setUnitValue] = useState(initialUnitValue);
|
||||||
|
const [unit, setUnit] = useState(initialUnit);
|
||||||
|
|
||||||
|
const range = RANGES.get(unit) || [1, 1];
|
||||||
|
|
||||||
|
const values: Array<number> = [];
|
||||||
|
for (let i = range[0]; i < range[1]; i += 1) {
|
||||||
|
values.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationDialog
|
||||||
|
moduleClassName={CSS_MODULE}
|
||||||
|
i18n={i18n}
|
||||||
|
theme={theme}
|
||||||
|
onClose={onClose}
|
||||||
|
title={i18n('DisappearingTimeDialog__title')}
|
||||||
|
hasXButton
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
text: i18n('DisappearingTimeDialog__set'),
|
||||||
|
style: 'affirmative',
|
||||||
|
action() {
|
||||||
|
onSubmit(unitValue * (UNIT_TO_MS.get(unit) || 1));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<p>{i18n('DisappearingTimeDialog__body')}</p>
|
||||||
|
<section className={`${CSS_MODULE}__time-boxes`}>
|
||||||
|
<Select
|
||||||
|
moduleClassName={`${CSS_MODULE}__time-boxes__value`}
|
||||||
|
value={unitValue}
|
||||||
|
onChange={newValue => setUnitValue(parseInt(newValue, 10))}
|
||||||
|
options={values.map(value => ({ value, text: value.toString() }))}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
moduleClassName={`${CSS_MODULE}__time-boxes__units`}
|
||||||
|
value={unit}
|
||||||
|
onChange={newUnit => {
|
||||||
|
setUnit(newUnit);
|
||||||
|
|
||||||
|
const ranges = RANGES.get(newUnit);
|
||||||
|
if (!ranges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [min, max] = ranges;
|
||||||
|
setUnitValue(Math.max(min, Math.min(max - 1, unitValue)));
|
||||||
|
}}
|
||||||
|
options={UNITS.map(unitName => {
|
||||||
|
return {
|
||||||
|
value: unitName,
|
||||||
|
text: i18n(`DisappearingTimeDialog__${unitName}`),
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</ConfirmationDialog>
|
||||||
|
);
|
||||||
|
}
|
|
@ -298,6 +298,9 @@ const renderItem = (id: string) => (
|
||||||
conversationId=""
|
conversationId=""
|
||||||
conversationAccepted
|
conversationAccepted
|
||||||
renderContact={() => '*ContactName*'}
|
renderContact={() => '*ContactName*'}
|
||||||
|
renderUniversalTimerNotification={() => (
|
||||||
|
<div>*UniversalTimerNotification*</div>
|
||||||
|
)}
|
||||||
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
|
renderAudioAttachment={() => <div>*AudioAttachment*</div>}
|
||||||
{...actions()}
|
{...actions()}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
|
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
|
||||||
|
import { UniversalTimerNotification } from './UniversalTimerNotification';
|
||||||
import { CallMode } from '../../types/Calling';
|
import { CallMode } from '../../types/Calling';
|
||||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||||
|
|
||||||
|
@ -34,6 +35,10 @@ const renderContact = (conversationId: string) => (
|
||||||
<React.Fragment key={conversationId}>{conversationId}</React.Fragment>
|
<React.Fragment key={conversationId}>{conversationId}</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderUniversalTimerNotification = () => (
|
||||||
|
<UniversalTimerNotification i18n={i18n} expireTimer={3600} />
|
||||||
|
);
|
||||||
|
|
||||||
const getDefaultProps = () => ({
|
const getDefaultProps = () => ({
|
||||||
conversationId: 'conversation-id',
|
conversationId: 'conversation-id',
|
||||||
conversationAccepted: true,
|
conversationAccepted: true,
|
||||||
|
@ -73,6 +78,7 @@ const getDefaultProps = () => ({
|
||||||
returnToActiveCall: action('returnToActiveCall'),
|
returnToActiveCall: action('returnToActiveCall'),
|
||||||
|
|
||||||
renderContact,
|
renderContact,
|
||||||
|
renderUniversalTimerNotification,
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||||
});
|
});
|
||||||
|
@ -115,6 +121,10 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
||||||
sender: getDefaultConversation(),
|
sender: getDefaultConversation(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'universalTimerNotification',
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -90,6 +90,10 @@ type TimerNotificationType = {
|
||||||
type: 'timerNotification';
|
type: 'timerNotification';
|
||||||
data: TimerNotificationProps;
|
data: TimerNotificationProps;
|
||||||
};
|
};
|
||||||
|
type UniversalTimerNotificationType = {
|
||||||
|
type: 'universalTimerNotification';
|
||||||
|
data: null;
|
||||||
|
};
|
||||||
type SafetyNumberNotificationType = {
|
type SafetyNumberNotificationType = {
|
||||||
type: 'safetyNumberNotification';
|
type: 'safetyNumberNotification';
|
||||||
data: SafetyNumberNotificationProps;
|
data: SafetyNumberNotificationProps;
|
||||||
|
@ -132,6 +136,7 @@ export type TimelineItemType =
|
||||||
| ResetSessionNotificationType
|
| ResetSessionNotificationType
|
||||||
| SafetyNumberNotificationType
|
| SafetyNumberNotificationType
|
||||||
| TimerNotificationType
|
| TimerNotificationType
|
||||||
|
| UniversalTimerNotificationType
|
||||||
| UnsupportedMessageType
|
| UnsupportedMessageType
|
||||||
| VerificationNotificationType;
|
| VerificationNotificationType;
|
||||||
|
|
||||||
|
@ -143,6 +148,7 @@ type PropsLocalType = {
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
selectMessage: (messageId: string, conversationId: string) => unknown;
|
selectMessage: (messageId: string, conversationId: string) => unknown;
|
||||||
renderContact: SmartContactRendererType;
|
renderContact: SmartContactRendererType;
|
||||||
|
renderUniversalTimerNotification: () => JSX.Element;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
interactionMode: InteractionModeType;
|
interactionMode: InteractionModeType;
|
||||||
theme?: ThemeType;
|
theme?: ThemeType;
|
||||||
|
@ -169,6 +175,7 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
theme,
|
theme,
|
||||||
messageSizeChanged,
|
messageSizeChanged,
|
||||||
renderContact,
|
renderContact,
|
||||||
|
renderUniversalTimerNotification,
|
||||||
returnToActiveCall,
|
returnToActiveCall,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
startCallingLobby,
|
startCallingLobby,
|
||||||
|
@ -225,6 +232,8 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
notification = (
|
notification = (
|
||||||
<TimerNotification {...this.props} {...item.data} i18n={i18n} />
|
<TimerNotification {...this.props} {...item.data} i18n={i18n} />
|
||||||
);
|
);
|
||||||
|
} else if (item.type === 'universalTimerNotification') {
|
||||||
|
notification = renderUniversalTimerNotification();
|
||||||
} else if (item.type === 'safetyNumberNotification') {
|
} else if (item.type === 'safetyNumberNotification') {
|
||||||
notification = (
|
notification = (
|
||||||
<SafetyNumberNotification {...this.props} {...item.data} i18n={i18n} />
|
<SafetyNumberNotification {...this.props} {...item.data} i18n={i18n} />
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
import { UniversalTimerNotification } from './UniversalTimerNotification';
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { EXPIRE_TIMERS } from '../../test-both/util/expireTimers';
|
||||||
|
|
||||||
|
const story = storiesOf('Components/UniversalTimerNotification', module);
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
EXPIRE_TIMERS.forEach(({ value, label }) => {
|
||||||
|
story.add(`Initial value: ${label}`, () => {
|
||||||
|
return <UniversalTimerNotification i18n={i18n} expireTimer={value} />;
|
||||||
|
});
|
||||||
|
});
|
28
ts/components/conversation/UniversalTimerNotification.tsx
Normal file
28
ts/components/conversation/UniversalTimerNotification.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
import * as expirationTimer from '../../util/expirationTimer';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
expireTimer: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UniversalTimerNotification: React.FC<Props> = props => {
|
||||||
|
const { i18n, expireTimer } = props;
|
||||||
|
|
||||||
|
if (!expireTimer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-universal-timer-notification">
|
||||||
|
{i18n('UniversalTimerNotification__text', {
|
||||||
|
timeValue: expirationTimer.format(i18n, expireTimer),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -29,13 +29,18 @@ const conversation: ConversationType = getDefaultConversation({
|
||||||
conversationColor: 'ultramarine' as const,
|
conversationColor: 'ultramarine' as const,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createProps = (hasGroupLink = false): Props => ({
|
const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
||||||
addMembers: async () => {
|
addMembers: async () => {
|
||||||
action('addMembers');
|
action('addMembers');
|
||||||
},
|
},
|
||||||
canEditGroupInfo: false,
|
canEditGroupInfo: false,
|
||||||
candidateContactsToAdd: times(10, () => getDefaultConversation()),
|
candidateContactsToAdd: times(10, () => getDefaultConversation()),
|
||||||
conversation,
|
conversation: expireTimer
|
||||||
|
? {
|
||||||
|
...conversation,
|
||||||
|
expireTimer,
|
||||||
|
}
|
||||||
|
: conversation,
|
||||||
hasGroupLink,
|
hasGroupLink,
|
||||||
i18n,
|
i18n,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
|
@ -122,6 +127,12 @@ story.add('Group Editable', () => {
|
||||||
return <ConversationDetails {...props} canEditGroupInfo />;
|
return <ConversationDetails {...props} canEditGroupInfo />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('Group Editable with custom disappearing timeout', () => {
|
||||||
|
const props = createProps(false, 3 * 24 * 60 * 60);
|
||||||
|
|
||||||
|
return <ConversationDetails {...props} canEditGroupInfo />;
|
||||||
|
});
|
||||||
|
|
||||||
story.add('Group Links On', () => {
|
story.add('Group Links On', () => {
|
||||||
const props = createProps(true);
|
const props = createProps(true);
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,10 @@ import { LocalizerType } from '../../../types/Util';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../LightboxGallery';
|
||||||
import { missingCaseError } from '../../../util/missingCaseError';
|
import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
|
|
||||||
|
import { Select } from '../../Select';
|
||||||
|
|
||||||
|
import { DisappearingTimeDialog } from '../DisappearingTimeDialog';
|
||||||
|
|
||||||
import { PanelRow } from './PanelRow';
|
import { PanelRow } from './PanelRow';
|
||||||
import { PanelSection } from './PanelSection';
|
import { PanelSection } from './PanelSection';
|
||||||
import { AddGroupMembersModal } from './AddGroupMembersModal';
|
import { AddGroupMembersModal } from './AddGroupMembersModal';
|
||||||
|
@ -34,6 +38,7 @@ enum ModalState {
|
||||||
NothingOpen,
|
NothingOpen,
|
||||||
EditingGroupAttributes,
|
EditingGroupAttributes,
|
||||||
AddingGroupMembers,
|
AddingGroupMembers,
|
||||||
|
CustomDisappearingTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StateProps = {
|
export type StateProps = {
|
||||||
|
@ -71,10 +76,6 @@ export type StateProps = {
|
||||||
|
|
||||||
export type Props = StateProps;
|
export type Props = StateProps;
|
||||||
|
|
||||||
const expirationTimerDefaultSet = new Set<number>(
|
|
||||||
expirationTimer.DEFAULT_DURATIONS_IN_SECONDS
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ConversationDetails: React.ComponentType<Props> = ({
|
export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
addMembers,
|
addMembers,
|
||||||
canEditGroupInfo,
|
canEditGroupInfo,
|
||||||
|
@ -111,8 +112,13 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
setAddGroupMembersRequestState,
|
setAddGroupMembersRequestState,
|
||||||
] = useState<RequestState>(RequestState.Inactive);
|
] = useState<RequestState>(RequestState.Inactive);
|
||||||
|
|
||||||
const updateExpireTimer = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const updateExpireTimer = (value: string) => {
|
||||||
setDisappearingMessages(parseInt(event.target.value, 10));
|
const intValue = parseInt(value, 10);
|
||||||
|
if (intValue === -1) {
|
||||||
|
setModalState(ModalState.CustomDisappearingTimeout);
|
||||||
|
} else {
|
||||||
|
setDisappearingMessages(intValue);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (conversation === undefined) {
|
if (conversation === undefined) {
|
||||||
|
@ -204,16 +210,54 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case ModalState.CustomDisappearingTimeout:
|
||||||
|
modalNode = (
|
||||||
|
<DisappearingTimeDialog
|
||||||
|
i18n={i18n}
|
||||||
|
initialValue={conversation.expireTimer}
|
||||||
|
onSubmit={value => {
|
||||||
|
setModalState(ModalState.NothingOpen);
|
||||||
|
setDisappearingMessages(value);
|
||||||
|
}}
|
||||||
|
onClose={() => setModalState(ModalState.NothingOpen)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(modalState);
|
throw missingCaseError(modalState);
|
||||||
}
|
}
|
||||||
|
|
||||||
const expireTimer = conversation.expireTimer || 0;
|
const expireTimer: number = conversation.expireTimer || 0;
|
||||||
|
|
||||||
let expirationTimerDurations = expirationTimer.DEFAULT_DURATIONS_IN_SECONDS;
|
let expirationTimerOptions: ReadonlyArray<{
|
||||||
if (!expirationTimerDefaultSet.has(expireTimer)) {
|
readonly value: number;
|
||||||
expirationTimerDurations = [...expirationTimerDurations, expireTimer];
|
readonly text: string;
|
||||||
}
|
}> = expirationTimer.DEFAULT_DURATIONS_IN_SECONDS.map(seconds => {
|
||||||
|
const text = expirationTimer.format(i18n, seconds, {
|
||||||
|
capitalizeOff: true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
value: seconds,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const isCustomTimeSelected = !expirationTimer.DEFAULT_DURATIONS_SET.has(
|
||||||
|
expireTimer
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom time...
|
||||||
|
expirationTimerOptions = [
|
||||||
|
...expirationTimerOptions,
|
||||||
|
{
|
||||||
|
value: -1,
|
||||||
|
text: i18n(
|
||||||
|
isCustomTimeSelected
|
||||||
|
? 'selectedCustomDisappearingTimeOption'
|
||||||
|
: 'customDisappearingTimeOption'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="conversation-details-panel">
|
<div className="conversation-details-panel">
|
||||||
|
@ -241,18 +285,16 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
info={i18n('ConversationDetails--disappearing-messages-info')}
|
info={i18n('ConversationDetails--disappearing-messages-info')}
|
||||||
label={i18n('ConversationDetails--disappearing-messages-label')}
|
label={i18n('ConversationDetails--disappearing-messages-label')}
|
||||||
right={
|
right={
|
||||||
<div className="module-conversation-details-select">
|
<Select
|
||||||
<select onChange={updateExpireTimer} value={expireTimer}>
|
onChange={updateExpireTimer}
|
||||||
{expirationTimerDurations.map((seconds: number) => {
|
value={isCustomTimeSelected ? -1 : expireTimer}
|
||||||
const label = expirationTimer.format(i18n, seconds);
|
options={expirationTimerOptions}
|
||||||
return (
|
/>
|
||||||
<option value={seconds} key={seconds} aria-label={label}>
|
}
|
||||||
{label}
|
rightInfo={
|
||||||
</option>
|
isCustomTimeSelected
|
||||||
);
|
? expirationTimer.format(i18n, expireTimer)
|
||||||
})}
|
: undefined
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export type Props = {
|
||||||
label: string | React.ReactNode;
|
label: string | React.ReactNode;
|
||||||
info?: string;
|
info?: string;
|
||||||
right?: string | React.ReactNode;
|
right?: string | React.ReactNode;
|
||||||
|
rightInfo?: string;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
@ -27,6 +28,7 @@ export const PanelRow: React.ComponentType<Props> = ({
|
||||||
label,
|
label,
|
||||||
info,
|
info,
|
||||||
right,
|
right,
|
||||||
|
rightInfo,
|
||||||
actions,
|
actions,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -37,7 +39,14 @@ export const PanelRow: React.ComponentType<Props> = ({
|
||||||
<div>{label}</div>
|
<div>{label}</div>
|
||||||
{info !== undefined ? <div className={bem('info')}>{info}</div> : null}
|
{info !== undefined ? <div className={bem('info')}>{info}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
{right !== undefined ? <div className={bem('right')}>{right}</div> : null}
|
{right !== undefined ? (
|
||||||
|
<div className={bem('right')}>
|
||||||
|
{right}
|
||||||
|
{rightInfo !== undefined ? (
|
||||||
|
<div className={bem('right-info')}>{rightInfo}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{actions !== undefined ? (
|
{actions !== undefined ? (
|
||||||
<div className={alwaysShowActions ? '' : bem('actions')}>{actions}</div>
|
<div className={alwaysShowActions ? '' : bem('actions')}>{actions}</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -47,6 +47,7 @@ import {
|
||||||
getClientZkGroupCipher,
|
getClientZkGroupCipher,
|
||||||
getClientZkProfileOperations,
|
getClientZkProfileOperations,
|
||||||
} from './util/zkgroup';
|
} from './util/zkgroup';
|
||||||
|
import * as universalExpireTimer from './util/universalExpireTimer';
|
||||||
import {
|
import {
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
arrayBufferToHex,
|
arrayBufferToHex,
|
||||||
|
@ -1675,6 +1676,11 @@ export async function createGroupV2({
|
||||||
window.MessageController.register(model.id, model);
|
window.MessageController.register(model.id, model);
|
||||||
conversation.trigger('newmessage', model);
|
conversation.trigger('newmessage', model);
|
||||||
|
|
||||||
|
const expireTimer = universalExpireTimer.get();
|
||||||
|
if (expireTimer) {
|
||||||
|
await conversation.updateExpirationTimer(expireTimer);
|
||||||
|
}
|
||||||
|
|
||||||
return conversation;
|
return conversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
ts/model-types.d.ts
vendored
2
ts/model-types.d.ts
vendored
|
@ -141,6 +141,7 @@ export type MessageAttributesType = {
|
||||||
| 'outgoing'
|
| 'outgoing'
|
||||||
| 'profile-change'
|
| 'profile-change'
|
||||||
| 'timer-notification'
|
| 'timer-notification'
|
||||||
|
| 'universal-timer-notification'
|
||||||
| 'verified-change';
|
| 'verified-change';
|
||||||
body: string;
|
body: string;
|
||||||
attachments: Array<WhatIsThis>;
|
attachments: Array<WhatIsThis>;
|
||||||
|
@ -254,6 +255,7 @@ export type ConversationAttributesType = {
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
verified?: number;
|
verified?: number;
|
||||||
profileLastFetchedAt?: number;
|
profileLastFetchedAt?: number;
|
||||||
|
pendingUniversalTimer?: string;
|
||||||
|
|
||||||
// Group-only
|
// Group-only
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
|
|
|
@ -55,6 +55,7 @@ import { getConversationMembers } from '../util/getConversationMembers';
|
||||||
import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor';
|
import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor';
|
||||||
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
|
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
|
||||||
import { filter, map, take } from '../util/iterables';
|
import { filter, map, take } from '../util/iterables';
|
||||||
|
import * as universalExpireTimer from '../util/universalExpireTimer';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
@ -1069,10 +1070,15 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On successful fetch - mark contact as registered.
|
if (!this.get('uuid')) {
|
||||||
if (this.get('uuid')) {
|
return;
|
||||||
this.setRegistered();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On successful fetch - mark contact as registered.
|
||||||
|
this.setRegistered();
|
||||||
|
|
||||||
|
// If we couldn't apply universal timer before - try it again.
|
||||||
|
this.queueJob(() => this.maybeSetPendingUniversalTimer());
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid(): boolean {
|
isValid(): boolean {
|
||||||
|
@ -2749,6 +2755,85 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addUniversalTimerNotification(): Promise<string> {
|
||||||
|
const now = Date.now();
|
||||||
|
const message = ({
|
||||||
|
conversationId: this.id,
|
||||||
|
type: 'universal-timer-notification',
|
||||||
|
sent_at: now,
|
||||||
|
received_at: window.Signal.Util.incrementMessageCounter(),
|
||||||
|
received_at_ms: now,
|
||||||
|
unread: 0,
|
||||||
|
// TODO: DESKTOP-722
|
||||||
|
} as unknown) as typeof window.Whisper.MessageAttributesType;
|
||||||
|
|
||||||
|
const id = await window.Signal.Data.saveMessage(message, {
|
||||||
|
Message: window.Whisper.Message,
|
||||||
|
});
|
||||||
|
const model = window.MessageController.register(
|
||||||
|
id,
|
||||||
|
new window.Whisper.Message({
|
||||||
|
...message,
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.trigger('newmessage', model);
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async maybeSetPendingUniversalTimer(): Promise<void> {
|
||||||
|
if (!this.isPrivate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isSMSOnly()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.get('pendingUniversalTimer') || this.get('expireTimer')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeAt = this.get('active_at');
|
||||||
|
if (activeAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expireTimer = universalExpireTimer.get();
|
||||||
|
if (!expireTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationId = await this.addUniversalTimerNotification();
|
||||||
|
this.set('pendingUniversalTimer', notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async maybeApplyUniversalTimer(): Promise<void> {
|
||||||
|
const notificationId = this.get('pendingUniversalTimer');
|
||||||
|
if (!notificationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = window.MessageController.getById(notificationId);
|
||||||
|
if (message) {
|
||||||
|
message.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.get('expireTimer')) {
|
||||||
|
this.set('pendingUniversalTimer', undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expireTimer = universalExpireTimer.get();
|
||||||
|
if (expireTimer) {
|
||||||
|
await this.updateExpirationTimer(expireTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('pendingUniversalTimer', undefined);
|
||||||
|
}
|
||||||
|
|
||||||
async onReadMessage(
|
async onReadMessage(
|
||||||
message: MessageModel,
|
message: MessageModel,
|
||||||
readAt?: number
|
readAt?: number
|
||||||
|
@ -3243,7 +3328,6 @@ export class ConversationModel extends window.Backbone
|
||||||
): Promise<WhatIsThis> {
|
): Promise<WhatIsThis> {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const outgoingReaction = { ...reaction, ...target };
|
const outgoingReaction = { ...reaction, ...target };
|
||||||
const expireTimer = this.get('expireTimer');
|
|
||||||
|
|
||||||
const reactionModel = window.Whisper.Reactions.add({
|
const reactionModel = window.Whisper.Reactions.add({
|
||||||
...outgoingReaction,
|
...outgoingReaction,
|
||||||
|
@ -3269,6 +3353,10 @@ export class ConversationModel extends window.Backbone
|
||||||
timestamp
|
timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.maybeApplyUniversalTimer();
|
||||||
|
|
||||||
|
const expireTimer = this.get('expireTimer');
|
||||||
|
|
||||||
const attributes = ({
|
const attributes = ({
|
||||||
id: window.getGuid(),
|
id: window.getGuid(),
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
|
@ -3447,12 +3535,15 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const destination = this.getSendTarget()!;
|
const destination = this.getSendTarget()!;
|
||||||
const expireTimer = this.get('expireTimer');
|
|
||||||
const recipients = this.getRecipients();
|
const recipients = this.getRecipients();
|
||||||
|
|
||||||
this.queueJob(async () => {
|
this.queueJob(async () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
|
await this.maybeApplyUniversalTimer();
|
||||||
|
|
||||||
|
const expireTimer = this.get('expireTimer');
|
||||||
|
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'Sending message to conversation',
|
'Sending message to conversation',
|
||||||
this.idForLogging(),
|
this.idForLogging(),
|
||||||
|
@ -3655,6 +3746,8 @@ export class ConversationModel extends window.Backbone
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.queueJob(() => this.maybeSetPendingUniversalTimer());
|
||||||
|
|
||||||
const ourConversationId = window.ConversationController.getOurConversationId();
|
const ourConversationId = window.ConversationController.getOurConversationId();
|
||||||
if (!ourConversationId) {
|
if (!ourConversationId) {
|
||||||
throw new Error('updateLastMessage: Failed to fetch ourConversationId');
|
throw new Error('updateLastMessage: Failed to fetch ourConversationId');
|
||||||
|
@ -3915,8 +4008,8 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
async updateExpirationTimer(
|
async updateExpirationTimer(
|
||||||
providedExpireTimer: number | undefined,
|
providedExpireTimer: number | undefined,
|
||||||
providedSource: unknown,
|
providedSource?: unknown,
|
||||||
receivedAt: number,
|
receivedAt?: number,
|
||||||
options: { fromSync?: unknown; fromGroupUpdate?: unknown } = {}
|
options: { fromSync?: unknown; fromGroupUpdate?: unknown } = {}
|
||||||
): Promise<boolean | null | MessageModel | void> {
|
): Promise<boolean | null | MessageModel | void> {
|
||||||
if (this.isGroupV2()) {
|
if (this.isGroupV2()) {
|
||||||
|
@ -3964,6 +4057,11 @@ export class ConversationModel extends window.Backbone
|
||||||
const timestamp = (receivedAt || Date.now()) - 1;
|
const timestamp = (receivedAt || Date.now()) - 1;
|
||||||
|
|
||||||
this.set({ expireTimer });
|
this.set({ expireTimer });
|
||||||
|
|
||||||
|
// This call actually removes universal timer notification and clears
|
||||||
|
// the pending flags.
|
||||||
|
await this.maybeApplyUniversalTimer();
|
||||||
|
|
||||||
window.Signal.Data.updateConversation(this.attributes);
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
|
||||||
const model = new window.Whisper.Message(({
|
const model = new window.Whisper.Message(({
|
||||||
|
@ -4677,6 +4775,7 @@ export class ConversationModel extends window.Backbone
|
||||||
lastMessage: null,
|
lastMessage: null,
|
||||||
timestamp: null,
|
timestamp: null,
|
||||||
active_at: null,
|
active_at: null,
|
||||||
|
pendingUniversalTimer: undefined,
|
||||||
});
|
});
|
||||||
window.Signal.Data.updateConversation(this.attributes);
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
|
||||||
|
|
|
@ -129,6 +129,10 @@ type MessageBubbleProps =
|
||||||
type: 'profileChange';
|
type: 'profileChange';
|
||||||
data: ProfileChangeNotificationPropsType;
|
data: ProfileChangeNotificationPropsType;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'universalTimerNotification';
|
||||||
|
data: null;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'chatSessionRefreshed';
|
type: 'chatSessionRefreshed';
|
||||||
data: null;
|
data: null;
|
||||||
|
@ -333,6 +337,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
!this.isKeyChange() &&
|
!this.isKeyChange() &&
|
||||||
!this.isMessageHistoryUnsynced() &&
|
!this.isMessageHistoryUnsynced() &&
|
||||||
!this.isProfileChange() &&
|
!this.isProfileChange() &&
|
||||||
|
!this.isUniversalTimerNotification() &&
|
||||||
!this.isUnsupportedMessage() &&
|
!this.isUnsupportedMessage() &&
|
||||||
!this.isVerifiedChange()
|
!this.isVerifiedChange()
|
||||||
);
|
);
|
||||||
|
@ -406,6 +411,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
data: this.getPropsForProfileChange(),
|
data: this.getPropsForProfileChange(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (this.isUniversalTimerNotification()) {
|
||||||
|
return {
|
||||||
|
type: 'universalTimerNotification',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (this.isChatSessionRefreshed()) {
|
if (this.isChatSessionRefreshed()) {
|
||||||
return {
|
return {
|
||||||
type: 'chatSessionRefreshed',
|
type: 'chatSessionRefreshed',
|
||||||
|
@ -600,6 +611,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return this.get('type') === 'profile-change';
|
return this.get('type') === 'profile-change';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isUniversalTimerNotification(): boolean {
|
||||||
|
return this.get('type') === 'universal-timer-notification';
|
||||||
|
}
|
||||||
|
|
||||||
// Props for each message type
|
// Props for each message type
|
||||||
getPropsForUnsupportedMessage(): PropsForUnsupportedMessage {
|
getPropsForUnsupportedMessage(): PropsForUnsupportedMessage {
|
||||||
const requiredVersion = this.get('requiredProtocolVersion');
|
const requiredVersion = this.get('requiredProtocolVersion');
|
||||||
|
@ -1941,6 +1956,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const isKeyChange = this.isKeyChange();
|
const isKeyChange = this.isKeyChange();
|
||||||
const isMessageHistoryUnsynced = this.isMessageHistoryUnsynced();
|
const isMessageHistoryUnsynced = this.isMessageHistoryUnsynced();
|
||||||
const isProfileChange = this.isProfileChange();
|
const isProfileChange = this.isProfileChange();
|
||||||
|
const isUniversalTimerNotification = this.isUniversalTimerNotification();
|
||||||
|
|
||||||
// Note: not all of these message types go through message.handleDataMessage
|
// Note: not all of these message types go through message.handleDataMessage
|
||||||
|
|
||||||
|
@ -1967,7 +1983,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
// Locally-generated notifications
|
// Locally-generated notifications
|
||||||
isKeyChange ||
|
isKeyChange ||
|
||||||
isMessageHistoryUnsynced ||
|
isMessageHistoryUnsynced ||
|
||||||
isProfileChange;
|
isProfileChange ||
|
||||||
|
isUniversalTimerNotification;
|
||||||
|
|
||||||
return !hasSomethingToDisplay;
|
return !hasSomethingToDisplay;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,10 @@ import {
|
||||||
getSafeLongFromTimestamp,
|
getSafeLongFromTimestamp,
|
||||||
getTimestampFromLong,
|
getTimestampFromLong,
|
||||||
} from '../util/timestampLongUtils';
|
} from '../util/timestampLongUtils';
|
||||||
|
import {
|
||||||
|
get as getUniversalExpireTimer,
|
||||||
|
set as setUniversalExpireTimer,
|
||||||
|
} from '../util/universalExpireTimer';
|
||||||
import { ourProfileKeyService } from './ourProfileKey';
|
import { ourProfileKeyService } from './ourProfileKey';
|
||||||
|
|
||||||
const { updateConversation } = dataInterface;
|
const { updateConversation } = dataInterface;
|
||||||
|
@ -182,6 +186,11 @@ export async function toAccountRecord(
|
||||||
accountRecord.primarySendsSms = Boolean(primarySendsSms);
|
accountRecord.primarySendsSms = Boolean(primarySendsSms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const universalExpireTimer = getUniversalExpireTimer();
|
||||||
|
if (universalExpireTimer) {
|
||||||
|
accountRecord.universalExpireTimer = Number(universalExpireTimer);
|
||||||
|
}
|
||||||
|
|
||||||
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
||||||
window.textsecure.protobuf.AccountRecord.PhoneNumberSharingMode;
|
window.textsecure.protobuf.AccountRecord.PhoneNumberSharingMode;
|
||||||
const phoneNumberSharingMode = parsePhoneNumberSharingMode(
|
const phoneNumberSharingMode = parsePhoneNumberSharingMode(
|
||||||
|
@ -811,6 +820,7 @@ export async function mergeAccountRecord(
|
||||||
sealedSenderIndicators,
|
sealedSenderIndicators,
|
||||||
typingIndicators,
|
typingIndicators,
|
||||||
primarySendsSms,
|
primarySendsSms,
|
||||||
|
universalExpireTimer,
|
||||||
} = accountRecord;
|
} = accountRecord;
|
||||||
|
|
||||||
window.storage.put('read-receipt-setting', readReceipts);
|
window.storage.put('read-receipt-setting', readReceipts);
|
||||||
|
@ -831,6 +841,10 @@ export async function mergeAccountRecord(
|
||||||
window.storage.put('primarySendsSms', primarySendsSms);
|
window.storage.put('primarySendsSms', primarySendsSms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof universalExpireTimer === 'number') {
|
||||||
|
setUniversalExpireTimer(universalExpireTimer);
|
||||||
|
}
|
||||||
|
|
||||||
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
const PHONE_NUMBER_SHARING_MODE_ENUM =
|
||||||
window.textsecure.protobuf.AccountRecord.PhoneNumberSharingMode;
|
window.textsecure.protobuf.AccountRecord.PhoneNumberSharingMode;
|
||||||
let phoneNumberSharingModeToStore: PhoneNumberSharingMode;
|
let phoneNumberSharingModeToStore: PhoneNumberSharingMode;
|
||||||
|
|
|
@ -3713,7 +3713,8 @@ async function getLastConversationActivity({
|
||||||
'verified-change',
|
'verified-change',
|
||||||
'message-history-unsynced',
|
'message-history-unsynced',
|
||||||
'keychange',
|
'keychange',
|
||||||
'group-v1-migration'
|
'group-v1-migration',
|
||||||
|
'universal-timer-notification'
|
||||||
)
|
)
|
||||||
) AND
|
) AND
|
||||||
(
|
(
|
||||||
|
@ -3763,7 +3764,8 @@ async function getLastConversationPreview({
|
||||||
'profile-change',
|
'profile-change',
|
||||||
'verified-change',
|
'verified-change',
|
||||||
'message-history-unsynced',
|
'message-history-unsynced',
|
||||||
'group-v1-migration'
|
'group-v1-migration',
|
||||||
|
'universal-timer-notification'
|
||||||
)
|
)
|
||||||
) AND NOT
|
) AND NOT
|
||||||
(
|
(
|
||||||
|
|
|
@ -171,6 +171,7 @@ export type MessageType = {
|
||||||
| 'outgoing'
|
| 'outgoing'
|
||||||
| 'profile-change'
|
| 'profile-change'
|
||||||
| 'timer-notification'
|
| 'timer-notification'
|
||||||
|
| 'universal-timer-notification'
|
||||||
| 'verified-change';
|
| 'verified-change';
|
||||||
quote?: { author?: string; authorUuid?: string };
|
quote?: { author?: string; authorUuid?: string };
|
||||||
received_at: number;
|
received_at: number;
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { CustomColorType } from '../../types/Colors';
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export type ItemsStateType = {
|
export type ItemsStateType = {
|
||||||
|
readonly universalExpireTimer?: number;
|
||||||
|
|
||||||
readonly [key: string]: unknown;
|
readonly [key: string]: unknown;
|
||||||
readonly customColors?: {
|
readonly customColors?: {
|
||||||
readonly colors: Record<string, CustomColorType>;
|
readonly colors: Record<string, CustomColorType>;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { ITEM_NAME as UNIVERSAL_EXPIRE_TIMER_ITEM } from '../../util/universalExpireTimer';
|
||||||
|
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
import { ItemsStateType } from '../ducks/items';
|
import { ItemsStateType } from '../ducks/items';
|
||||||
|
|
||||||
|
@ -18,3 +20,8 @@ export const getPinnedConversationIds = createSelector(
|
||||||
(state: ItemsStateType): Array<string> =>
|
(state: ItemsStateType): Array<string> =>
|
||||||
(state.pinnedConversationIds || []) as Array<string>
|
(state.pinnedConversationIds || []) as Array<string>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getUniversalExpireTimer = createSelector(
|
||||||
|
getItems,
|
||||||
|
(state: ItemsStateType): number => state[UNIVERSAL_EXPIRE_TIMER_ITEM] || 0
|
||||||
|
);
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
|
|
||||||
import { SmartContactName } from './ContactName';
|
import { SmartContactName } from './ContactName';
|
||||||
|
import { SmartUniversalTimerNotification } from './UniversalTimerNotification';
|
||||||
|
|
||||||
type ExternalProps = {
|
type ExternalProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -33,6 +34,10 @@ function renderContact(conversationId: string): JSX.Element {
|
||||||
return <FilteredSmartContactName conversationId={conversationId} />;
|
return <FilteredSmartContactName conversationId={conversationId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderUniversalTimerNotification(): JSX.Element {
|
||||||
|
return <SmartUniversalTimerNotification />;
|
||||||
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
const { id, conversationId } = props;
|
const { id, conversationId } = props;
|
||||||
|
|
||||||
|
@ -60,6 +65,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
customColor: conversation?.customColor,
|
customColor: conversation?.customColor,
|
||||||
isSelected,
|
isSelected,
|
||||||
renderContact,
|
renderContact,
|
||||||
|
renderUniversalTimerNotification,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
interactionMode: getInteractionMode(state),
|
interactionMode: getInteractionMode(state),
|
||||||
theme: getTheme(state),
|
theme: getTheme(state),
|
||||||
|
|
23
ts/state/smart/UniversalTimerNotification.tsx
Normal file
23
ts/state/smart/UniversalTimerNotification.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
import { UniversalTimerNotification } from '../../components/conversation/UniversalTimerNotification';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
import { getUniversalExpireTimer } from '../selectors/items';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType) => {
|
||||||
|
return {
|
||||||
|
...state.updates,
|
||||||
|
i18n: getIntl(state),
|
||||||
|
expireTimer: getUniversalExpireTimer(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export const SmartUniversalTimerNotification = smart(
|
||||||
|
UniversalTimerNotification
|
||||||
|
);
|
18
ts/test-both/util/expireTimers.ts
Normal file
18
ts/test-both/util/expireTimers.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export type TestExpireTimer = Readonly<{ value: number; label: string }>;
|
||||||
|
|
||||||
|
const SECOND = 1;
|
||||||
|
const MINUTE = 60 * SECOND;
|
||||||
|
const HOUR = 60 * MINUTE;
|
||||||
|
const DAY = 24 * HOUR;
|
||||||
|
const WEEK = 7 * DAY;
|
||||||
|
|
||||||
|
export const EXPIRE_TIMERS: ReadonlyArray<TestExpireTimer> = [
|
||||||
|
{ value: 42 * SECOND, label: '42 seconds' },
|
||||||
|
{ value: 5 * MINUTE, label: '5 minutes' },
|
||||||
|
{ value: 1 * HOUR, label: '1 hour' },
|
||||||
|
{ value: 6 * DAY, label: '6 days' },
|
||||||
|
{ value: 3 * WEEK, label: '3 weeks' },
|
||||||
|
];
|
1
ts/textsecure.d.ts
vendored
1
ts/textsecure.d.ts
vendored
|
@ -1067,6 +1067,7 @@ export declare class AccountRecordClass {
|
||||||
notDiscoverableByPhoneNumber?: boolean;
|
notDiscoverableByPhoneNumber?: boolean;
|
||||||
pinnedConversations?: PinnedConversationClass[];
|
pinnedConversations?: PinnedConversationClass[];
|
||||||
noteToSelfMarkedUnread?: boolean;
|
noteToSelfMarkedUnread?: boolean;
|
||||||
|
universalExpireTimer?: number;
|
||||||
primarySendsSms?: boolean;
|
primarySendsSms?: boolean;
|
||||||
|
|
||||||
__unknownFields?: ArrayBuffer;
|
__unknownFields?: ArrayBuffer;
|
||||||
|
|
|
@ -6,25 +6,33 @@ import humanizeDuration from 'humanize-duration';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
const SECONDS_PER_WEEK = 604800;
|
const SECONDS_PER_WEEK = 604800;
|
||||||
export const DEFAULT_DURATIONS_IN_SECONDS = [
|
export const DEFAULT_DURATIONS_IN_SECONDS: ReadonlyArray<number> = [
|
||||||
0,
|
0,
|
||||||
5,
|
moment.duration(4, 'weeks').asSeconds(),
|
||||||
10,
|
|
||||||
30,
|
|
||||||
moment.duration(1, 'minute').asSeconds(),
|
|
||||||
moment.duration(5, 'minutes').asSeconds(),
|
|
||||||
moment.duration(30, 'minutes').asSeconds(),
|
|
||||||
moment.duration(1, 'hour').asSeconds(),
|
|
||||||
moment.duration(6, 'hours').asSeconds(),
|
|
||||||
moment.duration(12, 'hours').asSeconds(),
|
|
||||||
moment.duration(1, 'day').asSeconds(),
|
|
||||||
moment.duration(1, 'week').asSeconds(),
|
moment.duration(1, 'week').asSeconds(),
|
||||||
|
moment.duration(1, 'day').asSeconds(),
|
||||||
|
moment.duration(8, 'hours').asSeconds(),
|
||||||
|
moment.duration(1, 'hour').asSeconds(),
|
||||||
|
moment.duration(5, 'minutes').asSeconds(),
|
||||||
|
moment.duration(30, 'seconds').asSeconds(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export function format(i18n: LocalizerType, dirtySeconds?: number): string {
|
export const DEFAULT_DURATIONS_SET: ReadonlySet<number> = new Set<number>(
|
||||||
|
DEFAULT_DURATIONS_IN_SECONDS
|
||||||
|
);
|
||||||
|
|
||||||
|
export type FormatOptions = {
|
||||||
|
capitalizeOff?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function format(
|
||||||
|
i18n: LocalizerType,
|
||||||
|
dirtySeconds?: number,
|
||||||
|
{ capitalizeOff = false }: FormatOptions = {}
|
||||||
|
): string {
|
||||||
let seconds = Math.abs(dirtySeconds || 0);
|
let seconds = Math.abs(dirtySeconds || 0);
|
||||||
if (!seconds) {
|
if (!seconds) {
|
||||||
return i18n('disappearingMessages__off');
|
return i18n(capitalizeOff ? 'off' : 'disappearingMessages__off');
|
||||||
}
|
}
|
||||||
seconds = Math.max(Math.floor(seconds), 1);
|
seconds = Math.max(Math.floor(seconds), 1);
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ import { StartupQueue } from './StartupQueue';
|
||||||
import { postLinkExperience } from './postLinkExperience';
|
import { postLinkExperience } from './postLinkExperience';
|
||||||
import { sendToGroup, sendContentMessageToGroup } from './sendToGroup';
|
import { sendToGroup, sendContentMessageToGroup } from './sendToGroup';
|
||||||
import { RetryPlaceholders } from './retryPlaceholders';
|
import { RetryPlaceholders } from './retryPlaceholders';
|
||||||
|
import * as expirationTimer from './expirationTimer';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
GoogleChrome,
|
GoogleChrome,
|
||||||
|
@ -73,4 +74,5 @@ export {
|
||||||
sleep,
|
sleep,
|
||||||
toWebSafeBase64,
|
toWebSafeBase64,
|
||||||
zkgroup,
|
zkgroup,
|
||||||
|
expirationTimer,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1233,6 +1233,22 @@
|
||||||
"updated": "2021-05-11T20:38:03.542Z",
|
"updated": "2021-05-11T20:38:03.542Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "js/views/settings_view.js",
|
||||||
|
"line": " template: () => $('#disappearingMessagesSettings').html(),",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-05-27T01:33:06.541Z",
|
||||||
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "js/views/settings_view.js",
|
||||||
|
"line": " this.$('.disappearing-messages-setting').append(",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-05-27T01:33:06.541Z",
|
||||||
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/settings_view.js",
|
"path": "js/views/settings_view.js",
|
||||||
|
@ -1241,6 +1257,14 @@
|
||||||
"updated": "2020-08-21T11:29:29.636Z",
|
"updated": "2020-08-21T11:29:29.636Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-append(",
|
||||||
|
"path": "js/views/settings_view.js",
|
||||||
|
"line": " this.$('.disappearing-messages-setting').append(",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-05-27T01:33:06.541Z",
|
||||||
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-html(",
|
"rule": "jQuery-html(",
|
||||||
"path": "js/views/settings_view.js",
|
"path": "js/views/settings_view.js",
|
||||||
|
@ -1257,6 +1281,14 @@
|
||||||
"updated": "2021-02-26T18:44:56.450Z",
|
"updated": "2021-02-26T18:44:56.450Z",
|
||||||
"reasonDetail": "Static selector, read-only access"
|
"reasonDetail": "Static selector, read-only access"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-html(",
|
||||||
|
"path": "js/views/settings_view.js",
|
||||||
|
"line": " template: () => $('#disappearingMessagesSettings').html(),",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-05-27T01:33:06.541Z",
|
||||||
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/standalone_registration_view.js",
|
"path": "js/views/standalone_registration_view.js",
|
||||||
|
|
12
ts/util/universalExpireTimer.ts
Normal file
12
ts/util/universalExpireTimer.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export const ITEM_NAME = 'universalExpireTimer';
|
||||||
|
|
||||||
|
export function get(): number {
|
||||||
|
return window.storage.get(ITEM_NAME) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set(newValue: number | undefined): Promise<void> {
|
||||||
|
return window.storage.put(ITEM_NAME, newValue || 0);
|
||||||
|
}
|
|
@ -1068,7 +1068,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
|
|
||||||
const finish = () => {
|
const finish = () => {
|
||||||
resolvePromise();
|
resolvePromise();
|
||||||
this.model.inProgressFinish = null;
|
this.model.inProgressFetch = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return finish;
|
return finish;
|
||||||
|
|
3
ts/window.d.ts
vendored
3
ts/window.d.ts
vendored
|
@ -102,6 +102,7 @@ import { MessageDetail } from './components/conversation/MessageDetail';
|
||||||
import { ProgressModal } from './components/ProgressModal';
|
import { ProgressModal } from './components/ProgressModal';
|
||||||
import { Quote } from './components/conversation/Quote';
|
import { Quote } from './components/conversation/Quote';
|
||||||
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
||||||
|
import { DisappearingTimeDialog } from './components/conversation/DisappearingTimeDialog';
|
||||||
import { MIMEType } from './types/MIME';
|
import { MIMEType } from './types/MIME';
|
||||||
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
||||||
import { SignalProtocolStore } from './SignalProtocolStore';
|
import { SignalProtocolStore } from './SignalProtocolStore';
|
||||||
|
@ -488,6 +489,7 @@ declare global {
|
||||||
ProgressModal: typeof ProgressModal;
|
ProgressModal: typeof ProgressModal;
|
||||||
Quote: typeof Quote;
|
Quote: typeof Quote;
|
||||||
StagedLinkPreview: typeof StagedLinkPreview;
|
StagedLinkPreview: typeof StagedLinkPreview;
|
||||||
|
DisappearingTimeDialog: typeof DisappearingTimeDialog;
|
||||||
};
|
};
|
||||||
OS: typeof OS;
|
OS: typeof OS;
|
||||||
Workflow: {
|
Workflow: {
|
||||||
|
@ -796,4 +798,5 @@ export type WhisperType = {
|
||||||
View: typeof Backbone.View & {
|
View: typeof Backbone.View & {
|
||||||
Templates: Record<string, string>;
|
Templates: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
DisappearingTimeDialog: typeof window.Whisper.View | undefined;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue