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

Add a notification component #1313

Merged
merged 3 commits into from
Oct 6, 2024
Merged
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
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
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,29 @@
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 && notificationsState?.length === 0)) && (
<NotificationEmptyStatus />
)}
{notificationsState &&
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;
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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 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 &&
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,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;
16 changes: 16 additions & 0 deletions desktop-app/src/renderer/store/features/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import {
IPC_MAIN_CHANNELS,
Notification,
PREVIEW_LAYOUTS,
PreviewLayout,
} from 'common/constants';
Expand All @@ -16,6 +17,7 @@ export interface RendererState {
isInspecting: boolean | undefined;
layout: PreviewLayout;
isCapturingScreenshot: boolean;
notifications: Notification[] | null;
}

const zoomSteps = [
Expand All @@ -41,6 +43,7 @@ const initialState: RendererState = {
isInspecting: undefined,
layout: window.electron.store.get('ui.previewLayout'),
isCapturingScreenshot: false,
notifications: null,
};

export const updateFileWatcher = (newURL: string) => {
Expand Down Expand Up @@ -126,6 +129,16 @@ export const rendererSlice = createSlice({
setIsCapturingScreenshot: (state, action: PayloadAction<boolean>) => {
state.isCapturingScreenshot = action.payload;
},
setNotifications: (state, action: PayloadAction<Notification>) => {
const notifications = state.notifications || [];
const index = notifications.findIndex(
(notification: Notification) => notification.id === action.payload.id
);

if (index === -1) {
state.notifications = [...notifications, action.payload];
}
},
},
});

Expand All @@ -139,6 +152,7 @@ export const {
setLayout,
setIsCapturingScreenshot,
setPageTitle,
setNotifications,
} = rendererSlice.actions;

// Use different zoom factor based on state's current layout
Expand All @@ -157,5 +171,7 @@ export const selectIsInspecting = (state: RootState) =>
export const selectLayout = (state: RootState) => state.renderer.layout;
export const selectIsCapturingScreenshot = (state: RootState) =>
state.renderer.isCapturingScreenshot;
export const selectNotifications = (state: RootState) =>
state.renderer.notifications;

export default rendererSlice.reducer;
Loading