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

Load notification images as raw data from main process #200

Draft
wants to merge 4 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion forge.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = {
name: '@electron-forge/plugin-webpack',
config: {
port: 8000,
devContentSecurityPolicy: `default-src 'self' *.github.com *.githubusercontent.com localhost:3000 'unsafe-eval' 'unsafe-inline'`,
devContentSecurityPolicy: `default-src 'self' *.github.com *.githubusercontent.com localhost:3000 'unsafe-eval' 'unsafe-inline' blob:`,
mainConfig: './webpack.main.config.js',
renderer: {
config: './webpack.renderer.config.js',
Expand Down
22 changes: 22 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import AutoLaunch from 'easy-auto-launch';
import dotEnv from 'dotenv';
import {
fetchNotificationsForAccount,
getRawImage,
markNotficationAsRead,
} from './lib/github-interface';
import { logMessage } from './lib/logging';
Expand Down Expand Up @@ -148,6 +149,27 @@ ipcMain.handle('token:get', async () => {
return getToken();
});

ipcMain.handle(
'image:get',
async (_event, src: string, account: AccountInfo) => {
try {
const rawImage = await getRawImage(src, account);
return rawImage;
} 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 image for account ${account.name} (${account.serverUrl}) at URL ${src}`,
'error'
);
return {
error: encodeError(account.id, error as Error),
};
}
}
);

ipcMain.handle('is-auto-launch:get', async () => {
return autoLauncher.isEnabled();
});
Expand Down
24 changes: 24 additions & 0 deletions src/main/lib/github-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ function makeProxyFetch(proxyUrl: string) {
};
}

export async function getRawImage(
imgUrl: string,
account: AccountInfo
): Promise<Uint8Array[]> {
const response = account.proxyUrl
? await makeProxyFetch(account.proxyUrl)(imgUrl, {})
: await fetch(imgUrl);
const reader = response.body?.getReader();
if (!reader) {
throw new Error(`No image body found for ${imgUrl} (${account.id})`);
}
// @todo is this the right type?
const arr: Uint8Array[] = [];
while (true) {
// @todo is there a way to make `value` have a TS type?
const { done, value } = await reader.read();
arr.push(value);
if (done) {
break;
}
}
return arr.filter(Boolean);
}

function createOctokit(account: AccountInfo) {
const options = {
auth: account.apiKey,
Expand Down
2 changes: 2 additions & 0 deletions src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const bridge: MainBridge = {
getVersion: () => ipcRenderer.invoke('version:get'),
isDemoMode: () => ipcRenderer.invoke('is-demo-mode:get'),
isAutoLaunchEnabled: () => ipcRenderer.invoke('is-auto-launch:get'),
getRawImage: (imageUrl: string, account: AccountInfo) =>
ipcRenderer.invoke('image:get', imageUrl, account),
getNotificationsForAccount: (account: AccountInfo) =>
ipcRenderer.invoke('notifications-for-account:get', account),
markNotificationRead: (note: Note, account: AccountInfo) =>
Expand Down
57 changes: 57 additions & 0 deletions src/renderer/components/fetched-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { AccountInfo } from '../../shared-types';

export function FetchedImage({
src,
account,
}: {
src: string;
account: AccountInfo;
}) {
const [imgSrc, setImgSrc] = React.useState<string | undefined>();
const isFetched = React.useRef<boolean>(false);

const fetchImage = React.useCallback(async () => {
const bytes = await window.electronApi.getRawImage(src, account);
if ('error' in bytes) {
console.error(`Error loading image ${src}`, bytes.error);
return;
}
const blob = new Blob(bytes);
const imageObjectURL = URL.createObjectURL(blob);
setImgSrc(imageObjectURL);
}, [src]);

React.useEffect(() => {
return () => {
// Make sure to reset isFetched if the component is unmounted.
isFetched.current = false;
};
}, []);

React.useEffect(() => {
if (isFetched.current) {
return;
}
isFetched.current = true;
fetchImage();
}, [fetchImage]);

if (!imgSrc) {
return <PlaceholderComponent />;
}
return <img src={imgSrc} />;
}

function PlaceholderComponent() {
return React.createElement(
'svg',
{ width: '33', height: '33' },
React.createElement('circle', {
cx: '16',
cy: '16',
r: '15',
fill: 'orange',
})
);
}
14 changes: 12 additions & 2 deletions src/renderer/components/notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import Gridicon from 'gridicons';
import debugFactory from 'debug';
import { formatDistanceToNow } from 'date-fns';
import EnsuredImage from './ensured-image';
import MuteIcon from './mute-icon';
import {
Note,
Expand All @@ -11,7 +10,11 @@ import {
MarkUnread,
MuteRepo,
UnmuteRepo,
AppReduxState,
} from '../types';
import { FetchedImage } from './fetched-image';
import { useSelector } from 'react-redux';
import EnsuredImage from './ensured-image';

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

Expand Down Expand Up @@ -47,6 +50,9 @@ export default function Notification({
const isUnread =
note.unread === true ? true : note.gitnewsMarkedUnread === true;

const accounts = useSelector((state: AppReduxState) => state.accounts);
const account = accounts.find(({ id }) => id === note.gitnewsAccountId);

const onClick = () => {
debug('clicked on notification', note);
setMuteRequested(false);
Expand Down Expand Up @@ -134,7 +140,11 @@ export default function Notification({
<div className="notification__image">
{isUnread && <span className="notification__new-dot" />}
{isMuted && <MuteIcon className="mute-icon" />}
<EnsuredImage src={avatarSrc} />
{account ? (
<FetchedImage src={avatarSrc} account={account} />
) : (
<EnsuredImage src={avatarSrc} />
)}
</div>
<div className="notification__body">
<div className="notification__repo">
Expand Down
91 changes: 51 additions & 40 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
@@ -1,55 +1,66 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' *.github.com *.githubusercontent.com localhost:3000 'unsafe-eval' 'unsafe-inline' blob:"
/>

<style>
.app-loading-text {
text-align: center;
}
.app-loading-text {
text-align: center;
}

.app-loading-spinner {
width: 40px;
height: 40px;
position: relative;
margin: 40vh auto 1.5em auto;
}
.app-loading-spinner {
width: 40px;
height: 40px;
position: relative;
margin: 40vh auto 1.5em auto;
}

.double-bounce1, .double-bounce2 {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #333;
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
.double-bounce1,
.double-bounce2 {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #333;
opacity: 0.6;
position: absolute;
top: 0;
left: 0;

-webkit-animation: sk-bounce 2.0s infinite ease-in-out;
animation: sk-bounce 2.0s infinite ease-in-out;
}
-webkit-animation: sk-bounce 2s infinite ease-in-out;
animation: sk-bounce 2s infinite ease-in-out;
}

.double-bounce2 {
-webkit-animation-delay: -1.0s;
animation-delay: -1.0s;
}
.double-bounce2 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}

@-webkit-keyframes sk-bounce {
0%, 100% { -webkit-transform: scale(0.0) }
50% { -webkit-transform: scale(1.0) }
}
@-webkit-keyframes sk-bounce {
0%,
100% {
-webkit-transform: scale(0);
}
50% {
-webkit-transform: scale(1);
}
}

@keyframes sk-bounce {
0%, 100% {
transform: scale(0.0);
-webkit-transform: scale(0.0);
} 50% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}
@keyframes sk-bounce {
0%,
100% {
transform: scale(0);
-webkit-transform: scale(0);
}
50% {
transform: scale(1);
-webkit-transform: scale(1);
}
}
</style>

</head>
<body>
<div id="app">
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ export interface MainBridge {
isAutoLaunchEnabled: () => Promise<boolean>;
saveAccounts: (accounts: AccountInfo[]) => void;
getAccounts: () => Promise<AccountInfo[]>;
getRawImage: (
imageUrl: string,
account: AccountInfo
) => Promise<Uint8Array[] | { error: Error }>;
}

export type UnknownFetchError = FetchErrorObject | string;
Expand Down
11 changes: 11 additions & 0 deletions src/shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,18 @@ export interface Note {
repositoryName: string;
type: string;
subjectUrl: string;

/**
* The image URL.
*
* For public GitHub it might be something like:
* https://avatars.githubusercontent.com/u/123123123u=ABCDEFG&v=4
*
* For GitHub Enterprise it might be something like:
* https://github.a8c.com/avatars/u/1234?
*/
commentAvatar?: string;

repositoryOwnerAvatar?: string;

gitnewsAccountId: AccountInfo['id'];
Expand Down
Loading