Skip to content

Commit 5ffa497

Browse files
committed
Add in product changelog
1 parent b9743c9 commit 5ffa497

File tree

4 files changed

+335
-0
lines changed

4 files changed

+335
-0
lines changed

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"onCommand:gitpod.syncProvider.remove",
3737
"onCommand:gitpod.exportLogs",
3838
"onCommand:gitpod.api.autoTunnel",
39+
"onCommand:gitpod.showReleaseNotes",
3940
"onAuthenticationRequest:gitpod",
4041
"onUri"
4142
],
@@ -92,6 +93,11 @@
9293
"command": "gitpod.exportLogs",
9394
"category": "Gitpod",
9495
"title": "Export all logs"
96+
},
97+
{
98+
"command": "gitpod.showReleaseNotes",
99+
"category": "Gitpod",
100+
"title": "Show Release Notes"
95101
}
96102
]
97103
},
@@ -109,6 +115,7 @@
109115
"@types/analytics-node": "^3.1.9",
110116
"@types/crypto-js": "4.1.1",
111117
"@types/google-protobuf": "^3.7.4",
118+
"@types/js-yaml": "^4.0.5",
112119
"@types/node": "16.x",
113120
"@types/node-fetch": "^2.5.12",
114121
"@types/semver": "^7.3.10",
@@ -135,6 +142,7 @@
135142
"@gitpod/local-app-api-grpcweb": "main",
136143
"@improbable-eng/grpc-web-node-http-transport": "^0.14.0",
137144
"analytics-node": "^6.0.0",
145+
"js-yaml": "^4.1.0",
138146
"node-fetch": "2.6.7",
139147
"pkce-challenge": "^3.0.0",
140148
"semver": "^7.3.7",

src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { enableSettingsSync, updateSyncContext } from './settingsSync';
1212
import { GitpodServer } from './gitpodServer';
1313
import TelemetryReporter from './telemetryReporter';
1414
import { exportLogs } from './exportLogs';
15+
import { registerReleaseNotesView } from './releaseNotes';
1516

1617
const EXTENSION_ID = 'gitpod.gitpod-desktop';
1718
const FIRST_INSTALL_KEY = 'gitpod-desktop.firstInstall';
@@ -89,6 +90,8 @@ export async function activate(context: vscode.ExtensionContext) {
8990
await context.globalState.update(FIRST_INSTALL_KEY, true);
9091
telemetry.sendTelemetryEvent('gitpod_desktop_installation', { kind: 'install' });
9192
}
93+
94+
registerReleaseNotesView(context, logger);
9295
}
9396

9497
export async function deactivate() {

src/releaseNotes.ts

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Gitpod. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import fetch from 'node-fetch';
7+
import * as vscode from 'vscode';
8+
import { load } from 'js-yaml';
9+
import Log from './common/logger';
10+
11+
const LAST_READ_RELEASE_NOTES_ID = 'gitpod-desktop.lastReadReleaseNotesId';
12+
13+
export function registerReleaseNotesView(context: vscode.ExtensionContext, logger: Log) {
14+
15+
async function shouldShowReleaseNotes(lastReadId: string | undefined) {
16+
const releaseId = await getLastPublish();
17+
logger.info(`releaseNotes lastReadId: ${lastReadId}, latestReleaseId: ${releaseId}`);
18+
return releaseId !== lastReadId;
19+
}
20+
21+
context.subscriptions.push(
22+
vscode.commands.registerCommand('gitpod.showReleaseNotes', () => {
23+
ReleaseNotesPanel.createOrShow(context);
24+
})
25+
);
26+
27+
// sync between machines
28+
context.globalState.setKeysForSync([LAST_READ_RELEASE_NOTES_ID]);
29+
30+
const lastReadId = context.globalState.get<string>(LAST_READ_RELEASE_NOTES_ID);
31+
shouldShowReleaseNotes(lastReadId).then(shouldShow => {
32+
if (shouldShow) {
33+
ReleaseNotesPanel.createOrShow(context);
34+
}
35+
});
36+
}
37+
38+
async function getLastPublish() {
39+
const resp = await fetch(`${websiteHost}/changelog/latest`);
40+
if (!resp.ok) {
41+
throw new Error(`Getting latest releaseId failed: ${resp.statusText}`);
42+
}
43+
const { releaseId } = JSON.parse(await resp.text());
44+
return releaseId as string;
45+
}
46+
47+
const websiteHost = 'https://www.gitpod.io';
48+
49+
class ReleaseNotesPanel {
50+
public static currentPanel: ReleaseNotesPanel | undefined;
51+
public static readonly viewType = 'gitpodReleaseNotes';
52+
private readonly panel: vscode.WebviewPanel;
53+
private lastReadId: string | undefined;
54+
private _disposables: vscode.Disposable[] = [];
55+
56+
private async loadChangelog(releaseId: string) {
57+
const resp = await fetch(`${websiteHost}/changelog/raw-markdown?releaseId=${releaseId}`);
58+
if (!resp.ok) {
59+
throw new Error(`Getting raw markdown content failed: ${resp.statusText}`);
60+
}
61+
const md = await resp.text();
62+
63+
const parseInfo = (md: string) => {
64+
if (!md.startsWith('---')) {
65+
return;
66+
}
67+
const lines = md.split('\n');
68+
const end = lines.indexOf('---', 1);
69+
const content = lines.slice(1, end).join('\n');
70+
return load(content) as { title: string; date: string; image: string; alt: string; excerpt: string };
71+
};
72+
const info = parseInfo(md);
73+
74+
const content = md
75+
.replace(/---.*?---/gms, '')
76+
.replace(/<script>.*?<\/script>/gms, '')
77+
.replace(/<Badge.*?text="(.*?)".*?\/>/gim, '`$1`')
78+
.replace(/<Contributors usernames="(.*?)" \/>/gim, (_, p1) => {
79+
const users = p1
80+
.split(',')
81+
.map((e: string) => `[${e}](https://github.com/${e})`);
82+
return `Contributors: ${users.join(', ')}`;
83+
})
84+
.replace(/<p>(.*?)<\/p>/gm, '$1')
85+
.replace(/^[\n]+/m, '');
86+
if (!info) {
87+
return content;
88+
}
89+
90+
return [
91+
`# ${info.title}`,
92+
`> Published at ${releaseId}, see also https://gitpod.io/changelog`,
93+
`![${info.alt ?? 'image'}](https://www.gitpod.io/images/changelog/${info.image})`,
94+
content,
95+
].join('\n\n');
96+
}
97+
98+
public async updateHtml(releaseId?: string) {
99+
if (!releaseId) {
100+
releaseId = await getLastPublish();
101+
}
102+
const mdContent = await this.loadChangelog(releaseId);
103+
const html = await vscode.commands.executeCommand('markdown.api.render', mdContent) as string;
104+
this.panel.webview.html = `<!DOCTYPE html>
105+
<html lang="en">
106+
<head>
107+
<meta charset="UTF-8">
108+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
109+
<title>Gitpod Release Notes</title>
110+
<style>
111+
${DEFAULT_MARKDOWN_STYLES}
112+
</style>
113+
</head>
114+
<body>
115+
${html}
116+
</body>
117+
</html>`;
118+
if (!this.lastReadId || releaseId > this.lastReadId) {
119+
await this.context.globalState.update(LAST_READ_RELEASE_NOTES_ID, releaseId);
120+
this.lastReadId = releaseId;
121+
}
122+
}
123+
124+
public static createOrShow(context: vscode.ExtensionContext) {
125+
const column = vscode.window.activeTextEditor
126+
? vscode.window.activeTextEditor.viewColumn
127+
: undefined;
128+
129+
if (ReleaseNotesPanel.currentPanel) {
130+
ReleaseNotesPanel.currentPanel.panel.reveal(column);
131+
return;
132+
}
133+
134+
const panel = vscode.window.createWebviewPanel(
135+
ReleaseNotesPanel.viewType,
136+
'Gitpod Release Notes',
137+
column || vscode.ViewColumn.One,
138+
{ enableScripts: true },
139+
);
140+
141+
ReleaseNotesPanel.currentPanel = new ReleaseNotesPanel(context, panel);
142+
}
143+
144+
public static revive(context: vscode.ExtensionContext, panel: vscode.WebviewPanel) {
145+
ReleaseNotesPanel.currentPanel = new ReleaseNotesPanel(context, panel);
146+
}
147+
148+
private constructor(
149+
private readonly context: vscode.ExtensionContext,
150+
panel: vscode.WebviewPanel
151+
) {
152+
this.lastReadId = this.context.globalState.get<string>(LAST_READ_RELEASE_NOTES_ID);
153+
this.panel = panel;
154+
155+
this.updateHtml();
156+
157+
this.panel.onDidDispose(() => this.dispose(), null, this._disposables);
158+
this.panel.onDidChangeViewState(
159+
() => {
160+
if (this.panel.visible) {
161+
this.updateHtml();
162+
}
163+
},
164+
null,
165+
this._disposables
166+
);
167+
}
168+
169+
public dispose() {
170+
ReleaseNotesPanel.currentPanel = undefined;
171+
this.panel.dispose();
172+
while (this._disposables.length) {
173+
const x = this._disposables.pop();
174+
if (x) {
175+
x.dispose();
176+
}
177+
}
178+
}
179+
}
180+
181+
// Align with https://github.com/gitpod-io/openvscode-server/blob/494f7eba3615344ee634e6bec0b20a1903e5881d/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts#L14
182+
export const DEFAULT_MARKDOWN_STYLES = `
183+
body {
184+
padding: 10px 20px;
185+
line-height: 22px;
186+
max-width: 882px;
187+
margin: 0 auto;
188+
}
189+
190+
body *:last-child {
191+
margin-bottom: 0;
192+
}
193+
194+
img {
195+
max-width: 100%;
196+
max-height: 100%;
197+
}
198+
199+
a {
200+
text-decoration: none;
201+
}
202+
203+
a:hover {
204+
text-decoration: underline;
205+
}
206+
207+
a:focus,
208+
input:focus,
209+
select:focus,
210+
textarea:focus {
211+
outline: 1px solid -webkit-focus-ring-color;
212+
outline-offset: -1px;
213+
}
214+
215+
hr {
216+
border: 0;
217+
height: 2px;
218+
border-bottom: 2px solid;
219+
}
220+
221+
h1 {
222+
padding-bottom: 0.3em;
223+
line-height: 1.2;
224+
border-bottom-width: 1px;
225+
border-bottom-style: solid;
226+
}
227+
228+
h1, h2, h3 {
229+
font-weight: normal;
230+
}
231+
232+
table {
233+
border-collapse: collapse;
234+
}
235+
236+
table > thead > tr > th {
237+
text-align: left;
238+
border-bottom: 1px solid;
239+
}
240+
241+
table > thead > tr > th,
242+
table > thead > tr > td,
243+
table > tbody > tr > th,
244+
table > tbody > tr > td {
245+
padding: 5px 10px;
246+
}
247+
248+
table > tbody > tr + tr > td {
249+
border-top-width: 1px;
250+
border-top-style: solid;
251+
}
252+
253+
blockquote {
254+
margin: 0 7px 0 5px;
255+
padding: 0 16px 0 10px;
256+
border-left-width: 5px;
257+
border-left-style: solid;
258+
}
259+
260+
code {
261+
font-family: "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace;
262+
}
263+
264+
pre code {
265+
font-family: var(--vscode-editor-font-family);
266+
font-weight: var(--vscode-editor-font-weight);
267+
font-size: var(--vscode-editor-font-size);
268+
line-height: 1.5;
269+
}
270+
271+
code > div {
272+
padding: 16px;
273+
border-radius: 3px;
274+
overflow: auto;
275+
}
276+
277+
.monaco-tokenized-source {
278+
white-space: pre;
279+
}
280+
281+
/** Theming */
282+
283+
.vscode-light code > div {
284+
background-color: rgba(220, 220, 220, 0.4);
285+
}
286+
287+
.vscode-dark code > div {
288+
background-color: rgba(10, 10, 10, 0.4);
289+
}
290+
291+
.vscode-high-contrast code > div {
292+
background-color: var(--vscode-textCodeBlock-background);
293+
}
294+
295+
.vscode-high-contrast h1 {
296+
border-color: rgb(0, 0, 0);
297+
}
298+
299+
.vscode-light table > thead > tr > th {
300+
border-color: rgba(0, 0, 0, 0.69);
301+
}
302+
303+
.vscode-dark table > thead > tr > th {
304+
border-color: rgba(255, 255, 255, 0.69);
305+
}
306+
307+
.vscode-light h1,
308+
.vscode-light hr,
309+
.vscode-light table > tbody > tr + tr > td {
310+
border-color: rgba(0, 0, 0, 0.18);
311+
}
312+
313+
.vscode-dark h1,
314+
.vscode-dark hr,
315+
.vscode-dark table > tbody > tr + tr > td {
316+
border-color: rgba(255, 255, 255, 0.18);
317+
}
318+
319+
`;

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@
177177
resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.6.tgz#674a69493ef2c849b95eafe69167ea59079eb504"
178178
integrity sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==
179179

180+
"@types/js-yaml@^4.0.5":
181+
version "4.0.5"
182+
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
183+
integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==
184+
180185
"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
181186
version "7.0.11"
182187
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"

0 commit comments

Comments
 (0)