Skip to content

Commit bceadc1

Browse files
feat(preferences): add about page and app referral links
1 parent 7a54f17 commit bceadc1

17 files changed

Lines changed: 387 additions & 25 deletions

File tree

‎src/main/i18n/locales/en_US/preferences.json‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,5 +375,24 @@
375375
"version": {
376376
"label": "Current version"
377377
}
378+
},
379+
"about": {
380+
"label": "About",
381+
"copyVersion": "Copy version",
382+
"checkUpdates": "Check for updates",
383+
"checkingUpdates": "Checking…",
384+
"website": "Website",
385+
"sourceCode": "Source code",
386+
"reportIssue": "Report an issue",
387+
"license": "License",
388+
"support": "Support the project",
389+
"checkUpdatesFailed": "Could not check for updates. Please restart massCode and try again.",
390+
"runtime": {
391+
"electron": "Electron",
392+
"chrome": "Chrome",
393+
"node": "Node.js",
394+
"v8": "V8",
395+
"os": "OS"
396+
}
378397
}
379398
}

‎src/main/i18n/locales/ru_RU/preferences.json‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,5 +342,24 @@
342342
"version": {
343343
"label": "Текущая версия"
344344
}
345+
},
346+
"about": {
347+
"label": "О приложении",
348+
"copyVersion": "Скопировать версию",
349+
"checkUpdates": "Проверить обновления",
350+
"checkingUpdates": "Проверка…",
351+
"website": "Сайт",
352+
"sourceCode": "Исходный код",
353+
"reportIssue": "Сообщить об ошибке",
354+
"license": "Лицензия",
355+
"support": "Поддержать проект",
356+
"checkUpdatesFailed": "Не удалось проверить обновления. Перезапустите massCode и попробуйте ещё раз.",
357+
"runtime": {
358+
"electron": "Electron",
359+
"chrome": "Chrome",
360+
"node": "Node.js",
361+
"v8": "V8",
362+
"os": "ОС"
363+
}
345364
}
346365
}

‎src/main/ipc/handlers/__tests__/system.test.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os from 'node:os'
2+
import process from 'node:process'
13
import { beforeEach, describe, expect, it, vi } from 'vitest'
24

35
const registeredHandlers = new Map<string, (...args: any[]) => unknown>()
@@ -146,6 +148,19 @@ beforeEach(() => {
146148
})
147149

148150
describe('registerSystemHandlers', () => {
151+
it('returns versions and OS from the main process', async () => {
152+
const { registerSystemHandlers } = await import('../system')
153+
registerSystemHandlers()
154+
155+
expect(registeredHandlers.get('system:runtime-info')!()).toEqual({
156+
electron: process.versions.electron,
157+
chrome: process.versions.chrome,
158+
node: process.versions.node,
159+
v8: process.versions.v8,
160+
os: `${os.type()} ${os.arch()} ${os.release()}`,
161+
})
162+
})
163+
149164
it.each(['hello', '', 'Привет\nworld'])(
150165
'writes text through the native clipboard: %j',
151166
async (value) => {

‎src/main/ipc/handlers/system.ts‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import type { RuntimeInfo } from '../../types/ipc'
2+
import os from 'node:os'
13
import path from 'node:path'
2-
import { app, clipboard, ipcMain, shell } from 'electron'
4+
import process from 'node:process'
5+
import { app, BrowserWindow, clipboard, ipcMain, shell } from 'electron'
36
import {
47
generateIntegrationToken,
58
revokeIntegrationToken,
@@ -41,7 +44,10 @@ import {
4144
} from '../../storage/providers/markdown/watcher'
4245
import { store } from '../../store'
4346
import { runTasksCleanupWithUndo, undoTasksCleanup } from '../../tasks'
44-
import { installDownloadedUpdate } from '../../updates'
47+
import {
48+
checkForUpdatesFromMenu,
49+
installDownloadedUpdate,
50+
} from '../../updates'
4551
import { log } from '../../utils'
4652

4753
function setVaultPathAndRestartWatcher(vaultPath: string): void {
@@ -132,6 +138,20 @@ function moveVaultAndRestartWatcher(
132138
}
133139

134140
export function registerSystemHandlers() {
141+
ipcMain.handle(
142+
'system:runtime-info',
143+
(): RuntimeInfo => ({
144+
electron: process.versions.electron,
145+
chrome: process.versions.chrome,
146+
node: process.versions.node,
147+
v8: process.versions.v8,
148+
os: `${os.type()} ${os.arch()} ${os.release()}`,
149+
}),
150+
)
151+
152+
ipcMain.handle('system:check-for-updates', event =>
153+
checkForUpdatesFromMenu(BrowserWindow.fromWebContents(event.sender)))
154+
135155
ipcMain.handle('system:clipboard-write-text', (_, value: string) => {
136156
if (typeof value !== 'string')
137157
throw new Error(i18n.t('messages:error.copyFailed'))

‎src/main/menu/main.ts‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,19 @@ const helpMenuItems: MenuConfig[] = [
136136
{
137137
label: i18n.t('menu:help.website'),
138138
click: () => {
139-
shell.openExternal('https://masscode.io')
139+
shell.openExternal('https://masscode.io?ref=masscode-app')
140140
},
141141
},
142142
{
143143
label: i18n.t('menu:help.documentation'),
144144
click: () => {
145-
shell.openExternal('https://masscode.io/documentation')
145+
shell.openExternal('https://masscode.io/documentation?ref=masscode-app')
146146
},
147147
},
148148
{
149149
label: i18n.t('menu:help.twitter'),
150150
click: () => {
151-
shell.openExternal('https://twitter.com/anton_reshetov')
151+
shell.openExternal('https://twitter.com/anton_reshetov?ref=masscode-app')
152152
},
153153
},
154154
{
@@ -157,27 +157,33 @@ const helpMenuItems: MenuConfig[] = [
157157
{
158158
label: i18n.t('menu:help.viewInGitHub'),
159159
click: () => {
160-
shell.openExternal('https://github.com/massCodeIO/massCode')
160+
shell.openExternal(
161+
'https://github.com/massCodeIO/massCode?ref=masscode-app',
162+
)
161163
},
162164
},
163165
{
164166
label: i18n.t('menu:help.changeLog'),
165167
click: () => {
166-
shell.openExternal('https://github.com/massCodeIO/massCode/releases')
168+
shell.openExternal(
169+
'https://github.com/massCodeIO/massCode/releases?ref=masscode-app',
170+
)
167171
},
168172
},
169173
{
170174
label: i18n.t('menu:help.reportIssue'),
171175
click: () => {
172176
shell.openExternal(
173-
'https://github.com/massCodeIO/massCode/issues/new/choose',
177+
'https://github.com/massCodeIO/massCode/issues/new/choose?ref=masscode-app',
174178
)
175179
},
176180
},
177181
{
178182
label: i18n.t('menu:help.giveStar'),
179183
click: () => {
180-
shell.openExternal('https://github.com/massCodeIO/massCode/stargazers')
184+
shell.openExternal(
185+
'https://github.com/massCodeIO/massCode/stargazers?ref=masscode-app',
186+
)
181187
},
182188
},
183189
{
@@ -187,7 +193,7 @@ const helpMenuItems: MenuConfig[] = [
187193
label: i18n.t('menu:help.extension.vscode'),
188194
click: () => {
189195
shell.openExternal(
190-
'https://marketplace.visualstudio.com/items?itemName=AntonReshetov.masscode-assistant',
196+
'https://marketplace.visualstudio.com/items?itemName=AntonReshetov.masscode-assistant&ref=masscode-app',
191197
)
192198
},
193199
},
@@ -197,19 +203,25 @@ const helpMenuItems: MenuConfig[] = [
197203
{
198204
label: i18n.t('menu:help.donate.openCollective'),
199205
click: () => {
200-
shell.openExternal('https://opencollective.com/masscode')
206+
shell.openExternal(
207+
'https://opencollective.com/masscode?ref=masscode-app',
208+
)
201209
},
202210
},
203211
{
204212
label: i18n.t('menu:help.donate.gumroad'),
205213
click: () => {
206-
shell.openExternal('https://antonreshetov.gumroad.com/l/masscode')
214+
shell.openExternal(
215+
'https://antonreshetov.gumroad.com/l/masscode?ref=masscode-app',
216+
)
207217
},
208218
},
209219
{
210220
label: i18n.t('menu:help.donate.payPal'),
211221
click: () => {
212-
shell.openExternal('https://www.paypal.com/paypalme/antongithub')
222+
shell.openExternal(
223+
'https://www.paypal.com/paypalme/antongithub?ref=masscode-app',
224+
)
213225
},
214226
},
215227
{

‎src/main/types/ipc.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ type SystemAction =
6767
| 'show-snippet-in-file-manager'
6868
| 'deep-link'
6969
| 'install-update'
70+
| 'check-for-updates'
71+
| 'runtime-info'
7072
| 'update-available'
7173
| 'update-downloaded'
7274
| 'confirm-leave'
@@ -312,3 +314,11 @@ export interface TaskCleanupUndoResult {
312314
restored: number
313315
conflicts: string[]
314316
}
317+
318+
export interface RuntimeInfo {
319+
electron: string
320+
chrome: string
321+
node: string
322+
v8: string
323+
os: string
324+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
const { parent, app, showMessageBoxSync, checkForUpdates, getFocusedWindow }
4+
= vi.hoisted(() => {
5+
const parent = { isDestroyed: () => false }
6+
return {
7+
parent,
8+
app: { isPackaged: false },
9+
showMessageBoxSync: vi.fn(),
10+
checkForUpdates: vi.fn(),
11+
getFocusedWindow: vi.fn(() => parent),
12+
}
13+
})
14+
15+
vi.mock('electron', () => ({
16+
app,
17+
BrowserWindow: { getFocusedWindow },
18+
dialog: { showMessageBoxSync },
19+
shell: { openExternal: vi.fn() },
20+
}))
21+
vi.mock('electron-updater', () => ({ autoUpdater: { checkForUpdates } }))
22+
vi.mock('../../i18n', () => ({ default: { t: (key: string) => key } }))
23+
vi.mock('../../ipc', () => ({ send: vi.fn() }))
24+
vi.mock('../../lifecycle', () => ({ requestLifecycleAction: vi.fn() }))
25+
vi.mock('../../store', () => ({ store: { preferences: { get: () => true } } }))
26+
vi.mock('../../utils', () => ({ log: vi.fn() }))
27+
28+
const { checkForUpdatesFromMenu } = await import('../index')
29+
30+
describe('manual update feedback', () => {
31+
beforeEach(() => {
32+
vi.clearAllMocks()
33+
app.isPackaged = false
34+
})
35+
36+
it('shows the same no-update dialog attached to the focused window in development', async () => {
37+
await checkForUpdatesFromMenu()
38+
expect(showMessageBoxSync).toHaveBeenCalledWith(parent, {
39+
message: 'messages:update.noAvailable',
40+
})
41+
})
42+
43+
it('attaches the packaged check result to the requesting window', async () => {
44+
app.isPackaged = true
45+
checkForUpdates.mockResolvedValue({ isUpdateAvailable: false })
46+
await checkForUpdatesFromMenu(parent as never)
47+
expect(showMessageBoxSync).toHaveBeenCalledWith(parent, {
48+
message: 'messages:update.noAvailable',
49+
})
50+
})
51+
52+
it('still shows feedback when the requesting window has closed', async () => {
53+
await checkForUpdatesFromMenu({ isDestroyed: () => true } as never)
54+
expect(showMessageBoxSync).toHaveBeenCalledWith({
55+
message: 'messages:update.noAvailable',
56+
})
57+
})
58+
})

‎src/main/updates/index.ts‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { app, dialog, shell } from 'electron'
1+
import type { MessageBoxSyncOptions } from 'electron'
2+
import { app, BrowserWindow, dialog, shell } from 'electron'
23
import { autoUpdater } from 'electron-updater'
34
import { repository, version } from '../../../package.json'
45
import i18n from '../i18n'
@@ -44,9 +45,17 @@ export function installDownloadedUpdate() {
4445

4546
// Ручная проверка из меню. Скачивание при необходимости запустит глобальный
4647
// обработчик 'update-available', здесь только диалоги.
47-
export async function checkForUpdatesFromMenu() {
48+
export async function checkForUpdatesFromMenu(
49+
parent = BrowserWindow.getFocusedWindow(),
50+
) {
51+
function showDialog(options: MessageBoxSyncOptions) {
52+
return parent && !parent.isDestroyed()
53+
? dialog.showMessageBoxSync(parent, options)
54+
: dialog.showMessageBoxSync(options)
55+
}
56+
4857
function showNoUpdatesDialog() {
49-
dialog.showMessageBoxSync({
58+
showDialog({
5059
message: i18n.t('messages:update.noAvailable'),
5160
})
5261
}
@@ -69,15 +78,15 @@ export async function checkForUpdatesFromMenu() {
6978
const isSameMajor = getMajorVersion(latestVersion) === currentMajorVersion
7079

7180
if (isAutoUpdateEnabled && isSameMajor) {
72-
dialog.showMessageBoxSync({
81+
showDialog({
7382
message: i18n.t('messages:update.downloading', {
7483
version: latestVersion,
7584
}),
7685
})
7786
return
7887
}
7988

80-
const buttonId = dialog.showMessageBoxSync({
89+
const buttonId = showDialog({
8190
message: i18n.t('messages:update.available', {
8291
newVersion: latestVersion,
8392
oldVersion: version,
@@ -88,7 +97,7 @@ export async function checkForUpdatesFromMenu() {
8897
})
8998

9099
if (buttonId === 0) {
91-
void shell.openExternal(`${repository}/releases`)
100+
void shell.openExternal(`${repository}/releases?ref=masscode-app`)
92101
}
93102
}
94103
catch (error) {

‎src/renderer/App.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function showWhatsNewOnce() {
9898
onClick: () => {
9999
ipc.invoke(
100100
'system:open-external',
101-
`${repository}/releases/tag/v${version}`,
101+
`${repository}/releases/tag/v${version}?ref=masscode-app`,
102102
)
103103
},
104104
},

‎src/renderer/components/math-notebook/ResultsPanel.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const props = defineProps<Props>()
2121
const showTotal = ref(true)
2222
const copy = useCopyToClipboard()
2323
const MATH_NOTEBOOK_DOCUMENTATION_URL
24-
= 'https://masscode.io/documentation/math-notebook.html'
24+
= 'https://masscode.io/documentation/math-notebook.html?ref=masscode-app'
2525
2626
const total = computed(() => {
2727
return sumNumericResults(props.results)

0 commit comments

Comments
 (0)