Skip to content

Commit

Permalink
Merge pull request #1313 from responsively-org/update-button
Browse files Browse the repository at this point in the history
Add a notification component
  • Loading branch information
violetadev authored Oct 6, 2024
2 parents c580ab7 + b600a86 commit 080b2e2
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 13 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
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;

0 comments on commit 080b2e2

Please sign in to comment.