Initial logic for release notes
This commit is contained in:
parent
34ef8dc2c8
commit
c5301688a1
13 changed files with 491 additions and 5 deletions
|
@ -33,6 +33,9 @@ export type ConfigKeyType =
|
|||
| 'desktop.experimentalTransportEnabled.beta'
|
||||
| 'desktop.experimentalTransportEnabled.prod'
|
||||
| 'desktop.cdsiViaLibsignal'
|
||||
| 'desktop.releaseNotes'
|
||||
| 'desktop.releaseNotes.beta'
|
||||
| 'desktop.releaseNotes.dev'
|
||||
| 'global.attachments.maxBytes'
|
||||
| 'global.attachments.maxReceiveBytes'
|
||||
| 'global.calling.maxGroupCallRingSize'
|
||||
|
|
|
@ -198,6 +198,7 @@ import { restoreRemoteConfigFromStorage } from './RemoteConfig';
|
|||
import { getParametersForRedux, loadAll } from './services/allLoaders';
|
||||
import { checkFirstEnvelope } from './util/checkFirstEnvelope';
|
||||
import { BLOCKED_UUIDS_ID } from './textsecure/storage/Blocked';
|
||||
import { ReleaseNotesFetcher } from './services/releaseNotesFetcher';
|
||||
|
||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||
|
@ -2160,6 +2161,8 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
drop(usernameIntegrity.start());
|
||||
|
||||
drop(ReleaseNotesFetcher.init(window.Whisper.events, newVersion));
|
||||
}
|
||||
|
||||
let initialStartupCount = 0;
|
||||
|
|
|
@ -631,6 +631,19 @@ function HeaderMenu({
|
|||
</MenuItem>
|
||||
)}
|
||||
</SubMenu>
|
||||
{conversation.isArchived ? (
|
||||
<MenuItem onClick={onConversationUnarchive}>
|
||||
{i18n('icu:moveConversationToInbox')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem onClick={onConversationArchive}>
|
||||
{i18n('icu:archiveConversation')}
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem onClick={onConversationDeleteMessages}>
|
||||
{i18n('icu:deleteConversation')}
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ import {
|
|||
getChangesForPropAtTimestamp,
|
||||
} from '../../util/editHelpers';
|
||||
import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
|
||||
const MAX_CONCURRENT_ATTACHMENT_UPLOADS = 5;
|
||||
|
||||
|
@ -88,6 +89,13 @@ export async function sendNormalMessage(
|
|||
return;
|
||||
}
|
||||
|
||||
if (isSignalConversation(messageConversation)) {
|
||||
log.error(
|
||||
`Message conversation '${messageConversation?.idForLogging()}' is the Signal serviceId, not sending`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOutgoing(message.attributes)) {
|
||||
log.error(
|
||||
`message ${messageId} was not an outgoing message to begin with. This is probably a bogus job. Giving up on sending it`
|
||||
|
|
|
@ -5,6 +5,7 @@ import type { ConversationModel } from '../../models/conversations';
|
|||
import type { LoggerType } from '../../types/Logging';
|
||||
import { getRecipients } from '../../util/getRecipients';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted';
|
||||
import { isSignalConversation } from '../../util/isSignalConversation';
|
||||
import { getUntrustedConversationServiceIds } from './getUntrustedConversationServiceIds';
|
||||
|
||||
export function shouldSendToConversation(
|
||||
|
@ -35,5 +36,12 @@ export function shouldSendToConversation(
|
|||
return false;
|
||||
}
|
||||
|
||||
if (isSignalConversation(conversation.attributes)) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} is Signal conversation; refusing to send`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1343,6 +1343,10 @@ export class ConversationModel extends window.Backbone
|
|||
return;
|
||||
}
|
||||
|
||||
if (isSignalConversation(this.attributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Coalesce multiple sendTypingMessage calls into one.
|
||||
//
|
||||
// `lastIsTyping` is set to the last `isTyping` value passed to the
|
||||
|
|
330
ts/services/releaseNotesFetcher.ts
Normal file
330
ts/services/releaseNotesFetcher.ts
Normal file
|
@ -0,0 +1,330 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import semver from 'semver';
|
||||
import { last } from 'lodash';
|
||||
|
||||
import * as durations from '../util/durations';
|
||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||
import * as Registration from '../util/registration';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import { HTTPError } from '../textsecure/Errors';
|
||||
import { drop } from '../util/drop';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import type { MessageAttributesType } from '../model-types';
|
||||
import { ReadStatus } from '../messages/MessageReadStatus';
|
||||
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||
import { SeenStatus } from '../MessageSeenStatus';
|
||||
import { saveNewMessageBatcher } from '../util/messageBatcher';
|
||||
import { generateMessageId } from '../util/generateMessageId';
|
||||
import { BodyRange } from '../types/BodyRange';
|
||||
import * as RemoteConfig from '../RemoteConfig';
|
||||
import { isBeta, isProduction } from '../util/version';
|
||||
import type {
|
||||
ReleaseNotesManifestResponseType,
|
||||
ReleaseNoteResponseType,
|
||||
} from '../textsecure/WebAPI';
|
||||
import type { WithRequiredProperties } from '../types/Util';
|
||||
|
||||
const FETCH_INTERVAL = 3 * durations.DAY;
|
||||
const ERROR_RETRY_DELAY = 3 * durations.HOUR;
|
||||
const NEXT_FETCH_TIME_STORAGE_KEY = 'releaseNotesNextFetchTime';
|
||||
const PREVIOUS_MANIFEST_HASH_STORAGE_KEY = 'releaseNotesPreviousManifestHash';
|
||||
const VERSION_WATERMARK_STORAGE_KEY = 'releaseNotesVersionWatermark';
|
||||
|
||||
type MinimalEventsType = {
|
||||
on(event: 'timetravel', callback: () => void): void;
|
||||
};
|
||||
|
||||
type ManifestReleaseNoteType = WithRequiredProperties<
|
||||
ReleaseNotesManifestResponseType['announcements'][0],
|
||||
'desktopMinVersion'
|
||||
>;
|
||||
|
||||
export type ReleaseNoteType = ReleaseNoteResponseType &
|
||||
Pick<ReleaseNotesManifestResponseType['announcements'][0], 'ctaId' | 'link'>;
|
||||
|
||||
let initComplete = false;
|
||||
|
||||
export class ReleaseNotesFetcher {
|
||||
private timeout: NodeJS.Timeout | undefined;
|
||||
private isRunning = false;
|
||||
|
||||
protected async scheduleUpdateForNow(): Promise<void> {
|
||||
const now = Date.now();
|
||||
await window.textsecure.storage.put(NEXT_FETCH_TIME_STORAGE_KEY, now);
|
||||
}
|
||||
|
||||
protected setTimeoutForNextRun(): void {
|
||||
const now = Date.now();
|
||||
const time = window.textsecure.storage.get(
|
||||
NEXT_FETCH_TIME_STORAGE_KEY,
|
||||
now
|
||||
);
|
||||
|
||||
log.info(
|
||||
'ReleaseNotesFetcher: Next update scheduled for',
|
||||
new Date(time).toISOString()
|
||||
);
|
||||
|
||||
let waitTime = time - now;
|
||||
if (waitTime < 0) {
|
||||
waitTime = 0;
|
||||
}
|
||||
|
||||
clearTimeoutIfNecessary(this.timeout);
|
||||
this.timeout = setTimeout(() => this.runWhenOnline(), waitTime);
|
||||
}
|
||||
|
||||
private getOrInitializeVersionWatermark(): string {
|
||||
const versionWatermark = window.textsecure.storage.get(
|
||||
VERSION_WATERMARK_STORAGE_KEY
|
||||
);
|
||||
if (versionWatermark) {
|
||||
return versionWatermark;
|
||||
}
|
||||
|
||||
log.info(
|
||||
'ReleaseNotesFetcher: Initializing version high watermark to current version'
|
||||
);
|
||||
const currentVersion = window.getVersion();
|
||||
drop(
|
||||
window.textsecure.storage.put(
|
||||
VERSION_WATERMARK_STORAGE_KEY,
|
||||
currentVersion
|
||||
)
|
||||
);
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
private async getReleaseNote(
|
||||
note: ManifestReleaseNoteType
|
||||
): Promise<ReleaseNoteType | undefined> {
|
||||
if (!window.textsecure.server) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { uuid, ctaId, link } = note;
|
||||
const result = await window.textsecure.server.getReleaseNote({
|
||||
uuid,
|
||||
});
|
||||
strictAssert(
|
||||
result.uuid === uuid,
|
||||
'UUID of localized release note should match requested UUID'
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
uuid,
|
||||
ctaId,
|
||||
link,
|
||||
};
|
||||
}
|
||||
|
||||
private async processReleaseNotes(
|
||||
notes: ReadonlyArray<ManifestReleaseNoteType>
|
||||
): Promise<void> {
|
||||
const sortedNotes = [...notes].sort(
|
||||
(a: ManifestReleaseNoteType, b: ManifestReleaseNoteType) =>
|
||||
semver.compare(a.desktopMinVersion, b.desktopMinVersion)
|
||||
);
|
||||
const hydratedNotes = [];
|
||||
for (const note of sortedNotes) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
hydratedNotes.push(await this.getReleaseNote(note));
|
||||
}
|
||||
if (!hydratedNotes.length) {
|
||||
log.warn('ReleaseNotesFetcher: No hydrated notes available, stopping');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('ReleaseNotesFetcher: Ensuring Signal conversation');
|
||||
const signalConversation =
|
||||
await window.ConversationController.getOrCreateSignalConversation();
|
||||
|
||||
const messages: Array<MessageAttributesType> = [];
|
||||
hydratedNotes.forEach(async (note, index) => {
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { title, body } = note;
|
||||
const messageBody = `${title}\n\n${body}`;
|
||||
const bodyRanges = [
|
||||
{ start: 0, length: title.length, style: BodyRange.Style.BOLD },
|
||||
];
|
||||
const timestamp = Date.now() + index;
|
||||
|
||||
const message: MessageAttributesType = {
|
||||
...generateMessageId(incrementMessageCounter()),
|
||||
body: messageBody,
|
||||
bodyRanges,
|
||||
conversationId: signalConversation.id,
|
||||
readStatus: ReadStatus.Unread,
|
||||
seenStatus: SeenStatus.Unseen,
|
||||
received_at_ms: timestamp,
|
||||
sent_at: timestamp,
|
||||
serverTimestamp: timestamp,
|
||||
sourceDevice: 1,
|
||||
sourceServiceId: signalConversation.getServiceId(),
|
||||
timestamp,
|
||||
type: 'incoming',
|
||||
};
|
||||
|
||||
window.MessageCache.toMessageAttributes(message);
|
||||
signalConversation.trigger('newmessage', message);
|
||||
|
||||
messages.push(message);
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
messages.map(message => saveNewMessageBatcher.add(message))
|
||||
);
|
||||
|
||||
signalConversation.set({ active_at: Date.now(), isArchived: false });
|
||||
drop(signalConversation.updateUnread());
|
||||
|
||||
const newestNote = last(sortedNotes);
|
||||
strictAssert(newestNote, 'processReleaseNotes requires at least 1 note');
|
||||
|
||||
const versionWatermark = newestNote.desktopMinVersion;
|
||||
log.info(
|
||||
`ReleaseNotesFetcher: Updating version watermark to ${versionWatermark}`
|
||||
);
|
||||
drop(
|
||||
window.textsecure.storage.put(
|
||||
VERSION_WATERMARK_STORAGE_KEY,
|
||||
versionWatermark
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async scheduleForNextRun(): Promise<void> {
|
||||
const now = Date.now();
|
||||
const nextTime = now + FETCH_INTERVAL;
|
||||
await window.textsecure.storage.put(NEXT_FETCH_TIME_STORAGE_KEY, nextTime);
|
||||
}
|
||||
|
||||
private async run(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
log.warn('ReleaseNotesFetcher: Already running, preventing reentrancy');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
log.info('ReleaseNotesFetcher: Starting');
|
||||
try {
|
||||
const versionWatermark = this.getOrInitializeVersionWatermark();
|
||||
log.info(`ReleaseNotesFetcher: Version watermark is ${versionWatermark}`);
|
||||
|
||||
if (!window.textsecure.server) {
|
||||
log.info('ReleaseNotesFetcher: WebAPI unavailable');
|
||||
throw new Error('WebAPI unavailable');
|
||||
}
|
||||
|
||||
const hash = await window.textsecure.server.getReleaseNotesManifestHash();
|
||||
if (!hash) {
|
||||
throw new Error('Release notes manifest hash missing');
|
||||
}
|
||||
|
||||
const previousHash = window.textsecure.storage.get(
|
||||
PREVIOUS_MANIFEST_HASH_STORAGE_KEY
|
||||
);
|
||||
if (hash !== previousHash) {
|
||||
log.info('ReleaseNotesFetcher: Manifest hash changed, fetching');
|
||||
const manifest =
|
||||
await window.textsecure.server.getReleaseNotesManifest();
|
||||
const validNotes = manifest.announcements.filter(
|
||||
(note): note is ManifestReleaseNoteType =>
|
||||
note.desktopMinVersion != null &&
|
||||
semver.gt(note.desktopMinVersion, versionWatermark)
|
||||
);
|
||||
if (validNotes.length) {
|
||||
log.info(
|
||||
`ReleaseNotesFetcher: Processing ${validNotes.length} new release notes`
|
||||
);
|
||||
drop(this.processReleaseNotes(validNotes));
|
||||
} else {
|
||||
log.info('ReleaseNotesFetcher: No new release notes');
|
||||
}
|
||||
|
||||
drop(
|
||||
window.textsecure.storage.put(
|
||||
PREVIOUS_MANIFEST_HASH_STORAGE_KEY,
|
||||
hash
|
||||
)
|
||||
);
|
||||
} else {
|
||||
log.info('ReleaseNotesFetcher: Manifest hash unchanged');
|
||||
}
|
||||
|
||||
await this.scheduleForNextRun();
|
||||
this.setTimeoutForNextRun();
|
||||
} catch (error) {
|
||||
const errorString =
|
||||
error instanceof HTTPError
|
||||
? error.code.toString()
|
||||
: Errors.toLogFormat(error);
|
||||
log.error(
|
||||
`ReleaseNotesFetcher: Error, trying again later. ${errorString}`
|
||||
);
|
||||
setTimeout(() => this.setTimeoutForNextRun(), ERROR_RETRY_DELAY);
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private runWhenOnline() {
|
||||
if (window.textsecure.server?.isOnline()) {
|
||||
drop(this.run());
|
||||
} else {
|
||||
log.info(
|
||||
'ReleaseNotesFetcher: We are offline; will fetch when we are next online'
|
||||
);
|
||||
const listener = () => {
|
||||
window.Whisper.events.off('online', listener);
|
||||
this.setTimeoutForNextRun();
|
||||
};
|
||||
window.Whisper.events.on('online', listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static async init(
|
||||
events: MinimalEventsType,
|
||||
isNewVersion: boolean
|
||||
): Promise<void> {
|
||||
if (initComplete || !this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
initComplete = true;
|
||||
|
||||
const listener = new ReleaseNotesFetcher();
|
||||
|
||||
if (isNewVersion) {
|
||||
await listener.scheduleUpdateForNow();
|
||||
}
|
||||
listener.setTimeoutForNextRun();
|
||||
|
||||
events.on('timetravel', () => {
|
||||
if (Registration.isDone()) {
|
||||
listener.setTimeoutForNextRun();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static isEnabled(): boolean {
|
||||
const version = window.getVersion();
|
||||
|
||||
if (isProduction(version)) {
|
||||
return RemoteConfig.isEnabled('desktop.releaseNotes');
|
||||
}
|
||||
|
||||
if (isBeta(version)) {
|
||||
return RemoteConfig.isEnabled('desktop.releaseNotes.beta');
|
||||
}
|
||||
|
||||
return RemoteConfig.isEnabled('desktop.releaseNotes.dev');
|
||||
}
|
||||
}
|
|
@ -380,10 +380,6 @@ export const _getLeftPaneLists = (
|
|||
};
|
||||
}
|
||||
|
||||
if (isSignalConversation(conversation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We always show pinned conversations
|
||||
if (conversation.isPinned) {
|
||||
pinnedConversations.push(conversation);
|
||||
|
|
|
@ -43,6 +43,7 @@ import { getKeysForServiceId } from './getKeysForServiceId';
|
|||
import { SignalService as Proto } from '../protobuf';
|
||||
import * as log from '../logging/log';
|
||||
import type { GroupSendToken } from '../types/GroupSendEndorsements';
|
||||
import { isSignalServiceId } from '../util/isSignalConversation';
|
||||
|
||||
export const enum SenderCertificateMode {
|
||||
WithE164,
|
||||
|
@ -686,6 +687,15 @@ export default class OutgoingMessage {
|
|||
}
|
||||
|
||||
async sendToServiceId(serviceId: ServiceIdString): Promise<void> {
|
||||
if (isSignalServiceId(serviceId)) {
|
||||
this.registerError(
|
||||
serviceId,
|
||||
'Failed to send to Signal serviceId',
|
||||
new Error("Can't send to Signal serviceId")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ourAci = window.textsecure.storage.user.getCheckedAci();
|
||||
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
|
||||
|
|
|
@ -657,6 +657,8 @@ const URL_CALLS = {
|
|||
callLinkCreateAuth: 'v1/call-link/create-auth',
|
||||
registration: 'v1/registration',
|
||||
registerCapabilities: 'v1/devices/capabilities',
|
||||
releaseNotesManifest: 'dynamic/release-notes/release-notes-v2.json',
|
||||
releaseNotes: 'static/release-notes',
|
||||
reportMessage: 'v1/messages/report',
|
||||
setBackupId: 'v1/archives/backupid',
|
||||
setBackupSignatureKey: 'v1/archives/keys',
|
||||
|
@ -1209,6 +1211,56 @@ export type GetBackupInfoResponseType = z.infer<
|
|||
typeof getBackupInfoResponseSchema
|
||||
>;
|
||||
|
||||
export type GetReleaseNoteOptionsType = Readonly<{
|
||||
uuid: string;
|
||||
}>;
|
||||
|
||||
export const releaseNoteSchema = z.object({
|
||||
uuid: z.string(),
|
||||
title: z.string(),
|
||||
body: z.string(),
|
||||
linkText: z.string().optional(),
|
||||
callToActionText: z.string().optional(),
|
||||
includeBoostMessage: z.boolean().optional().default(true),
|
||||
bodyRanges: z
|
||||
.array(
|
||||
z.object({
|
||||
style: z.string(),
|
||||
start: z.number(),
|
||||
length: z.number(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
media: z.string().optional(),
|
||||
mediaHeight: z.coerce
|
||||
.number()
|
||||
.optional()
|
||||
.transform(x => x || undefined),
|
||||
mediaWidth: z.coerce
|
||||
.number()
|
||||
.optional()
|
||||
.transform(x => x || undefined),
|
||||
mediaContentType: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ReleaseNoteResponseType = z.infer<typeof releaseNoteSchema>;
|
||||
|
||||
export const releaseNotesManifestSchema = z.object({
|
||||
announcements: z
|
||||
.object({
|
||||
uuid: z.string(),
|
||||
countries: z.string().optional(),
|
||||
desktopMinVersion: z.string().optional(),
|
||||
link: z.string().optional(),
|
||||
ctaId: z.string().optional(),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
|
||||
export type ReleaseNotesManifestResponseType = z.infer<
|
||||
typeof releaseNotesManifestSchema
|
||||
>;
|
||||
|
||||
export type CallLinkCreateAuthResponseType = Readonly<{
|
||||
credential: string;
|
||||
}>;
|
||||
|
@ -1339,6 +1391,11 @@ export type WebAPIType = {
|
|||
getSenderCertificate: (
|
||||
withUuid?: boolean
|
||||
) => Promise<GetSenderCertificateResultType>;
|
||||
getReleaseNote: (
|
||||
options: GetReleaseNoteOptionsType
|
||||
) => Promise<ReleaseNoteResponseType>;
|
||||
getReleaseNotesManifest: () => Promise<ReleaseNotesManifestResponseType>;
|
||||
getReleaseNotesManifestHash: () => Promise<string | undefined>;
|
||||
getSticker: (packId: string, stickerId: number) => Promise<Uint8Array>;
|
||||
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
|
||||
getStorageCredentials: MessageSender['getStorageCredentials'];
|
||||
|
@ -1807,6 +1864,9 @@ export function initialize({
|
|||
getProfile,
|
||||
getProfileUnauth,
|
||||
getProvisioningResource,
|
||||
getReleaseNote,
|
||||
getReleaseNotesManifest,
|
||||
getReleaseNotesManifestHash,
|
||||
getTransferArchive,
|
||||
getSenderCertificate,
|
||||
getSocketStatus,
|
||||
|
@ -2099,6 +2159,44 @@ export function initialize({
|
|||
languages: Record<string, Array<string>>;
|
||||
};
|
||||
}
|
||||
async function getReleaseNote({
|
||||
uuid,
|
||||
}: GetReleaseNoteOptionsType): Promise<ReleaseNoteResponseType> {
|
||||
const rawRes = await _ajax({
|
||||
call: 'releaseNotes',
|
||||
host: resourcesUrl,
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
urlParameters: `/${uuid}/en.json`,
|
||||
});
|
||||
return parseUnknown(releaseNoteSchema, rawRes);
|
||||
}
|
||||
|
||||
async function getReleaseNotesManifest(): Promise<ReleaseNotesManifestResponseType> {
|
||||
const rawRes = await _ajax({
|
||||
call: 'releaseNotesManifest',
|
||||
host: resourcesUrl,
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
});
|
||||
return parseUnknown(releaseNotesManifestSchema, rawRes);
|
||||
}
|
||||
|
||||
async function getReleaseNotesManifestHash(): Promise<string | undefined> {
|
||||
const { response } = await _ajax({
|
||||
call: 'releaseNotesManifest',
|
||||
host: resourcesUrl,
|
||||
httpType: 'HEAD',
|
||||
responseType: 'byteswithdetails',
|
||||
});
|
||||
|
||||
const etag = response.headers.get('etag');
|
||||
if (etag == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return etag;
|
||||
}
|
||||
|
||||
async function getStorageManifest(
|
||||
options: StorageServiceCallOptionsType = {}
|
||||
|
|
3
ts/types/Storage.d.ts
vendored
3
ts/types/Storage.d.ts
vendored
|
@ -196,6 +196,9 @@ export type StorageAccessType = {
|
|||
// Note: Upon capability deprecation - change the value type to `never` and
|
||||
// remove it in `ts/background.ts`
|
||||
};
|
||||
releaseNotesNextFetchTime: number;
|
||||
releaseNotesVersionWatermark: string;
|
||||
releaseNotesPreviousManifestHash: string;
|
||||
|
||||
// If present - we are downloading backup
|
||||
backupDownloadPath: string;
|
||||
|
|
|
@ -16,3 +16,7 @@ export function isSignalConversation(conversation: {
|
|||
|
||||
return window.ConversationController.isSignalConversationId(id);
|
||||
}
|
||||
|
||||
export function isSignalServiceId(serviceId: ServiceIdString): boolean {
|
||||
return serviceId === SIGNAL_ACI;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { isConversationUnregistered } from './isConversationUnregistered';
|
|||
import { missingCaseError } from './missingCaseError';
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import { mapEmplace } from './mapEmplace';
|
||||
import { isSignalConversation } from './isSignalConversation';
|
||||
|
||||
const CHUNK_SIZE = 100;
|
||||
|
||||
|
@ -124,7 +125,12 @@ export async function sendReceipts({
|
|||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSignalConversation(sender.attributes)) {
|
||||
log.info(
|
||||
`conversation ${sender.idForLogging()} is Signal conversation; refusing to send`
|
||||
);
|
||||
return;
|
||||
}
|
||||
log.info(`Sending receipt of type ${type} to ${sender.idForLogging()}`);
|
||||
|
||||
const conversation = window.ConversationController.get(conversationId);
|
||||
|
|
Loading…
Reference in a new issue