Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store accounts in main store so they migrate properly #188

Merged
merged 9 commits into from
Sep 9, 2024
50 changes: 46 additions & 4 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
import { menubar } from 'menubar';
import isDev from 'electron-is-dev';
import electronDebug from 'electron-debug';
import { getToken, toggleLogging } from './lib/main-store';
import {
getToken,
toggleLogging,
getAccounts,
setAccounts,
} from './lib/main-store';
import { getIconForState } from './lib/icon-path';
import { version } from '../../package.json';
import unhandled from 'electron-unhandled';
Expand All @@ -21,15 +26,15 @@ import {
markNotficationAsRead,
} from './lib/github-interface';
import { logMessage } from './lib/logging';
import type { AccountInfo, Note } from '../shared-types';
import type { AccountInfo, FetchErrorObject, Note } from '../shared-types';

// These are provided by electron forge
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;

dotEnv.config();

const debug = debugFactory('gitnews-menubar:main');
const debug = debugFactory('gitnews-menubar');

debug('initializing version', version);

Expand Down Expand Up @@ -97,6 +102,14 @@ ipcMain.on('toggle-logging', (_event, isLogging: boolean) => {
toggleLogging(isLogging);
});

ipcMain.on('accounts:set', (_event, accounts: AccountInfo[]) => {
setAccounts(accounts);
});

ipcMain.handle('accounts:get', async () => {
return getAccounts();
});

ipcMain.on('set-icon', (_event, arg: unknown) => {
if (typeof arg !== 'string') {
logMessage('Failed to set icon: it is invalid', 'error');
Expand Down Expand Up @@ -150,7 +163,21 @@ ipcMain.handle('is-demo-mode:get', async () => {
ipcMain.handle(
'notifications-for-account:get',
async (_event, account: AccountInfo) => {
return fetchNotificationsForAccount(account);
try {
const notes = await fetchNotificationsForAccount(account);
return notes;
} catch (error) {
// Electron IPC does not preserve Error objects so we must serialize what
// data we actually want. See
// https://github.com/electron/electron/issues/24427
logMessage(
`Failure while fetching notifications for account ${account.name}(${account.serverUrl})`,
'error'
);
return {
error: encodeError(account.id, error as Error),
};
}
}
);

Expand All @@ -161,6 +188,21 @@ ipcMain.handle(
}
);

// Errors must be JS objects to go through IPC. See
// https://github.com/electron/electron/issues/26338
function encodeError(accountId: string, error: any): FetchErrorObject {
return {
name: error.name,
message: error.message,
code: error.code,
statusText: error.statusText,
status: error.status,
url: error.url,
type: error.type,
accountId,
};
}

function setIcon(type?: string) {
if (!type) {
type = lastIconState;
Expand Down
16 changes: 16 additions & 0 deletions src/main/lib/main-store.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import Store from 'electron-store';
import type { AccountInfo } from '../../shared-types';

/**
* The data here will be saved in the OS so it will be available after an
* upgrade or any other situation where the electron context (localStorage
* where the in-app settings are saved) is lost.
*/
interface StoreSchema {
accounts: AccountInfo[];
'gitnews-token': string;
'is-logging-enabled': boolean;
}

const store = new Store<StoreSchema>({
defaults: {
accounts: [],
'gitnews-token': '',
'is-logging-enabled': false,
},
Expand All @@ -23,3 +31,11 @@ export function isLoggingEnabled(): boolean {
export function toggleLogging(isEnabled: boolean): void {
store.set('is-logging-enabled', isEnabled);
}

export function getAccounts(): AccountInfo[] {
return store.get('accounts');
}

export function setAccounts(accounts: AccountInfo[]): void {
store.set('accounts', accounts);
}
3 changes: 3 additions & 0 deletions src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const bridge: MainBridge = {
ipcRenderer.invoke('notifications-for-account:get', account),
markNotificationRead: (note: Note, account: AccountInfo) =>
ipcRenderer.invoke('mark-note-as-read', note, account),
saveAccounts: (accounts: AccountInfo[]) =>
ipcRenderer.send('accounts:set', accounts),
getAccounts: () => ipcRenderer.invoke('accounts:get'),
};

contextBridge.exposeInMainWorld('electronApi', bridge);
6 changes: 3 additions & 3 deletions src/renderer/components/account-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ export default function AccountEdit({
</div>
<div>
<p>
You must generate a GitHub authentication token so this app can see
your notifications. It will need the `notifications` and `repo`
scopes.
You must generate a GitHub authentication token ("Personal access
tokens (classic)") so this app can see your notifications. It will
need the `notifications` and `repo` scopes.
</p>
<label htmlFor="add-token-form__input">GitHub Token:</label>
<input
Expand Down
13 changes: 10 additions & 3 deletions src/renderer/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import FetchingInProgress from '../components/fetching-in-progress';
import createUpdater from '../components/updater';
import FilterButton from './filter-button';
import { PANE_NOTIFICATIONS } from '../lib/constants';
import { AppPane, FilterType } from '../types';
import { AppPane, AppReduxState, FilterType } from '../types';
import { useSelector } from 'react-redux';

const UpdatingLastChecked = createUpdater(LastChecked);
const UpdatingOfflineNotice = createUpdater(OfflineNotice);
Expand Down Expand Up @@ -88,7 +89,7 @@ export default function Header({
fetchingInProgress={fetchingInProgress}
lastSuccessfulCheck={lastSuccessfulCheck}
/>
{isTokenInvalid && <InvalidTokenNotice />}
{isTokenInvalid && !fetchingInProgress && <InvalidTokenNotice />}
{!isTokenInvalid && offline && (
<UpdatingOfflineNotice
fetchNotifications={fetchNotifications}
Expand Down Expand Up @@ -125,10 +126,16 @@ function SecondaryHeader({
}

function InvalidTokenNotice() {
const accounts = useSelector((state: AppReduxState) => state.accounts);
const accountNames = accounts
.filter((account) => account.isInvalid)
.map((account) => account.name)
.join(', ');
return (
<div className="offline-notice">
<span>
The token is not working. Please double-check that it is correct!
Some of your accounts are not working: '{accountNames}'. Please
double-check your info!
</span>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/main-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default function MainPane({
<AccountEdit account={selectedAccount} showAccounts={showAccounts} />
);
}
if (currentPane === PANE_ACCOUNTS) {
if (currentPane === PANE_ACCOUNTS || isTokenInvalid) {
return (
<AccountList accounts={accounts} showAccountEdit={showAccountEdit} />
);
Expand Down
9 changes: 7 additions & 2 deletions src/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Provider } from 'react-redux';

import App from './components/app';
import AppWrapper from './components/app-wrapper';
import { initToken, setIsDemoMode } from './lib/reducer';
import { initToken, setIsDemoMode, initAccounts } from './lib/reducer';
import { store } from './lib/store';

import './styles.css';
Expand All @@ -27,9 +27,14 @@ async function runApp() {

const isDemoMode = await window.electronApi.isDemoMode();
const token = await window.electronApi.getToken();
window.electronApi.logMessage('Initializing token to saved value', 'info');
const accounts = await window.electronApi.getAccounts();
window.electronApi.logMessage(
'Initializing accounts in app to data from store',
'info'
);
store.dispatch(setIsDemoMode(isDemoMode));
store.dispatch(initToken(token));
store.dispatch(initAccounts(accounts));

ReactDOM.render(
<Provider store={store}>
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/lib/config-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export const configMiddleware: Middleware<{}, AppReduxState> =
case 'TOGGLE_LOGGING':
window.electronApi.toggleLogging(action.isLogging);
break;
case 'SET_ACCOUNTS':
window.electronApi.saveAccounts(action.accounts);
break;
}
next(action);
};
70 changes: 27 additions & 43 deletions src/renderer/lib/gitnews-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import {
addConnectionError,
setIsTokenInvalid,
} from '../lib/reducer';
import { AccountInfo, AppReduxState, Note, UnknownFetchError } from '../types';
import {
AccountInfo,
AppReduxState,
FetchErrorObject,
Note,
UnknownFetchError,
} from '../types';
import { AppDispatch } from './store';
import { createDemoNotifications } from './demo-mode';

Expand Down Expand Up @@ -54,7 +60,13 @@ export function createFetcher(): Middleware<{}, AppReduxState> {
'Accounts changed; fetching with updated accounts',
'info'
);
performFetch(store.getState(), next);
performFetch(
{
...store.getState(),
accounts: action.accounts,
},
next
);
return next(action);
}

Expand Down Expand Up @@ -92,7 +104,7 @@ export function createFetcher(): Middleware<{}, AppReduxState> {
next(changeToOffline());
return;
}
if (!state.token) {
if (state.accounts.length < 1) {
next(changeToOffline());
return;
}
Expand All @@ -112,13 +124,13 @@ export function createFetcher(): Middleware<{}, AppReduxState> {
next(fetchDone());
next(gotNotes(notes));
} catch (err) {
debug('fetching notifications threw an error', err);
debug('Fetching notifications threw an error', err);
window.electronApi.logMessage(
`Fetching notifications threw an error`,
'warn'
);
next(fetchDone());
getErrorHandler(next)(err as Error, state.token);
getErrorHandler(next)(err as FetchErrorObject);
}
}

Expand All @@ -133,6 +145,9 @@ export function createFetcher(): Middleware<{}, AppReduxState> {
let allNotes: Note[] = [];
for (const account of accounts) {
const notes = await fetchNotifications(account);
if ('error' in notes) {
throw notes.error;
}
allNotes = [...allNotes, ...notes];
}
return allNotes;
Expand All @@ -142,7 +157,9 @@ export function createFetcher(): Middleware<{}, AppReduxState> {
return fetcher;
}

async function fetchNotifications(account: AccountInfo): Promise<Note[]> {
async function fetchNotifications(
account: AccountInfo
): Promise<Note[] | { error: Error }> {
return window.electronApi.getNotificationsForAccount(account);
}

Expand All @@ -155,46 +172,13 @@ async function getDemoNotifications(): Promise<Note[]> {
}

export function getErrorHandler(dispatch: AppDispatch) {
return function handleFetchError(
err: UnknownFetchError,
token: string | undefined = undefined
) {
if (
typeof err === 'object' &&
err.code === 'GitHubTokenNotFound' &&
!token
) {
const message =
'Notifications check failed because there is no token; taking no action';
debug(message);
window.electronApi.logMessage(message, 'info');
// Do nothing. The case of having no token is handled in the App component.
return;
}

if (
typeof err === 'object' &&
err.code === 'GitHubTokenNotFound' &&
token
) {
// This should never happen, I hope!
const message =
'Notifications check failed because there is no token, even though one is set';
debug(message);
window.electronApi.logMessage(message, 'error');
const errorString =
'Error fetching notifications: ' + getErrorMessage(err);
console.error(errorString); //eslint-disable-line no-console
dispatch(addConnectionError(errorString));
return;
}

return function handleFetchError(err: UnknownFetchError) {
if (typeof err === 'object' && isTokenInvalid(err)) {
const message = 'Notifications check failed the token is invalid';
const message = `Notifications check failed because the token is invalid for '${err.accountId ?? 'unknown'}'`;
debug(message);
window.electronApi.logMessage(message, 'warn');
dispatch(changeToOffline());
dispatch(setIsTokenInvalid(true));
dispatch(setIsTokenInvalid(err.accountId ?? 'unknown', true));
return;
}

Expand Down Expand Up @@ -247,7 +231,7 @@ export function getErrorHandler(dispatch: AppDispatch) {
window.electronApi.logMessage(message, 'error');
const errorString = 'Error fetching notifications: ' + getErrorMessage(err);
console.error(errorString); //eslint-disable-line no-console
console.error(err); //eslint-disable-line no-console
console.error('Raw error:', err); //eslint-disable-line no-console
dispatch(addConnectionError(errorString));
};
}
2 changes: 1 addition & 1 deletion src/renderer/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function isTokenInvalid(error: UnknownFetchError): boolean {
return Boolean(
typeof error === 'object' &&
error.status &&
error.status.toString() === '401'
error.status.toString().startsWith('4')
);
}

Expand Down
Loading
Loading