Skip to content

Commit

Permalink
notification component
Browse files Browse the repository at this point in the history
  • Loading branch information
violetadev committed Sep 21, 2024
1 parent 322e450 commit bc0abd4
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 14 deletions.
7 changes: 7 additions & 0 deletions desktop-app/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export const PREVIEW_LAYOUTS = {
export type PreviewLayout =
typeof PREVIEW_LAYOUTS[keyof typeof PREVIEW_LAYOUTS];

export type Notification = {
id: string;
link?: string;
linkText?: string;
text: string;
};

export interface OpenUrlArgs {
url: string;
}
Expand Down
4 changes: 4 additions & 0 deletions desktop-app/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ const createWindow = async () => {
ipcMain.on('stop-watcher', async () => {
await stopWatchFiles();
});

ipcMain.handle('get-app-version', () => {
return app.getVersion();
});
};

app.on('open-url', async (event, url) => {
Expand Down
33 changes: 33 additions & 0 deletions desktop-app/src/renderer/components/Notifications/Notification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
IPC_MAIN_CHANNELS,
Notification as NotificationType,
} from 'common/constants';
import Button from '../Button';

const Notification = ({ notification }: { notification: NotificationType }) => {
const handleLinkClick = (url: string) => {
window.electron.ipcRenderer.sendMessage(IPC_MAIN_CHANNELS.OPEN_EXTERNAL, {
url,
});
};

return (
<div className="mb-2 text-sm text-white">
<p> {notification.text} </p>
{notification.link && notification.linkText && (
<Button
isPrimary
title={notification.linkText}
onClick={() =>
notification.link && handleLinkClick(notification.link)
}
className="mt-2"
>
{notification.linkText}
</Button>
)}
</div>
);
};

export default Notification;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const NotificationEmptyStatus = () => {
return (
<div className="mb-2 text-sm text-white">
<p>You are all caught up! No new notifications at the moment.</p>
</div>
);
};

export default NotificationEmptyStatus;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useSelector } from 'react-redux';
import { selectNotifications } from 'renderer/store/features/renderer';
import { v4 as uuidv4 } from 'uuid';
import { Notification as NotificationType } from 'common/constants';
import Notification from './Notification';
import NotificationEmptyStatus from './NotificationEmptyStatus';

const Notifications = () => {
const notificationsState = useSelector(selectNotifications);

return (
<div className="mb-4 max-h-[200px] overflow-y-auto rounded-lg p-1 px-4 shadow-lg dark:bg-slate-900">
<span className="text-lg">Notifications</span>
<div className="mt-2">
{(!notificationsState || notificationsState?.length === 0) && (
<NotificationEmptyStatus />
)}
{notificationsState?.length > 0 &&
notificationsState?.map((notification: NotificationType) => (
<Notification key={uuidv4()} notification={notification} />
))}
</div>
</div>
);
};

export default Notifications;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const NotificationsBubble = () => {
return (
<span className="absolute top-0 right-0 flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500" />
</span>
);
};

export default NotificationsBubble;
22 changes: 21 additions & 1 deletion desktop-app/src/renderer/components/Previewer/Device/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Icon } from '@iconify/react';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import Button from 'renderer/components/Button';
import useSound from 'use-sound';
import { ScreenshotArgs, ScreenshotResult } from 'main/screenshot';
Expand All @@ -8,6 +8,9 @@ import WebPage from 'main/screenshot/webpage';

import screenshotSfx from 'renderer/assets/sfx/screenshot.mp3';
import { updateWebViewHeightAndScale } from 'common/webViewUtils';
import useCheckVersion from 'renderer/components/useCheckVersion/useCheckVersion';
import { useDispatch } from 'react-redux';
import { setNotifications } from 'renderer/store/features/renderer';
import { ColorBlindnessTools } from './ColorBlindnessTools';

interface Props {
Expand All @@ -21,6 +24,13 @@ interface Props {
isIndividualLayout: boolean;
}

const newVersionText = {
id: 'new-version',
text: 'There is a new version available.',
link: 'https://responsively.app/download',
linkText: 'See More',
};

const Toolbar = ({
webview,
device,
Expand All @@ -38,6 +48,16 @@ const Toolbar = ({
useState<boolean>(false);
const [rotated, setRotated] = useState<boolean>(false);

const dispatch = useDispatch();

const { isNewVersionAvailable } = useCheckVersion();

useEffect(() => {
if (isNewVersionAvailable) {
dispatch(setNotifications(newVersionText));
}
}, [dispatch, isNewVersionAvailable]);

const refreshView = () => {
if (webview) {
webview.reload();
Expand Down
14 changes: 4 additions & 10 deletions desktop-app/src/renderer/components/ToolBar/Menu/Flyout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useDispatch } from 'react-redux';

import Button from 'renderer/components/Button';
import { APP_VIEWS, setAppView } from 'renderer/store/features/ui';
import MasonryLayout from 'renderer/components/Masonry/MasonryLayout';
import Notifications from 'renderer/components/Notifications/Notifications';
import { Divider } from 'renderer/components/Divider';
import Devtools from './Devtools';
import UITheme from './UITheme';
import Zoom from './Zoom';
Expand All @@ -12,17 +10,11 @@ import PreviewLayout from './PreviewLayout';
import Bookmark from './Bookmark';
import { Settings } from './Settings';

const Divider = () => (
<div className="h-[1px] bg-slate-200 dark:bg-slate-700" />
);

interface Props {
closeFlyout: () => void;
}

const MenuFlyout = ({ closeFlyout }: Props) => {
const dispatch = useDispatch();

return (
<div className="absolute top-[26px] right-[4px] z-50 flex w-80 flex-col gap-2 rounded bg-white p-2 pb-0 text-sm shadow-lg ring-1 ring-slate-500 !ring-opacity-40 focus:outline-none dark:bg-slate-900 dark:ring-white dark:!ring-opacity-40">
<Zoom />
Expand All @@ -38,6 +30,8 @@ const MenuFlyout = ({ closeFlyout }: Props) => {
<Bookmark />
<Settings closeFlyout={closeFlyout} />
</div>
<Divider />
<Notifications />
</div>
);
};
Expand Down
15 changes: 14 additions & 1 deletion desktop-app/src/renderer/components/ToolBar/Menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ import { useDetectClickOutside } from 'react-detect-click-outside';
import Button from 'renderer/components/Button';
import { useDispatch, useSelector } from 'react-redux';
import { closeMenuFlyout, selectMenuFlyout } from 'renderer/store/features/ui';
import { selectNotifications } from 'renderer/store/features/renderer';
import useLocalStorage from 'renderer/components/useLocalStorage/useLocalStorage';
import NotificationsBubble from 'renderer/components/Notifications/NotificationsBubble';
import MenuFlyout from './Flyout';

const Menu = () => {
const dispatch = useDispatch();
const isMenuFlyoutOpen = useSelector(selectMenuFlyout);
const notifications = useSelector(selectNotifications);

const [hasNewNotifications, setHasNewNotifications] = useLocalStorage(
'hasNewNotifications',
true
);

const ref = useDetectClickOutside({
onTriggered: () => {
Expand All @@ -20,16 +29,20 @@ const Menu = () => {

const handleFlyout = () => {
dispatch(closeMenuFlyout(!isMenuFlyoutOpen));
setHasNewNotifications(false);
};

const onClose = () => {
dispatch(closeMenuFlyout(false));
};

return (
<div className="relative flex items-center" ref={ref}>
<div className="relative mr-2 flex items-center" ref={ref}>
<Button onClick={handleFlyout} isActive={isMenuFlyoutOpen}>
<Icon icon="carbon:overflow-menu-vertical" />
{notifications?.length > 0 && Boolean(hasNewNotifications) && (
<NotificationsBubble />
)}
</Button>
<div style={{ visibility: isMenuFlyoutOpen ? 'visible' : 'hidden' }}>
<MenuFlyout closeFlyout={onClose} />
Expand Down
3 changes: 1 addition & 2 deletions desktop-app/src/renderer/components/ToolBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
setIsCapturingScreenshot,
setIsInspecting,
setRotate,
setNotifications,
} from 'renderer/store/features/renderer';
import { Icon } from '@iconify/react';
import { ScreenshotAllArgs } from 'main/screenshot';
Expand Down Expand Up @@ -103,9 +104,7 @@ const ToolBar = () => {
return (
<div className="flex items-center justify-between gap-2">
<NavigationControls />

<AddressBar />

<Button
onClick={handleRotate}
isActive={rotateDevices}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useState, useEffect } from 'react';
import { gt } from 'semver';

const fetchAssets = async () => {
try {
const response = await fetch(
'https://api.github.com/repos/responsively-org/responsively-app-releases/releases/latest'
);
const data = await response.json();
return data.tag_name;
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error getting assets', err);
return '';
}
};

const getAppVersion = async () => {
try {
const version = await window.electron.ipcRenderer.invoke('get-app-version');
return `v${version}`;
} catch (err) {
// eslint-disable-next-line no-console
console.error('Error getting app version', err);
return '';
}
};

const useCheckVersion = () => {
const [isNewVersionAvailable, setIsNewVersionAvailable] = useState<
null | boolean
>(null);
const [latestVersion, setLatestVersion] = useState('');
const [currentVersion, setCurrentVersion] = useState('');

useEffect(() => {
const checkVersion = async () => {
const currentVersionResult = await getAppVersion();
const latestVersionResult = await fetchAssets();

setCurrentVersion(currentVersionResult);
setLatestVersion(latestVersionResult);

const isNewVersion =
currentVersionResult &&
latestVersionResult &&
gt(latestVersionResult, currentVersionResult);

setIsNewVersionAvailable(isNewVersion);
};

if (isNewVersionAvailable === null) {
checkVersion();
}
}, [currentVersion, isNewVersionAvailable, latestVersion]);

return {
isNewVersionAvailable,
currentVersion,
latestVersion,
};
};

export default useCheckVersion;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState, useEffect } from 'react';

function useLocalStorage<T>(key: string, initialValue?: T) {
const [storedValue, setStoredValue] = useState<T | undefined>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : undefined;
} catch (error) {
console.error('Error reading from localStorage', error);
return undefined;
}
});

useEffect(() => {
if (storedValue === undefined && initialValue !== undefined) {
setStoredValue(initialValue);
window.localStorage.setItem(key, JSON.stringify(initialValue));
}
}, [initialValue, storedValue, key]);

const setValue = (value: T | ((val: T | undefined) => T)) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Error setting localStorage', error);
}
};

const removeValue = () => {
try {
window.localStorage.removeItem(key);
setStoredValue(undefined);
} catch (error) {
console.error('Error removing from localStorage', error);
}
};

return [storedValue, setValue, removeValue] as const;
}

export default useLocalStorage;
Loading

0 comments on commit bc0abd4

Please sign in to comment.