Release Notes Channel: Add support for server body ranges (#9631)
This commit is contained in:
parent
6b2d65c1e7
commit
da7002fc64
6 changed files with 132 additions and 7 deletions
|
@ -18,6 +18,7 @@ import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||||
import { SeenStatus } from '../MessageSeenStatus';
|
import { SeenStatus } from '../MessageSeenStatus';
|
||||||
import { saveNewMessageBatcher } from '../util/messageBatcher';
|
import { saveNewMessageBatcher } from '../util/messageBatcher';
|
||||||
import { generateMessageId } from '../util/generateMessageId';
|
import { generateMessageId } from '../util/generateMessageId';
|
||||||
|
import type { RawBodyRange } from '../types/BodyRange';
|
||||||
import { BodyRange } from '../types/BodyRange';
|
import { BodyRange } from '../types/BodyRange';
|
||||||
import * as RemoteConfig from '../RemoteConfig';
|
import * as RemoteConfig from '../RemoteConfig';
|
||||||
import { isBeta, isProduction } from '../util/version';
|
import { isBeta, isProduction } from '../util/version';
|
||||||
|
@ -50,6 +51,13 @@ export type ReleaseNoteType = ReleaseNoteResponseType &
|
||||||
|
|
||||||
let initComplete = false;
|
let initComplete = false;
|
||||||
|
|
||||||
|
const STYLE_MAPPING: Record<string, BodyRange.Style> = {
|
||||||
|
bold: BodyRange.Style.BOLD,
|
||||||
|
italic: BodyRange.Style.ITALIC,
|
||||||
|
strikethrough: BodyRange.Style.STRIKETHROUGH,
|
||||||
|
spoiler: BodyRange.Style.SPOILER,
|
||||||
|
mono: BodyRange.Style.MONOSPACE,
|
||||||
|
};
|
||||||
export class ReleaseNotesFetcher {
|
export class ReleaseNotesFetcher {
|
||||||
#timeout: NodeJS.Timeout | undefined;
|
#timeout: NodeJS.Timeout | undefined;
|
||||||
#isRunning = false;
|
#isRunning = false;
|
||||||
|
@ -245,10 +253,37 @@ export class ReleaseNotesFetcher {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, body } = note;
|
const { title, body, bodyRanges: noteBodyRanges } = note;
|
||||||
const messageBody = `${title}\n\n${body}`;
|
const titleBodySeparator = '\n\n';
|
||||||
const bodyRanges = [
|
const filteredNoteBodyRanges: Array<RawBodyRange> = (
|
||||||
|
noteBodyRanges ?? []
|
||||||
|
)
|
||||||
|
.map(range => {
|
||||||
|
if (
|
||||||
|
range.length == null ||
|
||||||
|
range.start == null ||
|
||||||
|
range.style == null ||
|
||||||
|
!STYLE_MAPPING[range.style] ||
|
||||||
|
range.start + range.length - 1 >= body.length
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativeStart =
|
||||||
|
range.start + title.length + titleBodySeparator.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: relativeStart,
|
||||||
|
length: range.length,
|
||||||
|
style: STYLE_MAPPING[range.style],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
|
|
||||||
|
const messageBody = `${title}${titleBodySeparator}${body}`;
|
||||||
|
const bodyRanges: Array<RawBodyRange> = [
|
||||||
{ start: 0, length: title.length, style: BodyRange.Style.BOLD },
|
{ start: 0, length: title.length, style: BodyRange.Style.BOLD },
|
||||||
|
...filteredNoteBodyRanges,
|
||||||
];
|
];
|
||||||
const timestamp = Date.now() + index;
|
const timestamp = Date.now() + index;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import createDebug from 'debug';
|
import createDebug from 'debug';
|
||||||
|
|
||||||
import { expect } from 'playwright/test';
|
import { expect } from 'playwright/test';
|
||||||
|
import { assert } from 'chai';
|
||||||
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';
|
||||||
|
@ -41,7 +42,7 @@ describe('release notes', function (this: Mocha.Suite) {
|
||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows release notes with an image', async () => {
|
it('shows release notes with an image and body ranges', async () => {
|
||||||
const firstWindow = await app.getWindow();
|
const firstWindow = await app.getWindow();
|
||||||
|
|
||||||
await firstWindow.evaluate('window.SignalCI.resetReleaseNotesFetcher()');
|
await firstWindow.evaluate('window.SignalCI.resetReleaseNotesFetcher()');
|
||||||
|
@ -68,5 +69,68 @@ describe('release notes', function (this: Mocha.Suite) {
|
||||||
await expect(
|
await expect(
|
||||||
timelineMessage.locator('img.module-image__image')
|
timelineMessage.locator('img.module-image__image')
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
const boldCallBodyRange = timelineMessage
|
||||||
|
.locator('span > strong')
|
||||||
|
.getByText('Call', { exact: true });
|
||||||
|
|
||||||
|
assert.isTrue(
|
||||||
|
await boldCallBodyRange.isVisible(),
|
||||||
|
'expected message to have bold text'
|
||||||
|
);
|
||||||
|
|
||||||
|
const italicBodyRange = timelineMessage
|
||||||
|
.locator('span > em')
|
||||||
|
.getByText('links', { exact: true });
|
||||||
|
|
||||||
|
assert.isTrue(
|
||||||
|
await italicBodyRange.isVisible(),
|
||||||
|
'expected message to have italicized text'
|
||||||
|
);
|
||||||
|
|
||||||
|
const strikethroughBodyRange = timelineMessage
|
||||||
|
.locator('span > s')
|
||||||
|
.getByText('are', { exact: true });
|
||||||
|
|
||||||
|
assert.isTrue(
|
||||||
|
await strikethroughBodyRange.isVisible(),
|
||||||
|
'expected message to have strikethrough text'
|
||||||
|
);
|
||||||
|
|
||||||
|
const spoilerBodyRange = timelineMessage
|
||||||
|
.locator('.MessageTextRenderer__formatting--spoiler')
|
||||||
|
.getByText('the', { exact: true });
|
||||||
|
|
||||||
|
assert.isTrue(
|
||||||
|
(await spoilerBodyRange.count()) > 0,
|
||||||
|
'expected message to have spoiler text'
|
||||||
|
);
|
||||||
|
|
||||||
|
const monospaceBodyRange = timelineMessage
|
||||||
|
.locator('span.MessageTextRenderer__formatting--monospace')
|
||||||
|
.getByText('missing', { exact: true });
|
||||||
|
|
||||||
|
assert.isTrue(
|
||||||
|
await monospaceBodyRange.isVisible(),
|
||||||
|
'expected message to have monospace text'
|
||||||
|
);
|
||||||
|
|
||||||
|
const secondTimelineMessage = await getTimelineMessageWithText(
|
||||||
|
secondWindow,
|
||||||
|
'Bold text has invalid ranges, italic has valid'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(secondTimelineMessage).toBeVisible();
|
||||||
|
|
||||||
|
const boldCallBodyRanges = secondTimelineMessage.locator('span > strong');
|
||||||
|
|
||||||
|
// 1 for the title
|
||||||
|
assert.isTrue((await boldCallBodyRanges.count()) === 1);
|
||||||
|
|
||||||
|
const italicBodyRanges = secondTimelineMessage.locator('span > em');
|
||||||
|
|
||||||
|
assert.isTrue(
|
||||||
|
(await italicBodyRanges.count()) === 1,
|
||||||
|
'expected message to have italic text'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,12 @@
|
||||||
"desktopMinVersion": "7.30.0",
|
"desktopMinVersion": "7.30.0",
|
||||||
"androidMinVersion": "1482",
|
"androidMinVersion": "1482",
|
||||||
"uuid": "1813845b-cc98-4210-a07c-164f9b328ca8"
|
"uuid": "1813845b-cc98-4210-a07c-164f9b328ca8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ctaId": "sample_note",
|
||||||
|
"desktopMinVersion": "7.30.0",
|
||||||
|
"androidMinVersion": "1482",
|
||||||
|
"uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"megaphones": [
|
"megaphones": [
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
{
|
{
|
||||||
|
"bodyRanges": [
|
||||||
|
{ "style": "bold", "start": 0, "length": 4 },
|
||||||
|
{ "style": "italic", "start": 5, "length": 5 },
|
||||||
|
{ "style": "strikethrough", "start": 11, "length": 3 },
|
||||||
|
{ "style": "spoiler", "start": 15, "length": 3 },
|
||||||
|
{ "style": "mono", "start": 19, "length": 7 }
|
||||||
|
],
|
||||||
"mediaHeight": "864",
|
"mediaHeight": "864",
|
||||||
"mediaWidth": "1536",
|
"mediaWidth": "1536",
|
||||||
"media": "/static/release-notes/call_links.png",
|
"media": "/static/release-notes/call_links.png",
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"bodyRanges": [
|
||||||
|
{ "style": "bold", "start": 109, "length": 1 },
|
||||||
|
{ "style": "bold", "start": 0, "length": 110 },
|
||||||
|
{ "style": "bold", "start": 10, "length": 0 },
|
||||||
|
{ "style": "bold", "start": 10, "length": 100 },
|
||||||
|
{ "style": "italic", "start": 100, "length": 9 }
|
||||||
|
],
|
||||||
|
"uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||||
|
"title": "Formatting Test",
|
||||||
|
"body": "Sample body text that tests formatting boundaries. Bold text has invalid ranges, italic has valid. length 109",
|
||||||
|
"callToActionText": "Create Call Link"
|
||||||
|
}
|
|
@ -1231,9 +1231,9 @@ export const releaseNoteSchema = z.object({
|
||||||
bodyRanges: z
|
bodyRanges: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
style: z.string(),
|
style: z.string().optional(),
|
||||||
start: z.number(),
|
start: z.number().optional(),
|
||||||
length: z.number(),
|
length: z.number().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
Loading…
Add table
Reference in a new issue