refactor: updater

This commit is contained in:
tiddlygit-test 2021-03-06 16:33:18 +08:00
parent 2b77b2b5a1
commit f81cb15a58
11 changed files with 129 additions and 113 deletions

View file

@ -6,20 +6,20 @@ import TextField from '@material-ui/core/TextField';
import GitHubLogin from './github-login';
import type { IAuthingUserInfo } from '@services/types';
import { useUserInfoObservable } from '@services/auth/hooks';
const GitTokenInput = styled(TextField)``;
export const setGithubToken = async (token: string | undefined): Promise<void> => await window.service.auth.set('github-token', token);
export const getGithubToken = async (): Promise<string | undefined> => await window.service.auth.get('github-token');
export function GithubTokenForm(props: {
accessTokenSetter: (token?: string) => void;
userNameSetter: (userName?: string) => void;
accessToken?: string;
children: JSX.Element | Array<JSX.Element | undefined | string>;
}): JSX.Element {
const { accessToken, children } = props;
export function GithubTokenForm(props: { children?: JSX.Element | Array<JSX.Element | undefined | string> }): JSX.Element {
const { children } = props;
const { t } = useTranslation();
const userInfo = useUserInfoObservable();
if (userInfo === undefined) {
return <div>Loading...</div>;
}
return (
<>
<GitHubLogin
@ -27,7 +27,7 @@ export function GithubTokenForm(props: {
const accessTokenToSet = response?.userInfo?.thirdPartyIdentity?.accessToken;
const authDataString = response?.userInfo?.oauth;
if (accessTokenToSet !== undefined) {
props.accessTokenSetter(accessTokenToSet);
void window.service.auth.set('github-token', accessTokenToSet);
}
// all data we need
if (accessTokenToSet !== undefined && authDataString !== undefined) {
@ -35,26 +35,29 @@ export function GithubTokenForm(props: {
const nextUserInfo = {
...response.userInfo,
...authData,
...response.userInfo.thirdPartyIdentity,
...response.userInfo?.thirdPartyIdentity,
};
delete nextUserInfo.oauth;
delete nextUserInfo.thirdPartyIdentity;
props.userNameSetter((nextUserInfo as IAuthingUserInfo).username);
void window.service.auth.set('github-userName', (nextUserInfo as IAuthingUserInfo).username);
}
}}
onLogout={() => props.accessTokenSetter()}
onLogout={() => {
void window.service.auth.set('github-token', '');
void window.service.auth.set('github-userName', '');
}}
onFailure={() => {
props.accessTokenSetter();
props.userNameSetter();
void window.service.auth.set('github-token', '');
void window.service.auth.set('github-userName', '');
}}
/>
<GitTokenInput
helperText={t('AddWorkspace.GitTokenDescription')}
fullWidth
onChange={(event) => {
props.accessTokenSetter(event.target.value);
void window.service.auth.set('github-token', event.target.value);
}}
value={accessToken ?? ''}
value={userInfo['github-token']}
/>
{children}
</>

View file

@ -108,7 +108,7 @@ export default function AddWorkspace(): JSX.Element {
<TabBar currentTab={currentTab} currentTabSetter={currentTabSetter} />
<Description isCreateMainWorkspace={isCreateMainWorkspace} isCreateMainWorkspaceSetter={isCreateMainWorkspaceSetter} />
<SyncContainer elevation={2} square>
<GithubTokenForm accessTokenSetter={accessTokenSetter} userNameSetter={userNameSetter} accessToken={accessToken}>
<GithubTokenForm>
{githubWikiUrl?.length > 0 ? (
<GithubRepoLink onClick={async () => await window.service.native.open(githubWikiUrl)} variant="subtitle2" align="center">
({githubWikiUrl})

View file

@ -1,6 +1,5 @@
/* eslint-disable consistent-return */
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { useObservable } from 'beautiful-react-hooks';
import React, { useEffect, useCallback } from 'react';
import styled from 'styled-components';
import semver from 'semver';
import fromUnixTime from 'date-fns/fromUnixTime';
@ -44,8 +43,9 @@ import { IPreferences, PreferenceSections } from '@services/preferences/interfac
import { usePreferenceSections } from './useSections';
import { usePromiseValue } from '@/helpers/use-service-value';
import { usePreferenceObservable } from '@services/preferences/hooks';
import { useSystemPreferenceObservable } from '@services/systemPreferences/hooks';
import { getOpenAtLoginString, useSystemPreferenceObservable } from '@services/systemPreferences/hooks';
import { useUserInfoObservable } from '@services/auth/hooks';
import { getUpdaterDesc, useUpdaterObservable } from '@services/updater/hooks';
const Root = styled.div`
padding: theme.spacing(2);
@ -137,44 +137,6 @@ const getThemeString = (theme: any) => {
return 'System default';
};
const getOpenAtLoginString = (openAtLogin: any) => {
if (openAtLogin === 'yes-hidden') return 'Yes, but minimized';
if (openAtLogin === 'yes') return 'Yes';
return 'No';
};
const formatBytes = (bytes: any, decimals = 2) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const index = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** index).toFixed(dm))} ${sizes[index]}`;
};
const getUpdaterDesc = (status: any, info: any) => {
if (status === 'download-progress') {
if (info !== null) {
const { transferred, total, bytesPerSecond } = info;
return `Downloading updates (${formatBytes(transferred)}/${formatBytes(total)} at ${formatBytes(bytesPerSecond)}/s)...`;
}
return 'Downloading updates...';
}
if (status === 'checking-for-update') {
return 'Checking for updates...';
}
if (status === 'update-available') {
return 'Downloading updates...';
}
if (status === 'update-downloaded') {
if (info && info.version) return `A new version (${info.version}) has been downloaded.`;
return 'A new version has been downloaded.';
}
};
export default function Preferences(): JSX.Element {
const { t } = useTranslation();
const sections = usePreferenceSections();
@ -197,7 +159,8 @@ export default function Preferences(): JSX.Element {
const preference = usePreferenceObservable();
const systemPreference = useSystemPreferenceObservable();
const userInfo = useUserInfoObservable();
if (preference === undefined || systemPreference === undefined || userInfo === undefined) {
const updaterMetaData = useUpdaterObservable();
if (preference === undefined || systemPreference === undefined || userInfo === undefined || updaterMetaData === undefined) {
return <Root>Loading...</Root>;
}
@ -241,7 +204,7 @@ export default function Preferences(): JSX.Element {
<List dense>
{Object.keys(sections).map((sectionKey, index) => {
const { Icon, text, ref, hidden } = sections[sectionKey as PreferenceSections];
if (hidden === true) return;
if (hidden === true) return <></>;
return (
<React.Fragment key={sectionKey}>
{index > 0 && <Divider />}
@ -281,7 +244,7 @@ export default function Preferences(): JSX.Element {
<ListItem>
<ListItemText primary={t('Preference.Token')} secondary={t('Preference.TokenDescription')} />
<TokenContainer>
<GithubTokenForm accessTokenSetter={accessTokenSetter} userInfoSetter={userInfoSetter} accessToken={accessToken} />
<GithubTokenForm />
</TokenContainer>
</ListItem>
<ListItem>
@ -705,7 +668,7 @@ export default function Preferences(): JSX.Element {
<ListItem
button
onClick={() => {
window.service.notification.show({
void window.service.notification.show({
title: 'Test notifications',
body: 'It is working!',
});
@ -714,7 +677,7 @@ export default function Preferences(): JSX.Element {
primary="Test notifications"
secondary={(() => {
// only show this message on macOS Catalina 10.15 & above
if (platform === 'darwin' && oSVersion && semver.gte(oSVersion, '10.15.0')) {
if (platform === 'darwin' && oSVersion !== undefined && semver.gte(oSVersion, '10.15.0')) {
return (
<>
<span>If notifications don&apos;t show up,</span>
@ -957,7 +920,13 @@ export default function Preferences(): JSX.Element {
<SectionTitle ref={sections.advanced.ref}>Advanced</SectionTitle>
<Paper elevation={0}>
<List dense disablePadding>
<ListItem button onClick={async () => await window.service.native.open(LOG_FOLDER, true)}>
<ListItem
button
onClick={() => {
if (LOG_FOLDER !== undefined) {
void window.service.native.open(LOG_FOLDER, true);
}
}}>
<ListItemText primary={t('Preference.OpenLogFolder')} secondary={t('Preference.OpenLogFolderDetail')} />
<ChevronRightIcon color="action" />
</ListItem>
@ -1037,14 +1006,13 @@ export default function Preferences(): JSX.Element {
button
onClick={async () => await window.service.updater.checkForUpdates(false)}
disabled={
updaterStatus === 'checking-for-update' ||
updaterStatus === 'download-progress' ||
updaterStatus === 'download-progress' ||
updaterStatus === 'update-available'
updaterMetaData.status === 'checking-for-update' ||
updaterMetaData.status === 'download-progress' ||
updaterMetaData.status === 'update-available'
}>
<ListItemText
primary={updaterStatus === 'update-downloaded' ? 'Restart to Apply Updates' : 'Check for Updates'}
secondary={getUpdaterDesc(updaterStatus, updaterInfo)}
primary={updaterMetaData.status === 'update-downloaded' ? 'Restart to Apply Updates' : 'Check for Updates'}
secondary={getUpdaterDesc(updaterMetaData.status, updaterMetaData.info)}
/>
<ChevronRightIcon color="action" />
</ListItem>

View file

@ -6,6 +6,7 @@ import { Subject } from 'rxjs';
export interface IUserInfos {
userName: string;
'github-token'?: string;
'github-userName'?: string;
}
/**

View file

@ -1,6 +1,6 @@
import { useObservable } from "beautiful-react-hooks";
import { useState } from "react";
import { IPreferences } from "./interface";
import { useObservable } from 'beautiful-react-hooks';
import { useState } from 'react';
import { IPreferences } from './interface';
export function usePreferenceObservable(): IPreferences | undefined {
const [preference, preferenceSetter] = useState<IPreferences | undefined>();

View file

@ -8,7 +8,6 @@ import serviceIdentifier from '@services/serviceIdentifier';
import type { IWindowService } from '@services/windows/interface';
import type { INotificationService } from '@services/notifications/interface';
import { WindowNames } from '@services/windows/WindowProperties';
import { PreferenceChannel } from '@/constants/channels';
import { container } from '@services/container';
import i18n from '@services/libs/i18n';
import { IPreferences, IPreferenceService } from './interface';
@ -83,7 +82,7 @@ export class Preference implements IPreferenceService {
return preferenceToSanitize;
}
public async set<K extends keyof IPreferences>(key: K, value: IPreferences[K]): Promise<void> {
public set<K extends keyof IPreferences>(key: K, value: IPreferences[K]): Promise<void> {
this.cachedPreferences[key] = value;
this.cachedPreferences = { ...this.cachedPreferences, ...this.sanitizePreference(this.cachedPreferences) };
@ -119,7 +118,6 @@ export class Preference implements IPreferenceService {
this.updatePreferenceSubject();
}
public getPreferences = (): IPreferences => {
// store in memory to boost performance
if (this.cachedPreferences === undefined) {

View file

@ -7,3 +7,9 @@ export function useSystemPreferenceObservable(): IUsedElectionSettings | undefin
useObservable<IUsedElectionSettings | undefined>(window.service.systemPreference.systemPreference$, systemPreferenceSetter);
return systemPreference;
}
export function getOpenAtLoginString(openAtLogin: IUsedElectionSettings['openAtLogin']): string {
if (openAtLogin === 'yes-hidden') return 'Yes, but minimized';
if (openAtLogin === 'yes') return 'Yes';
return 'No';
}

View file

@ -19,7 +19,7 @@ export interface ISystemPreferenceService {
export const SystemPreferenceServiceIPCDescriptor = {
channel: SystemPreferenceChannel.name,
properties: {
systemPreference$: ProxyPropertyType.Value$;
systemPreference$: ProxyPropertyType.Value$,
set: ProxyPropertyType.Function,
getPreferences: ProxyPropertyType.Function,
get: ProxyPropertyType.Function,

View file

@ -0,0 +1,35 @@
import { useState } from 'react';
import { useObservable } from 'beautiful-react-hooks';
import formatBytes from '@services/libs/format-bytes';
import { IUpdaterMetaData } from './interface';
export function useUpdaterObservable(): IUpdaterMetaData | undefined {
const [updaterMetaData, updaterMetaDataSetter] = useState<IUpdaterMetaData | undefined>();
useObservable<IUpdaterMetaData | undefined>(window.service.updater.updaterMetaData$, updaterMetaDataSetter);
return updaterMetaData;
}
export function getUpdaterDesc(status: IUpdaterMetaData['status'], info: IUpdaterMetaData['info']): string {
if (info instanceof Error) {
return info.message;
}
if (status === 'download-progress') {
if (info !== undefined && 'transferred' in info) {
const { transferred, total, bytesPerSecond } = info;
return `Downloading updates (${formatBytes(transferred)}/${formatBytes(total)} at ${formatBytes(bytesPerSecond)}/s)...`;
}
return 'Downloading updates...';
}
if (status === 'checking-for-update') {
return 'Checking for updates...';
}
if (status === 'update-available') {
return 'Downloading updates...';
}
if (status === 'update-downloaded') {
if (info !== undefined && 'version' in info) return `A new version (${info.version}) has been downloaded.`;
return 'A new version has been downloaded.';
}
return '';
}

View file

@ -8,10 +8,11 @@ import serviceIdentifier from '@services/serviceIdentifier';
import type { IWindowService } from '@services/windows/interface';
import { WindowNames } from '@services/windows/WindowProperties';
import { lazyInject } from '@services/container';
import { MainChannel, UpdaterChannel } from '@/constants/channels';
import { MainChannel } from '@/constants/channels';
import { IUpdaterService, IUpdaterMetaData } from './interface';
import { IMenuService } from '@services/menu/interface';
import { logger } from '@services/libs/log';
import { Subject } from 'rxjs';
// TODO: use electron-forge 's auto update solution maybe see https://headspring.com/2020/09/24/building-signing-and-publishing-electron-forge-applications-for-windows/
@injectable()
@ -21,10 +22,26 @@ export class Updater implements IUpdaterService {
private updateSilent = true;
private updaterMetaData = {} as IUpdaterMetaData;
public updaterMetaData$: Subject<IUpdaterMetaData>;
public constructor() {
this.updateSilent = true;
this.configAutoUpdater();
this.updaterMetaData$ = new Subject<IUpdaterMetaData>();
this.updateUpdaterSubject();
}
private updateUpdaterSubject(): void {
this.updaterMetaData$.next(this.updaterMetaData);
}
private setMetaData(newUpdaterMetaData: IUpdaterMetaData): void {
this.updaterMetaData = {
...this.updaterMetaData,
...newUpdaterMetaData,
};
this.updateUpdaterSubject();
this.menuService.buildMenu();
}
public async checkForUpdates(isSilent: boolean): Promise<void> {
@ -75,11 +92,9 @@ export class Updater implements IUpdaterService {
private configAutoUpdater(): void {
autoUpdater.on('checking-for-update', () => {
this.updaterMetaData = {
this.setMetaData({
status: 'checking-for-update',
};
this.windowService.sendToAllWindows(UpdaterChannel.updateUpdater, this.updaterMetaData);
this.menuService.buildMenu();
});
});
autoUpdater.on('update-available', (info: UpdateInfo) => {
const mainWindow = this.windowService.get(WindowNames.main);
@ -93,12 +108,10 @@ export class Updater implements IUpdaterService {
});
this.updateSilent = true;
}
this.updaterMetaData = {
this.setMetaData({
status: 'update-available',
info,
};
this.windowService.sendToAllWindows(UpdaterChannel.updateUpdater, this.updaterMetaData);
this.menuService.buildMenu();
});
});
autoUpdater.on('update-not-available', (info: UpdateInfo) => {
const mainWindow = this.windowService.get(WindowNames.main);
@ -114,12 +127,10 @@ export class Updater implements IUpdaterService {
.catch(console.log);
this.updateSilent = true;
}
this.updaterMetaData = {
this.setMetaData({
status: 'update-not-available',
info,
};
this.windowService.sendToAllWindows(UpdaterChannel.updateUpdater, this.updaterMetaData);
this.menuService.buildMenu();
});
});
autoUpdater.on('error', (error: Error) => {
const mainWindow = this.windowService.get(WindowNames.main);
@ -136,46 +147,37 @@ export class Updater implements IUpdaterService {
this.updateSilent = true;
}
logger.error(error);
this.updaterMetaData = {
this.setMetaData({
status: 'error',
info: error,
};
this.windowService.sendToAllWindows(UpdaterChannel.updateUpdater, this.updaterMetaData);
this.menuService.buildMenu();
});
});
autoUpdater.on('update-cancelled', () => {
this.updaterMetaData = {
this.setMetaData({
status: 'update-cancelled',
};
this.windowService.sendToAllWindows(UpdaterChannel.updateUpdater, this.updaterMetaData);
this.menuService.buildMenu();
});
});
autoUpdater.on('download-progress', (progressObject: ProgressInfo) => {
this.updaterMetaData = {
this.setMetaData({
status: 'download-progress',
info: progressObject,
};
this.windowService.sendToAllWindows(UpdaterChannel.updateUpdater, this.updaterMetaData);
this.menuService.buildMenu();
});
});
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
const mainWindow = this.windowService.get(WindowNames.main);
if (mainWindow !== undefined) {
this.updaterMetaData = {
this.setMetaData({
status: 'update-downloaded',
info,
};
this.windowService.sendToAllWindows(UpdaterChannel.updateUpdater, this.updaterMetaData);
this.menuService.buildMenu();
const dialogOptions = {
});
dialog
.showMessageBox(mainWindow, {
type: 'info',
buttons: ['Restart', 'Later'],
title: 'Application Update',
message: `A new version (${info.version}) has been downloaded. Restart the application to apply the updates.`,
cancelId: 1,
};
dialog
.showMessageBox(mainWindow, dialogOptions)
})
.then(({ response }) => {
if (response === 0) {
// Fix autoUpdater.quitAndInstall() does not quit immediately

View file

@ -2,17 +2,20 @@ import type { UpdateInfo } from 'electron-updater';
import type { ProgressInfo } from 'builder-util-runtime';
import { ProxyPropertyType } from '@/helpers/electron-ipc-proxy/common';
import { UpdaterChannel } from '@/constants/channels';
import { Subject } from 'rxjs';
export interface IUpdaterMetaData {
status?: 'update-not-available' | 'checking-for-update' | 'update-available' | 'error' | 'update-cancelled' | 'download-progress' | 'update-downloaded';
info?: UpdateInfo | Error | ProgressInfo;
}
export interface IUpdaterService {
updaterMetaData$: Subject<IUpdaterMetaData>;
checkForUpdates(isSilent: boolean): Promise<void>;
}
export const UpdaterServiceIPCDescriptor = {
channel: UpdaterChannel.name,
properties: {
updaterMetaData$: ProxyPropertyType.Value$,
checkForUpdates: ProxyPropertyType.Function,
},
};