Release Notes Channel: Support image attachments (#9587)
This commit is contained in:
parent
91e42b4f2e
commit
4a55ac4c86
10 changed files with 257 additions and 50 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -123,7 +123,7 @@
|
||||||
"@indutny/parallel-prettier": "3.0.0",
|
"@indutny/parallel-prettier": "3.0.0",
|
||||||
"@indutny/rezip-electron": "2.0.1",
|
"@indutny/rezip-electron": "2.0.1",
|
||||||
"@napi-rs/canvas": "0.1.61",
|
"@napi-rs/canvas": "0.1.61",
|
||||||
"@signalapp/mock-server": "10.4.1",
|
"@signalapp/mock-server": "10.5.0",
|
||||||
"@storybook/addon-a11y": "8.4.4",
|
"@storybook/addon-a11y": "8.4.4",
|
||||||
"@storybook/addon-actions": "8.4.4",
|
"@storybook/addon-actions": "8.4.4",
|
||||||
"@storybook/addon-controls": "8.4.4",
|
"@storybook/addon-controls": "8.4.4",
|
||||||
|
@ -6491,9 +6491,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@signalapp/mock-server": {
|
"node_modules/@signalapp/mock-server": {
|
||||||
"version": "10.4.1",
|
"version": "10.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@signalapp/mock-server/-/mock-server-10.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@signalapp/mock-server/-/mock-server-10.5.0.tgz",
|
||||||
"integrity": "sha512-9aBmFbOx3KfbN4Ptcx5PBRnVnROe6A58rRoErB/w1x+SSW9TjRHZxviJfgNpt9nGUOreryzOBsONSzWBIE12nQ==",
|
"integrity": "sha512-w7KXcWYRPXhAxYWzeBNesu+A9DO6FYJUhg52md3M9yQkPR2Yr/YBvc9zBhFo5fafZmkaRoxDoz9y29Ivd5ZykQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -214,7 +214,7 @@
|
||||||
"@indutny/parallel-prettier": "3.0.0",
|
"@indutny/parallel-prettier": "3.0.0",
|
||||||
"@indutny/rezip-electron": "2.0.1",
|
"@indutny/rezip-electron": "2.0.1",
|
||||||
"@napi-rs/canvas": "0.1.61",
|
"@napi-rs/canvas": "0.1.61",
|
||||||
"@signalapp/mock-server": "10.4.1",
|
"@signalapp/mock-server": "10.5.0",
|
||||||
"@storybook/addon-a11y": "8.4.4",
|
"@storybook/addon-a11y": "8.4.4",
|
||||||
"@storybook/addon-actions": "8.4.4",
|
"@storybook/addon-actions": "8.4.4",
|
||||||
"@storybook/addon-controls": "8.4.4",
|
"@storybook/addon-controls": "8.4.4",
|
||||||
|
|
|
@ -27,6 +27,8 @@ import type {
|
||||||
} from '../textsecure/WebAPI';
|
} from '../textsecure/WebAPI';
|
||||||
import type { WithRequiredProperties } from '../types/Util';
|
import type { WithRequiredProperties } from '../types/Util';
|
||||||
import { MessageModel } from '../models/messages';
|
import { MessageModel } from '../models/messages';
|
||||||
|
import { stringToMIMEType } from '../types/MIME';
|
||||||
|
import { isNotNil } from '../util/isNotNil';
|
||||||
|
|
||||||
const FETCH_INTERVAL = 3 * durations.DAY;
|
const FETCH_INTERVAL = 3 * durations.DAY;
|
||||||
const ERROR_RETRY_DELAY = 3 * durations.HOUR;
|
const ERROR_RETRY_DELAY = 3 * durations.HOUR;
|
||||||
|
@ -103,7 +105,8 @@ export class ReleaseNotesFetcher {
|
||||||
note: ManifestReleaseNoteType
|
note: ManifestReleaseNoteType
|
||||||
): Promise<ReleaseNoteType | undefined> {
|
): Promise<ReleaseNoteType | undefined> {
|
||||||
if (!window.textsecure.server) {
|
if (!window.textsecure.server) {
|
||||||
return undefined;
|
log.info('ReleaseNotesFetcher: WebAPI unavailable');
|
||||||
|
throw new Error('WebAPI unavailable');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { uuid, ctaId, link } = note;
|
const { uuid, ctaId, link } = note;
|
||||||
|
@ -157,15 +160,75 @@ export class ReleaseNotesFetcher {
|
||||||
async #processReleaseNotes(
|
async #processReleaseNotes(
|
||||||
notes: ReadonlyArray<ManifestReleaseNoteType>
|
notes: ReadonlyArray<ManifestReleaseNoteType>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (!window.textsecure.server) {
|
||||||
|
log.info('ReleaseNotesFetcher: WebAPI unavailable');
|
||||||
|
throw new Error('WebAPI unavailable');
|
||||||
|
}
|
||||||
const sortedNotes = [...notes].sort(
|
const sortedNotes = [...notes].sort(
|
||||||
(a: ManifestReleaseNoteType, b: ManifestReleaseNoteType) =>
|
(a: ManifestReleaseNoteType, b: ManifestReleaseNoteType) =>
|
||||||
semver.compare(a.desktopMinVersion, b.desktopMinVersion)
|
semver.compare(a.desktopMinVersion, b.desktopMinVersion)
|
||||||
);
|
);
|
||||||
const hydratedNotes = [];
|
|
||||||
for (const note of sortedNotes) {
|
const hydratedNotesWithRawAttachments = (
|
||||||
// eslint-disable-next-line no-await-in-loop
|
await Promise.all(
|
||||||
hydratedNotes.push(await this.#getReleaseNote(note));
|
sortedNotes.map(async note => {
|
||||||
}
|
if (!window.textsecure.server) {
|
||||||
|
log.info('ReleaseNotesFetcher: WebAPI unavailable');
|
||||||
|
throw new Error('WebAPI unavailable');
|
||||||
|
}
|
||||||
|
if (!note) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hydratedNote = await this.#getReleaseNote(note);
|
||||||
|
if (!hydratedNote) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (hydratedNote.media) {
|
||||||
|
const { imageData: rawAttachmentData, contentType } =
|
||||||
|
await window.textsecure.server.getReleaseNoteImageAttachment(
|
||||||
|
hydratedNote.media
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hydratedNote,
|
||||||
|
rawAttachmentData,
|
||||||
|
contentType: hydratedNote.mediaContentType ?? contentType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hydratedNote, rawAttachmentData: null, contentType: null };
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).filter(isNotNil);
|
||||||
|
|
||||||
|
const hydratedNotes = await Promise.all(
|
||||||
|
hydratedNotesWithRawAttachments.map(
|
||||||
|
async ({ hydratedNote, rawAttachmentData, contentType }) => {
|
||||||
|
if (rawAttachmentData && !contentType) {
|
||||||
|
throw new Error('Content type is missing from attachment');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawAttachmentData || !contentType) {
|
||||||
|
return { hydratedNote, processedAttachment: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const localAttachment =
|
||||||
|
await window.Signal.Migrations.writeNewAttachmentData(
|
||||||
|
rawAttachmentData
|
||||||
|
);
|
||||||
|
|
||||||
|
const processedAttachment =
|
||||||
|
await window.Signal.Migrations.processNewAttachment({
|
||||||
|
...localAttachment,
|
||||||
|
contentType: stringToMIMEType(contentType),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { hydratedNote, processedAttachment };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (!hydratedNotes.length) {
|
if (!hydratedNotes.length) {
|
||||||
log.warn('ReleaseNotesFetcher: No hydrated notes available, stopping');
|
log.warn('ReleaseNotesFetcher: No hydrated notes available, stopping');
|
||||||
return;
|
return;
|
||||||
|
@ -176,39 +239,44 @@ export class ReleaseNotesFetcher {
|
||||||
await window.ConversationController.getOrCreateSignalConversation();
|
await window.ConversationController.getOrCreateSignalConversation();
|
||||||
|
|
||||||
const messages: Array<MessageAttributesType> = [];
|
const messages: Array<MessageAttributesType> = [];
|
||||||
hydratedNotes.forEach(async (note, index) => {
|
hydratedNotes.forEach(
|
||||||
if (!note) {
|
({ hydratedNote: note, processedAttachment }, index) => {
|
||||||
return;
|
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 = new MessageModel({
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
...(processedAttachment
|
||||||
|
? { attachments: [processedAttachment] }
|
||||||
|
: {}),
|
||||||
|
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.register(message);
|
||||||
|
drop(signalConversation.onNewMessage(message));
|
||||||
|
|
||||||
|
messages.push(message.attributes);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
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 = new MessageModel({
|
|
||||||
...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.register(message);
|
|
||||||
drop(signalConversation.onNewMessage(message));
|
|
||||||
|
|
||||||
messages.push(message.attributes);
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
messages.map(message => saveNewMessageBatcher.add(message))
|
messages.map(message => saveNewMessageBatcher.add(message))
|
||||||
|
@ -276,7 +344,7 @@ export class ReleaseNotesFetcher {
|
||||||
log.info(
|
log.info(
|
||||||
`ReleaseNotesFetcher: Processing ${validNotes.length} new release notes`
|
`ReleaseNotesFetcher: Processing ${validNotes.length} new release notes`
|
||||||
);
|
);
|
||||||
drop(this.#processReleaseNotes(validNotes));
|
await this.#processReleaseNotes(validNotes);
|
||||||
} else {
|
} else {
|
||||||
log.info('ReleaseNotesFetcher: No new release notes');
|
log.info('ReleaseNotesFetcher: No new release notes');
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,6 +191,7 @@ export class Bootstrap {
|
||||||
// Limit number of storage read keys for easier testing
|
// Limit number of storage read keys for easier testing
|
||||||
maxStorageReadKeys: MAX_STORAGE_READ_KEYS,
|
maxStorageReadKeys: MAX_STORAGE_READ_KEYS,
|
||||||
cdn3Path: this.cdn3Path,
|
cdn3Path: this.cdn3Path,
|
||||||
|
updates2Path: path.join(__dirname, 'updates-data'),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#options = {
|
this.#options = {
|
||||||
|
|
|
@ -253,13 +253,21 @@ export async function createGroup(
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function clickOnConversationWithAci(
|
||||||
|
page: Page,
|
||||||
|
aci: string
|
||||||
|
): Promise<void> {
|
||||||
|
const leftPane = page.locator('#LeftPane');
|
||||||
|
await leftPane.getByTestId(aci).click();
|
||||||
|
}
|
||||||
|
|
||||||
export async function clickOnConversation(
|
export async function clickOnConversation(
|
||||||
page: Page,
|
page: Page,
|
||||||
contact: PrimaryDevice
|
contact: PrimaryDevice
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const leftPane = page.locator('#LeftPane');
|
await clickOnConversationWithAci(page, contact.device.aci);
|
||||||
await leftPane.getByTestId(contact.device.aci).click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pinContact(
|
export async function pinContact(
|
||||||
phone: PrimaryDevice,
|
phone: PrimaryDevice,
|
||||||
contact: PrimaryDevice
|
contact: PrimaryDevice
|
||||||
|
|
|
@ -2,13 +2,17 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import createDebug from 'debug';
|
import createDebug from 'debug';
|
||||||
import { assert } from 'chai';
|
|
||||||
|
|
||||||
|
import { expect } from 'playwright/test';
|
||||||
import type { App } from '../playwright';
|
import type { App } from '../playwright';
|
||||||
import { Bootstrap } from '../bootstrap';
|
import { Bootstrap } from '../bootstrap';
|
||||||
import { MINUTE } from '../../util/durations';
|
import { MINUTE } from '../../util/durations';
|
||||||
|
|
||||||
import { SIGNAL_ACI } from '../../types/SignalConversation';
|
import { SIGNAL_ACI } from '../../types/SignalConversation';
|
||||||
|
import {
|
||||||
|
clickOnConversationWithAci,
|
||||||
|
getTimelineMessageWithText,
|
||||||
|
} from '../helpers';
|
||||||
|
|
||||||
export const debug = createDebug('mock:test:releaseNotes');
|
export const debug = createDebug('mock:test:releaseNotes');
|
||||||
|
|
||||||
|
@ -37,7 +41,7 @@ describe('release notes', function (this: Mocha.Suite) {
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows release notes', async () => {
|
it('shows release notes with an image', async () => {
|
||||||
const firstWindow = await app.getWindow();
|
const firstWindow = await app.getWindow();
|
||||||
|
|
||||||
await firstWindow.evaluate('window.SignalCI.resetReleaseNotesFetcher()');
|
await firstWindow.evaluate('window.SignalCI.resetReleaseNotesFetcher()');
|
||||||
|
@ -52,6 +56,17 @@ describe('release notes', function (this: Mocha.Suite) {
|
||||||
const releaseNoteConversation = leftPane.getByTestId(SIGNAL_ACI);
|
const releaseNoteConversation = leftPane.getByTestId(SIGNAL_ACI);
|
||||||
await releaseNoteConversation.waitFor();
|
await releaseNoteConversation.waitFor();
|
||||||
|
|
||||||
assert.isTrue(await releaseNoteConversation.isVisible());
|
await expect(releaseNoteConversation).toBeVisible();
|
||||||
|
|
||||||
|
await clickOnConversationWithAci(secondWindow, SIGNAL_ACI);
|
||||||
|
|
||||||
|
const timelineMessage = await getTimelineMessageWithText(
|
||||||
|
secondWindow,
|
||||||
|
'Call links'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
timelineMessage.locator('img.module-image__image')
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"announcements": [
|
||||||
|
{
|
||||||
|
"ctaId": "chat_folder",
|
||||||
|
"androidMinVersion": "9999",
|
||||||
|
"uuid": "b1b8cafb-11e8-490b-858d-dd26fc47c58a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ctaId": "calls_tab",
|
||||||
|
"desktopMinVersion": "7.30.0",
|
||||||
|
"androidMinVersion": "1482",
|
||||||
|
"uuid": "1813845b-cc98-4210-a07c-164f9b328ca8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"megaphones": [
|
||||||
|
{
|
||||||
|
"primaryCtaId": "donate",
|
||||||
|
"secondaryCtaId": "snooze",
|
||||||
|
"countries": "27:1000000,298:1000000,299:1000000,30:1000000,31:1000000,32:1000000,33:1000000,34:1000000,351:1000000,352:1000000,353:1000000,354:1000000,356:1000000,357:1000000,358:1000000,359:1000000,36:1000000,370:1000000,371:1000000,372:1000000,373:1000000,374:1000000,375:1000000,377:1000000,378:1000000,379:1000000,380:1000000,381:1000000,382:1000000,385:1000000,386:1000000,39:1000000,40:1000000,41:1000000,420:1000000,421:1000000,423:1000000,43:1000000,44:1000000,45:1000000,46:1000000,47:1000000,48:1000000,49:1000000,506:1000000,51:1000000,52:1000000,54:1000000,55:1000000,57:1000000,60:1000000,61:1000000,64:1000000,65:1000000,7:1000000,81:1000000,852:1000000,853:1000000,86:1000000,886:1000000,966:1000000,970:1000000,971:1000000,972:1000000,973:1000000,974:1000000,994:1000000,995:1000000",
|
||||||
|
"iosMinVersion": "6.1.0.17",
|
||||||
|
"priority": 100,
|
||||||
|
"dontShowBeforeEpochSeconds": 1732024800,
|
||||||
|
"uuid": "2f4f0c57-8e3b-437d-9d4b-6b84139cf5d7",
|
||||||
|
"showForNumberOfDays": 30,
|
||||||
|
"conditionalId": "standard_donate",
|
||||||
|
"dontShowAfterEpochSeconds": 1734616800,
|
||||||
|
"secondaryCtaData": { "snoozeDurationDays": [5, 7, 9, 100] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"primaryCtaId": "donate",
|
||||||
|
"secondaryCtaId": "snooze",
|
||||||
|
"androidMinVersion": "1164",
|
||||||
|
"countries": "20:1000000,212:1000000,213:1000000,216:1000000,218:1000000,221:1000000,223:1000000,225:1000000,226:1000000,229:1000000,230:1000000,233:1000000,234:1000000,237:1000000,243:1000000,244:1000000,254:1000000,255:1000000,256:1000000,27:1000000,30:1000000,31:1000000,32:1000000,33:1000000,34:1000000,351:1000000,352:1000000,353:1000000,354:1000000,355:1000000,356:1000000,357:1000000,358:1000000,359:1000000,36:1000000,370:1000000,371:1000000,372:1000000,373:1000000,374:1000000,380:1000000,381:1000000,382:1000000,383:1000000,385:1000000,386:1000000,387:1000000,389:1000000,39:1000000,40:1000000,41:1000000,420:1000000,421:1000000,43:1000000,44:1000000,45:1000000,46:1000000,47:1000000,48:1000000,49:1000000,502:1000000,503:1000000,505:1000000,506:1000000,507:1000000,51:1000000,52:1000000,54:1000000,55:1000000,56:1000000,57:1000000,58:1000000,591:1000000,593:1000000,595:1000000,596:1000000,598:1000000,60:1000000,61:1000000,62:1000000,63:1000000,64:1000000,65:1000000,66:1000000,7:1000000,81:1000000,82:1000000,84:1000000,852:1000000,853:1000000,855:1000000,880:1000000,886:1000000,90:1000000,91:1000000,92:1000000,93:1000000,94:1000000,95:1000000,961:1000000,962:1000000,964:1000000,965:1000000,966:1000000,967:1000000,968:1000000,971:1000000,972:1000000,973:1000000,974:1000000,977:1000000,994:1000000,995:1000000,996:1000000",
|
||||||
|
"priority": 100,
|
||||||
|
"dontShowBeforeEpochSeconds": 1732024800,
|
||||||
|
"uuid": "0ef1b765-1887-4620-b6a7-e4284720acde",
|
||||||
|
"showForNumberOfDays": 30,
|
||||||
|
"conditionalId": "standard_donate",
|
||||||
|
"dontShowAfterEpochSeconds": 1734616800,
|
||||||
|
"secondaryCtaData": { "snoozeDurationDays": [5, 7, 9, 100] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"primaryCtaId": "donate",
|
||||||
|
"secondaryCtaId": "snooze",
|
||||||
|
"countries": "1:1000000",
|
||||||
|
"iosMinVersion": "6.1.0.17",
|
||||||
|
"priority": 100,
|
||||||
|
"dontShowBeforeEpochSeconds": 1732024800,
|
||||||
|
"uuid": "999f2573-7d83-4d60-b045-259de448da8d",
|
||||||
|
"showForNumberOfDays": 30,
|
||||||
|
"conditionalId": "standard_donate",
|
||||||
|
"dontShowAfterEpochSeconds": 1734616800,
|
||||||
|
"secondaryCtaData": { "snoozeDurationDays": [5, 7, 9, 100] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"primaryCtaId": "donate",
|
||||||
|
"secondaryCtaId": "snooze",
|
||||||
|
"androidMinVersion": "1164",
|
||||||
|
"countries": "1:1000000",
|
||||||
|
"priority": 100,
|
||||||
|
"dontShowBeforeEpochSeconds": 1732024800,
|
||||||
|
"uuid": "9c3f0ba5-cce1-400b-b3b1-af34d5f073a5",
|
||||||
|
"showForNumberOfDays": 30,
|
||||||
|
"conditionalId": "standard_donate",
|
||||||
|
"dontShowAfterEpochSeconds": 1734616800,
|
||||||
|
"secondaryCtaData": { "snoozeDurationDays": [5, 7, 9, 100] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"mediaHeight": "864",
|
||||||
|
"mediaWidth": "1536",
|
||||||
|
"media": "/static/release-notes/call_links.png",
|
||||||
|
"uuid": "1813845b-cc98-4210-a07c-164f9b328ca8",
|
||||||
|
"title": "Introducing Call Links",
|
||||||
|
"body": "Call links are the missing link for calendar invites and impromptu gatherings. Now you can quickly create an easy link that anyone on Signal can use to join a group call without having to join a Signal group chat first.\n\nCall links are reusable and ideal for recurring phone dates with your best friends or weekly check-ins with your coworkers. You can manage your call links, control approval settings, and copy links from the calls tab for quick sharing. ",
|
||||||
|
"callToActionText": "Create Call Link"
|
||||||
|
}
|
BIN
ts/test-mock/updates-data/static/release-notes/call_links.png
Normal file
BIN
ts/test-mock/updates-data/static/release-notes/call_links.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
|
@ -1267,6 +1267,11 @@ export type ReleaseNotesManifestResponseType = z.infer<
|
||||||
typeof releaseNotesManifestSchema
|
typeof releaseNotesManifestSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type GetReleaseNoteImageAttachmentResultType = Readonly<{
|
||||||
|
imageData: Uint8Array;
|
||||||
|
contentType: string | null;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type CallLinkCreateAuthResponseType = Readonly<{
|
export type CallLinkCreateAuthResponseType = Readonly<{
|
||||||
credential: string;
|
credential: string;
|
||||||
}>;
|
}>;
|
||||||
|
@ -1417,6 +1422,9 @@ export type WebAPIType = {
|
||||||
) => Promise<string | undefined>;
|
) => Promise<string | undefined>;
|
||||||
getReleaseNotesManifest: () => Promise<ReleaseNotesManifestResponseType>;
|
getReleaseNotesManifest: () => Promise<ReleaseNotesManifestResponseType>;
|
||||||
getReleaseNotesManifestHash: () => Promise<string | undefined>;
|
getReleaseNotesManifestHash: () => Promise<string | undefined>;
|
||||||
|
getReleaseNoteImageAttachment: (
|
||||||
|
path: string
|
||||||
|
) => Promise<GetReleaseNoteImageAttachmentResultType>;
|
||||||
getSticker: (packId: string, stickerId: number) => Promise<Uint8Array>;
|
getSticker: (packId: string, stickerId: number) => Promise<Uint8Array>;
|
||||||
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
|
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
|
||||||
getStorageCredentials: MessageSender['getStorageCredentials'];
|
getStorageCredentials: MessageSender['getStorageCredentials'];
|
||||||
|
@ -1890,6 +1898,7 @@ export function initialize({
|
||||||
getReleaseNoteHash,
|
getReleaseNoteHash,
|
||||||
getReleaseNotesManifest,
|
getReleaseNotesManifest,
|
||||||
getReleaseNotesManifestHash,
|
getReleaseNotesManifestHash,
|
||||||
|
getReleaseNoteImageAttachment,
|
||||||
getTransferArchive,
|
getTransferArchive,
|
||||||
getSenderCertificate,
|
getSenderCertificate,
|
||||||
getSocketStatus,
|
getSocketStatus,
|
||||||
|
@ -2257,6 +2266,29 @@ export function initialize({
|
||||||
return etag;
|
return etag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getReleaseNoteImageAttachment(
|
||||||
|
path: string
|
||||||
|
): Promise<GetReleaseNoteImageAttachmentResultType> {
|
||||||
|
const { origin: expectedOrigin } = new URL(resourcesUrl);
|
||||||
|
const url = `${resourcesUrl}${path}`;
|
||||||
|
const { origin } = new URL(url);
|
||||||
|
strictAssert(origin === expectedOrigin, `Unexpected origin: ${origin}`);
|
||||||
|
|
||||||
|
const { data: imageData, contentType } = await _outerAjax(url, {
|
||||||
|
certificateAuthority,
|
||||||
|
proxyUrl,
|
||||||
|
responseType: 'byteswithdetails',
|
||||||
|
timeout: 0,
|
||||||
|
type: 'GET',
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageData,
|
||||||
|
contentType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function getStorageManifest(
|
async function getStorageManifest(
|
||||||
options: StorageServiceCallOptionsType = {}
|
options: StorageServiceCallOptionsType = {}
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
|
@ -3917,7 +3949,12 @@ export function initialize({
|
||||||
if (options?.downloadOffset) {
|
if (options?.downloadOffset) {
|
||||||
targetHeaders.range = `bytes=${options.downloadOffset}-`;
|
targetHeaders.range = `bytes=${options.downloadOffset}-`;
|
||||||
}
|
}
|
||||||
streamWithDetails = await _outerAjax(`${cdnUrl}${cdnPath}`, {
|
const { origin: expectedOrigin } = new URL(cdnUrl);
|
||||||
|
const fullCdnUrl = `${cdnUrl}${cdnPath}`;
|
||||||
|
const { origin } = new URL(fullCdnUrl);
|
||||||
|
strictAssert(origin === expectedOrigin, `Unexpected origin: ${origin}`);
|
||||||
|
|
||||||
|
streamWithDetails = await _outerAjax(fullCdnUrl, {
|
||||||
headers: targetHeaders,
|
headers: targetHeaders,
|
||||||
certificateAuthority,
|
certificateAuthority,
|
||||||
disableRetries: options?.disableRetries,
|
disableRetries: options?.disableRetries,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue