Sync mute state
This commit is contained in:
parent
15247e1c9a
commit
6c0acd09df
16 changed files with 236 additions and 61 deletions
|
@ -3328,6 +3328,10 @@
|
||||||
"message": "Mute for one hour",
|
"message": "Mute for one hour",
|
||||||
"description": "Label for muting the conversation"
|
"description": "Label for muting the conversation"
|
||||||
},
|
},
|
||||||
|
"muteEightHours": {
|
||||||
|
"message": "Mute for eight hours",
|
||||||
|
"description": "Label for muting the conversation"
|
||||||
|
},
|
||||||
"muteDay": {
|
"muteDay": {
|
||||||
"message": "Mute for one day",
|
"message": "Mute for one day",
|
||||||
"description": "Label for muting the conversation"
|
"description": "Label for muting the conversation"
|
||||||
|
@ -3336,14 +3340,18 @@
|
||||||
"message": "Mute for one week",
|
"message": "Mute for one week",
|
||||||
"description": "Label for muting the conversation"
|
"description": "Label for muting the conversation"
|
||||||
},
|
},
|
||||||
"muteYear": {
|
"muteAlways": {
|
||||||
"message": "Mute for one year",
|
"message": "Mute always",
|
||||||
"description": "Label for muting the conversation"
|
"description": "Label for muting the conversation"
|
||||||
},
|
},
|
||||||
"unmute": {
|
"unmute": {
|
||||||
"message": "Unmute",
|
"message": "Unmute",
|
||||||
"description": "Label for unmuting the conversation"
|
"description": "Label for unmuting the conversation"
|
||||||
},
|
},
|
||||||
|
"muteExpirationLabelAlways": {
|
||||||
|
"message": "Muted always",
|
||||||
|
"description": "Shown in the mute notifications submenu whenever a conversation has been muted"
|
||||||
|
},
|
||||||
"muteExpirationLabel": {
|
"muteExpirationLabel": {
|
||||||
"message": "Muted until $duration$",
|
"message": "Muted until $duration$",
|
||||||
"description": "Shown in the mute notifications submenu whenever a conversation has been muted",
|
"description": "Shown in the mute notifications submenu whenever a conversation has been muted",
|
||||||
|
|
|
@ -62,34 +62,37 @@ message ContactRecord {
|
||||||
UNVERIFIED = 2;
|
UNVERIFIED = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional string serviceUuid = 1;
|
optional string serviceUuid = 1;
|
||||||
optional string serviceE164 = 2;
|
optional string serviceE164 = 2;
|
||||||
optional bytes profileKey = 3;
|
optional bytes profileKey = 3;
|
||||||
optional bytes identityKey = 4;
|
optional bytes identityKey = 4;
|
||||||
optional IdentityState identityState = 5;
|
optional IdentityState identityState = 5;
|
||||||
optional string givenName = 6;
|
optional string givenName = 6;
|
||||||
optional string familyName = 7;
|
optional string familyName = 7;
|
||||||
optional string username = 8;
|
optional string username = 8;
|
||||||
optional bool blocked = 9;
|
optional bool blocked = 9;
|
||||||
optional bool whitelisted = 10;
|
optional bool whitelisted = 10;
|
||||||
optional bool archived = 11;
|
optional bool archived = 11;
|
||||||
optional bool markedUnread = 12;
|
optional bool markedUnread = 12;
|
||||||
|
optional uint64 mutedUntilTimestamp = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GroupV1Record {
|
message GroupV1Record {
|
||||||
optional bytes id = 1;
|
optional bytes id = 1;
|
||||||
optional bool blocked = 2;
|
optional bool blocked = 2;
|
||||||
optional bool whitelisted = 3;
|
optional bool whitelisted = 3;
|
||||||
optional bool archived = 4;
|
optional bool archived = 4;
|
||||||
optional bool markedUnread = 5;
|
optional bool markedUnread = 5;
|
||||||
|
optional uint64 mutedUntilTimestamp = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GroupV2Record {
|
message GroupV2Record {
|
||||||
optional bytes masterKey = 1;
|
optional bytes masterKey = 1;
|
||||||
optional bool blocked = 2;
|
optional bool blocked = 2;
|
||||||
optional bool whitelisted = 3;
|
optional bool whitelisted = 3;
|
||||||
optional bool archived = 4;
|
optional bool archived = 4;
|
||||||
optional bool markedUnread = 5;
|
optional bool markedUnread = 5;
|
||||||
|
optional uint64 mutedUntilTimestamp = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AccountRecord {
|
message AccountRecord {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
const ByteBuffer = require('../components/bytebuffer/dist/ByteBufferAB.js');
|
const ByteBuffer = require('../components/bytebuffer/dist/ByteBufferAB.js');
|
||||||
|
const Long = require('../components/long/dist/Long.js');
|
||||||
const { setEnvironment, Environment } = require('../ts/environment');
|
const { setEnvironment, Environment } = require('../ts/environment');
|
||||||
|
|
||||||
setEnvironment(Environment.Test);
|
setEnvironment(Environment.Test);
|
||||||
|
@ -18,6 +19,7 @@ global.window = {
|
||||||
i18n: key => `i18n(${key})`,
|
i18n: key => `i18n(${key})`,
|
||||||
dcodeIO: {
|
dcodeIO: {
|
||||||
ByteBuffer,
|
ByteBuffer,
|
||||||
|
Long,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -239,6 +239,22 @@ const stories: Array<ConversationHeaderStory> = [
|
||||||
outgoingCallButtonStyle: OutgoingCallButtonStyle.Join,
|
outgoingCallButtonStyle: OutgoingCallButtonStyle.Join,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'In a forever muted group',
|
||||||
|
props: {
|
||||||
|
...commonProps,
|
||||||
|
color: 'signal-blue',
|
||||||
|
title: 'Way too many messages',
|
||||||
|
name: 'Way too many messages',
|
||||||
|
phoneNumber: '',
|
||||||
|
id: '1',
|
||||||
|
type: 'group',
|
||||||
|
expireTimer: 10,
|
||||||
|
acceptedMessageRequest: true,
|
||||||
|
outgoingCallButtonStyle: OutgoingCallButtonStyle.JustVideo,
|
||||||
|
muteExpiresAt: Infinity,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -378,14 +378,24 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
|
||||||
const muteOptions: Array<MuteOption> = [];
|
const muteOptions: Array<MuteOption> = [];
|
||||||
if (isMuted(muteExpiresAt)) {
|
if (isMuted(muteExpiresAt)) {
|
||||||
const expires = moment(muteExpiresAt);
|
const expires = moment(muteExpiresAt);
|
||||||
const muteExpirationLabel = moment().isSame(expires, 'day')
|
|
||||||
? expires.format('hh:mm A')
|
let muteExpirationLabel: string;
|
||||||
: expires.format('M/D/YY, hh:mm A');
|
if (Number(muteExpiresAt) >= Number.MAX_SAFE_INTEGER) {
|
||||||
|
muteExpirationLabel = i18n('muteExpirationLabelAlways');
|
||||||
|
} else {
|
||||||
|
const muteExpirationUntil = moment().isSame(expires, 'day')
|
||||||
|
? expires.format('hh:mm A')
|
||||||
|
: expires.format('M/D/YY, hh:mm A');
|
||||||
|
|
||||||
|
muteExpirationLabel = i18n('muteExpirationLabel', [
|
||||||
|
muteExpirationUntil,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
muteOptions.push(
|
muteOptions.push(
|
||||||
...[
|
...[
|
||||||
{
|
{
|
||||||
name: i18n('muteExpirationLabel', [muteExpirationLabel]),
|
name: muteExpirationLabel,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
|
|
|
@ -121,9 +121,9 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
||||||
/* eslint-disable no-nested-ternary */
|
/* eslint-disable no-nested-ternary */
|
||||||
messageText = (
|
messageText = (
|
||||||
<>
|
<>
|
||||||
{muteExpiresAt && Date.now() < muteExpiresAt && (
|
{muteExpiresAt && Date.now() < muteExpiresAt ? (
|
||||||
<span className={`${MESSAGE_TEXT_CLASS_NAME}__muted`} />
|
<span className={`${MESSAGE_TEXT_CLASS_NAME}__muted`} />
|
||||||
)}
|
) : null}
|
||||||
{!acceptedMessageRequest ? (
|
{!acceptedMessageRequest ? (
|
||||||
<span className={`${MESSAGE_TEXT_CLASS_NAME}__message-request`}>
|
<span className={`${MESSAGE_TEXT_CLASS_NAME}__message-request`}>
|
||||||
{i18n('ConversationListItem--message-request')}
|
{i18n('ConversationListItem--message-request')}
|
||||||
|
|
|
@ -5021,6 +5021,42 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMuteExpiration(
|
||||||
|
muteExpiresAt = 0,
|
||||||
|
{ viaStorageServiceSync = false } = {}
|
||||||
|
): void {
|
||||||
|
const prevExpiration = this.get('muteExpiresAt');
|
||||||
|
|
||||||
|
if (prevExpiration === muteExpiresAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we use a timeoutId here so that we can reference the mute that was
|
||||||
|
// potentially set in the ConversationController. Specifically for a
|
||||||
|
// scenario where a conversation is already muted and we boot up the app,
|
||||||
|
// a timeout will be already set. But if we change the mute to a later
|
||||||
|
// date a new timeout would need to be set and the old one cleared. With
|
||||||
|
// this ID we can reference the existing timeout.
|
||||||
|
const timeoutId = this.getMuteTimeoutId();
|
||||||
|
window.Signal.Services.removeTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (muteExpiresAt && muteExpiresAt < Number.MAX_SAFE_INTEGER) {
|
||||||
|
window.Signal.Services.onTimeout(
|
||||||
|
muteExpiresAt,
|
||||||
|
() => {
|
||||||
|
this.setMuteExpiration(0);
|
||||||
|
},
|
||||||
|
timeoutId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set({ muteExpiresAt });
|
||||||
|
if (!viaStorageServiceSync) {
|
||||||
|
this.captureChange('mutedUntilTimestamp');
|
||||||
|
}
|
||||||
|
window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
}
|
||||||
|
|
||||||
isMuted(): boolean {
|
isMuted(): boolean {
|
||||||
return isMuted(this.get('muteExpiresAt'));
|
return isMuted(this.get('muteExpiresAt'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -626,7 +626,8 @@ async function mergeRecord(
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'storageService.mergeRecord: Error with',
|
'storageService.mergeRecord: Error with',
|
||||||
redactStorageID(storageID),
|
redactStorageID(storageID),
|
||||||
itemType
|
itemType,
|
||||||
|
String(err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,10 @@ import {
|
||||||
} from '../util/phoneNumberDiscoverability';
|
} from '../util/phoneNumberDiscoverability';
|
||||||
import { arePinnedConversationsEqual } from '../util/arePinnedConversationsEqual';
|
import { arePinnedConversationsEqual } from '../util/arePinnedConversationsEqual';
|
||||||
import { ConversationModel } from '../models/conversations';
|
import { ConversationModel } from '../models/conversations';
|
||||||
|
import {
|
||||||
|
getSafeLongFromTimestamp,
|
||||||
|
getTimestampFromLong,
|
||||||
|
} from '../util/timestampLongUtils';
|
||||||
|
|
||||||
const { updateConversation } = dataInterface;
|
const { updateConversation } = dataInterface;
|
||||||
|
|
||||||
|
@ -131,6 +135,9 @@ export async function toContactRecord(
|
||||||
contactRecord.whitelisted = Boolean(conversation.get('profileSharing'));
|
contactRecord.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||||
contactRecord.archived = Boolean(conversation.get('isArchived'));
|
contactRecord.archived = Boolean(conversation.get('isArchived'));
|
||||||
contactRecord.markedUnread = Boolean(conversation.get('markedUnread'));
|
contactRecord.markedUnread = Boolean(conversation.get('markedUnread'));
|
||||||
|
contactRecord.mutedUntilTimestamp = getSafeLongFromTimestamp(
|
||||||
|
conversation.get('muteExpiresAt')
|
||||||
|
);
|
||||||
|
|
||||||
applyUnknownFields(contactRecord, conversation);
|
applyUnknownFields(contactRecord, conversation);
|
||||||
|
|
||||||
|
@ -278,6 +285,9 @@ export async function toGroupV1Record(
|
||||||
groupV1Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
groupV1Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||||
groupV1Record.archived = Boolean(conversation.get('isArchived'));
|
groupV1Record.archived = Boolean(conversation.get('isArchived'));
|
||||||
groupV1Record.markedUnread = Boolean(conversation.get('markedUnread'));
|
groupV1Record.markedUnread = Boolean(conversation.get('markedUnread'));
|
||||||
|
groupV1Record.mutedUntilTimestamp = getSafeLongFromTimestamp(
|
||||||
|
conversation.get('muteExpiresAt')
|
||||||
|
);
|
||||||
|
|
||||||
applyUnknownFields(groupV1Record, conversation);
|
applyUnknownFields(groupV1Record, conversation);
|
||||||
|
|
||||||
|
@ -297,6 +307,9 @@ export async function toGroupV2Record(
|
||||||
groupV2Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
groupV2Record.whitelisted = Boolean(conversation.get('profileSharing'));
|
||||||
groupV2Record.archived = Boolean(conversation.get('isArchived'));
|
groupV2Record.archived = Boolean(conversation.get('isArchived'));
|
||||||
groupV2Record.markedUnread = Boolean(conversation.get('markedUnread'));
|
groupV2Record.markedUnread = Boolean(conversation.get('markedUnread'));
|
||||||
|
groupV2Record.mutedUntilTimestamp = getSafeLongFromTimestamp(
|
||||||
|
conversation.get('muteExpiresAt')
|
||||||
|
);
|
||||||
|
|
||||||
applyUnknownFields(groupV2Record, conversation);
|
applyUnknownFields(groupV2Record, conversation);
|
||||||
|
|
||||||
|
@ -522,6 +535,13 @@ export async function mergeGroupV1Record(
|
||||||
storageID,
|
storageID,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
conversation.setMuteExpiration(
|
||||||
|
getTimestampFromLong(groupV1Record.mutedUntilTimestamp),
|
||||||
|
{
|
||||||
|
viaStorageServiceSync: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
applyMessageRequestState(groupV1Record, conversation);
|
applyMessageRequestState(groupV1Record, conversation);
|
||||||
|
|
||||||
let hasPendingChanges: boolean;
|
let hasPendingChanges: boolean;
|
||||||
|
@ -622,6 +642,13 @@ export async function mergeGroupV2Record(
|
||||||
storageID,
|
storageID,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
conversation.setMuteExpiration(
|
||||||
|
getTimestampFromLong(groupV2Record.mutedUntilTimestamp),
|
||||||
|
{
|
||||||
|
viaStorageServiceSync: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
applyMessageRequestState(groupV2Record, conversation);
|
applyMessageRequestState(groupV2Record, conversation);
|
||||||
|
|
||||||
addUnknownFields(groupV2Record, conversation);
|
addUnknownFields(groupV2Record, conversation);
|
||||||
|
@ -731,6 +758,13 @@ export async function mergeContactRecord(
|
||||||
storageID,
|
storageID,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
conversation.setMuteExpiration(
|
||||||
|
getTimestampFromLong(contactRecord.mutedUntilTimestamp),
|
||||||
|
{
|
||||||
|
viaStorageServiceSync: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const hasPendingChanges = doesRecordHavePendingChanges(
|
const hasPendingChanges = doesRecordHavePendingChanges(
|
||||||
await toContactRecord(conversation),
|
await toContactRecord(conversation),
|
||||||
contactRecord,
|
contactRecord,
|
||||||
|
|
|
@ -59,10 +59,12 @@ export function onTimeout(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeTimeout(uuid: string): void {
|
export function removeTimeout(uuid: string): void {
|
||||||
if (timeoutStore.has(uuid)) {
|
if (!timeoutStore.has(uuid)) {
|
||||||
timeoutStore.delete(uuid);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timeoutStore.delete(uuid);
|
||||||
|
|
||||||
allTimeouts.forEach((timeout: TimeoutType) => {
|
allTimeouts.forEach((timeout: TimeoutType) => {
|
||||||
if (uuid === timeout.uuid) {
|
if (uuid === timeout.uuid) {
|
||||||
allTimeouts.delete(timeout);
|
allTimeouts.delete(timeout);
|
||||||
|
|
51
ts/test-both/util/timestampLongUtils_test.ts
Normal file
51
ts/test-both/util/timestampLongUtils_test.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getSafeLongFromTimestamp,
|
||||||
|
getTimestampFromLong,
|
||||||
|
} from '../../util/timestampLongUtils';
|
||||||
|
|
||||||
|
describe('getSafeLongFromTimestamp', () => {
|
||||||
|
const { Long } = window.dcodeIO;
|
||||||
|
|
||||||
|
it('returns zero when passed undefined', () => {
|
||||||
|
assert(getSafeLongFromTimestamp(undefined).isZero());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the number as a Long when passed a "normal" number', () => {
|
||||||
|
assert(getSafeLongFromTimestamp(0).isZero());
|
||||||
|
assert.strictEqual(getSafeLongFromTimestamp(123).toString(), '123');
|
||||||
|
assert.strictEqual(getSafeLongFromTimestamp(-456).toString(), '-456');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns Long.MAX_VALUE when passed Infinity', () => {
|
||||||
|
assert(getSafeLongFromTimestamp(Infinity).equals(Long.MAX_VALUE));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns Long.MAX_VALUE when passed very large numbers, outside of JavaScript's safely representable range", () => {
|
||||||
|
assert.equal(getSafeLongFromTimestamp(Number.MAX_VALUE), Long.MAX_VALUE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTimestampFromLong', () => {
|
||||||
|
const { Long } = window.dcodeIO;
|
||||||
|
|
||||||
|
it('returns zero when passed 0 Long', () => {
|
||||||
|
assert.equal(getTimestampFromLong(Long.fromNumber(0)), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns Number.MAX_SAFE_INTEGER when passed Long.MAX_VALUE', () => {
|
||||||
|
assert.equal(getTimestampFromLong(Long.MAX_VALUE), Number.MAX_SAFE_INTEGER);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a normal number', () => {
|
||||||
|
assert.equal(getTimestampFromLong(Long.fromNumber(16)), 16);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 for null value', () => {
|
||||||
|
assert.equal(getTimestampFromLong(null), 0);
|
||||||
|
});
|
||||||
|
});
|
3
ts/textsecure.d.ts
vendored
3
ts/textsecure.d.ts
vendored
|
@ -1057,6 +1057,7 @@ export declare class ContactRecordClass {
|
||||||
whitelisted?: boolean | null;
|
whitelisted?: boolean | null;
|
||||||
archived?: boolean | null;
|
archived?: boolean | null;
|
||||||
markedUnread?: boolean;
|
markedUnread?: boolean;
|
||||||
|
mutedUntilTimestamp?: ProtoBigNumberType;
|
||||||
|
|
||||||
__unknownFields?: ArrayBuffer;
|
__unknownFields?: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
@ -1073,6 +1074,7 @@ export declare class GroupV1RecordClass {
|
||||||
whitelisted?: boolean | null;
|
whitelisted?: boolean | null;
|
||||||
archived?: boolean | null;
|
archived?: boolean | null;
|
||||||
markedUnread?: boolean;
|
markedUnread?: boolean;
|
||||||
|
mutedUntilTimestamp?: ProtoBigNumberType;
|
||||||
|
|
||||||
__unknownFields?: ArrayBuffer;
|
__unknownFields?: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
@ -1089,6 +1091,7 @@ export declare class GroupV2RecordClass {
|
||||||
whitelisted?: boolean | null;
|
whitelisted?: boolean | null;
|
||||||
archived?: boolean | null;
|
archived?: boolean | null;
|
||||||
markedUnread?: boolean;
|
markedUnread?: boolean;
|
||||||
|
mutedUntilTimestamp?: ProtoBigNumberType;
|
||||||
|
|
||||||
__unknownFields?: ArrayBuffer;
|
__unknownFields?: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,10 @@ export function getMuteOptions(i18n: LocalizerType): Array<MuteOption> {
|
||||||
name: i18n('muteHour'),
|
name: i18n('muteHour'),
|
||||||
value: moment.duration(1, 'hour').as('milliseconds'),
|
value: moment.duration(1, 'hour').as('milliseconds'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: i18n('muteEightHours'),
|
||||||
|
value: moment.duration(8, 'hour').as('milliseconds'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: i18n('muteDay'),
|
name: i18n('muteDay'),
|
||||||
value: moment.duration(1, 'day').as('milliseconds'),
|
value: moment.duration(1, 'day').as('milliseconds'),
|
||||||
|
@ -25,8 +29,8 @@ export function getMuteOptions(i18n: LocalizerType): Array<MuteOption> {
|
||||||
value: moment.duration(1, 'week').as('milliseconds'),
|
value: moment.duration(1, 'week').as('milliseconds'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: i18n('muteYear'),
|
name: i18n('muteAlways'),
|
||||||
value: moment.duration(1, 'year').as('milliseconds'),
|
value: Number.MAX_SAFE_INTEGER,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
26
ts/util/timestampLongUtils.ts
Normal file
26
ts/util/timestampLongUtils.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { Long } from '../window.d';
|
||||||
|
|
||||||
|
export function getSafeLongFromTimestamp(timestamp = 0): Long {
|
||||||
|
if (timestamp >= Number.MAX_SAFE_INTEGER) {
|
||||||
|
return window.dcodeIO.Long.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.dcodeIO.Long.fromNumber(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimestampFromLong(value: Long | null): number {
|
||||||
|
if (!value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = value.toNumber();
|
||||||
|
|
||||||
|
if (num >= Number.MAX_SAFE_INTEGER) {
|
||||||
|
return Number.MAX_SAFE_INTEGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return num;
|
||||||
|
}
|
|
@ -478,7 +478,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
: this.model.getTitle();
|
: this.model.getTitle();
|
||||||
searchInConversation(this.model.id, name);
|
searchInConversation(this.model.id, name);
|
||||||
},
|
},
|
||||||
onSetMuteNotifications: (ms: number) => this.setMuteNotifications(ms),
|
onSetMuteNotifications: (ms: number) =>
|
||||||
|
this.model.setMuteExpiration(
|
||||||
|
ms >= Number.MAX_SAFE_INTEGER ? ms : Date.now() + ms
|
||||||
|
),
|
||||||
onSetPin: this.setPin.bind(this),
|
onSetPin: this.setPin.bind(this),
|
||||||
// These are view only and don't update the Conversation model, so they
|
// These are view only and don't update the Conversation model, so they
|
||||||
// need a manual update call.
|
// need a manual update call.
|
||||||
|
@ -3162,31 +3165,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setMuteNotifications(ms: number) {
|
|
||||||
const muteExpiresAt = ms > 0 ? Date.now() + ms : undefined;
|
|
||||||
|
|
||||||
if (muteExpiresAt) {
|
|
||||||
// we use a timeoutId here so that we can reference the mute that was
|
|
||||||
// potentially set in the ConversationController. Specifically for a
|
|
||||||
// scenario where a conversation is already muted and we boot up the app,
|
|
||||||
// a timeout will be already set. But if we change the mute to a later
|
|
||||||
// date a new timeout would need to be set and the old one cleared. With
|
|
||||||
// this ID we can reference the existing timeout.
|
|
||||||
const timeoutId = this.model.getMuteTimeoutId();
|
|
||||||
window.Signal.Services.removeTimeout(timeoutId);
|
|
||||||
window.Signal.Services.onTimeout(
|
|
||||||
muteExpiresAt,
|
|
||||||
() => {
|
|
||||||
this.setMuteNotifications(0);
|
|
||||||
},
|
|
||||||
timeoutId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.model.set({ muteExpiresAt });
|
|
||||||
this.saveModel();
|
|
||||||
},
|
|
||||||
|
|
||||||
async destroyMessages() {
|
async destroyMessages() {
|
||||||
window.showConfirmationDialog({
|
window.showConfirmationDialog({
|
||||||
message: window.i18n('deleteConversationConfirmation'),
|
message: window.i18n('deleteConversationConfirmation'),
|
||||||
|
|
1
ts/window.d.ts
vendored
1
ts/window.d.ts
vendored
|
@ -573,6 +573,7 @@ export type DCodeIOType = {
|
||||||
Long: DCodeIOType['Long'];
|
Long: DCodeIOType['Long'];
|
||||||
};
|
};
|
||||||
Long: Long & {
|
Long: Long & {
|
||||||
|
MAX_VALUE: Long;
|
||||||
equals: (other: Long | number | string) => boolean;
|
equals: (other: Long | number | string) => boolean;
|
||||||
fromBits: (low: number, high: number, unsigned: boolean) => number;
|
fromBits: (low: number, high: number, unsigned: boolean) => number;
|
||||||
fromNumber: (value: number, unsigned?: boolean) => Long;
|
fromNumber: (value: number, unsigned?: boolean) => Long;
|
||||||
|
|
Loading…
Reference in a new issue