diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js index 9ddab746b..a70814e8f 100644 --- a/packages/bruno-app/src/components/Modal/index.js +++ b/packages/bruno-app/src/components/Modal/index.js @@ -1,4 +1,5 @@ import React, { useEffect, useState, useRef } from 'react'; +import { IconX } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; import useFocusTrap from 'hooks/useFocusTrap'; import Button from 'ui/Button'; @@ -12,13 +13,15 @@ const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => ( {handleCancel && !hideClose ? ( // TODO: Remove data-test-id and use data-testid instead across the codebase.
handleCancel() : null} data-testid="modal-close-button"> - × +
) : null} ); -const ModalContent = ({ children }) =>
{children}
; +const ModalContent = ({ children, noPadding }) => ( +
{children}
+); const ModalFooter = ({ confirmText, @@ -84,7 +87,8 @@ const Modal = ({ onClick, closeModalFadeTimeout = 500, dataTestId, - confirmButtonColor = 'primary' + confirmButtonColor = 'primary', + noPadding }) => { const modalRef = useRef(null); const [isClosing, setIsClosing] = useState(false); @@ -148,7 +152,7 @@ const Modal = ({ handleCancel={() => closeModal({ type: 'icon' })} customHeader={customHeader} /> - {children} + {children} { + let badgeColor = theme.colors.text.purple; + try { + parseToRgb(color); + badgeColor = color; + } catch { + // invalid color; keep the fallback + } + return { + backgroundColor: rgba(badgeColor, 0.15), + color: badgeColor + }; +}; + +const getSanitizedDescription = (description) => { + return DOMPurify.sanitize(description || '', { + ALLOWED_TAGS: ['a', 'ul', 'img', 'li', 'div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'br', 'strong', 'em'], + ALLOWED_ATTR: ['href', 'style', 'target', 'src', 'alt'] + }); +}; + +const NotificationDetail = ({ notification }) => { + const { theme } = useTheme(); + + // Rendered in a sandboxed iframe (no allow-scripts); theme CSS is inlined + // since the iframe doesn't inherit app styles. + const buildDescriptionDocument = (description) => { + const body = getSanitizedDescription(description); + return ` + + + + + + + ${body} +`; + }; + + if (!notification) { + return ( +
+
Select a notification to read more.
+
+ ); + } + + return ( +
+
+
+ {notification.type && ( + + {notification.type} + + )} + {humanizeDate(notification.date)} +
+
{notification.title}
+
+ -
- - ) : ( -
You are all caught up!
- )} - - - - )} - + {isOpen && } + ); }; diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index d91729405..2b527262f 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -38,6 +38,7 @@ import { isElectron } from 'utils/common/platform'; import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments'; import { collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByCredentialsId, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index'; import { addLog } from 'providers/ReduxStore/slices/logs'; +import { loadNotifications } from 'providers/ReduxStore/slices/notifications'; import { updateSystemResources } from 'providers/ReduxStore/slices/performance'; import { apiSpecAddFileEvent, apiSpecChangeFileEvent } from 'providers/ReduxStore/slices/apiSpec'; @@ -343,6 +344,10 @@ const useIpcEvents = () => { dispatch(setGitVersion(val)); }); + const removeLoadNotificationsListener = ipcRenderer.on('main:load-notifications', (notifications) => { + dispatch(loadNotifications(notifications)); + }); + return () => { removeCollectionTreeUpdateListener(); removeApiSpecTreeUpdateListener(); @@ -376,6 +381,7 @@ const useIpcEvents = () => { removePersistentEnvVariablesUpdateListener(); removeSystemResourcesListener(); gitVersionListener(); + removeLoadNotificationsListener(); }; }, [isElectron]); }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js index c03db6d6a..23e2ddc56 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js @@ -2,6 +2,8 @@ import toast from 'react-hot-toast'; import { createSlice } from '@reduxjs/toolkit'; import { getAppInstallDate } from 'utils/common/platform'; import semver from 'semver'; +import { version } from '../../../../package.json'; + const getReadNotificationIds = () => { try { let readNotificationIdsString = window.localStorage.getItem('bruno.notifications.read'); @@ -21,10 +23,27 @@ const setReadNotificationsIds = (val) => { } }; +const getClearedNotificationIds = () => { + try { + let raw = window.localStorage.getItem('bruno.notifications.cleared'); + return raw ? JSON.parse(raw) : []; + } catch (err) { + return []; + } +}; + +const setClearedNotificationIds = (val) => { + try { + window.localStorage.setItem('bruno.notifications.cleared', JSON.stringify(val)); + } catch (err) { + // ignore + } +}; + const initialState = { - loading: false, notifications: [], - readNotificationIds: getReadNotificationIds() || [] + readNotificationIds: getReadNotificationIds() || [], + clearedNotificationIds: getClearedNotificationIds() || [] }; export const filterNotificationsByVersion = (notifications, currentVersion) => { @@ -51,9 +70,6 @@ export const notificationSlice = createSlice({ name: 'notifications', initialState, reducers: { - setFetchingStatus: (state, action) => { - state.loading = action.payload.fetching; - }, setNotifications: (state, action) => { let notifications = action.payload.notifications || []; let readNotificationIds = state.readNotificationIds; @@ -99,31 +115,25 @@ export const notificationSlice = createSlice({ state.notifications.forEach((notification) => { notification.read = true; }); + }, + clearAllNotifications: (state) => { + const ids = state.notifications.map((n) => n.id); + const merged = Array.from(new Set([...(state.clearedNotificationIds || []), ...ids])); + state.clearedNotificationIds = merged; + setClearedNotificationIds(merged); } } }); -export const { setNotifications, setFetchingStatus, markNotificationAsRead, markAllNotificationsAsRead } - = notificationSlice.actions; +export const { + setNotifications, + markNotificationAsRead, + markAllNotificationsAsRead, + clearAllNotifications +} = notificationSlice.actions; -export const fetchNotifications = ({ currentVersion }) => (dispatch, getState) => { - return new Promise((resolve) => { - const { ipcRenderer } = window; - dispatch(setFetchingStatus(true)); - ipcRenderer - .invoke('renderer:fetch-notifications') - .then((notifications) => { - notifications = filterNotificationsByVersion(notifications, currentVersion); - dispatch(setNotifications({ notifications })); - dispatch(setFetchingStatus(false)); - resolve(notifications); - }) - .catch((err) => { - dispatch(setFetchingStatus(false)); - console.error(err); - resolve([]); - }); - }); +export const loadNotifications = (notifications) => (dispatch) => { + dispatch(setNotifications({ notifications: filterNotificationsByVersion(notifications, version) })); }; export default notificationSlice.reducer; diff --git a/packages/bruno-app/src/themes/dark/catppuccin-frappe.js b/packages/bruno-app/src/themes/dark/catppuccin-frappe.js index 331f67dff..b57680f55 100644 --- a/packages/bruno-app/src/themes/dark/catppuccin-frappe.js +++ b/packages/bruno-app/src/themes/dark/catppuccin-frappe.js @@ -271,15 +271,13 @@ const catppuccinFrappeTheme = { }, notifications: { - bg: colors.SURFACE0, + bg: colors.BASE, list: { - bg: colors.SURFACE0, - borderRight: colors.SURFACE2, + bg: colors.BASE, borderBottom: colors.SURFACE1, hoverBg: colors.SURFACE1, active: { - border: colors.BLUE, - bg: colors.SURFACE2, + bg: colors.SURFACE0, hoverBg: colors.SURFACE2 } } diff --git a/packages/bruno-app/src/themes/dark/catppuccin-macchiato.js b/packages/bruno-app/src/themes/dark/catppuccin-macchiato.js index 79b52808d..c5863ec20 100644 --- a/packages/bruno-app/src/themes/dark/catppuccin-macchiato.js +++ b/packages/bruno-app/src/themes/dark/catppuccin-macchiato.js @@ -271,15 +271,13 @@ const catppuccinMacchiatoTheme = { }, notifications: { - bg: colors.SURFACE0, + bg: colors.BASE, list: { - bg: colors.SURFACE0, - borderRight: colors.SURFACE2, + bg: colors.BASE, borderBottom: colors.SURFACE1, hoverBg: colors.SURFACE1, active: { - border: colors.BLUE, - bg: colors.SURFACE2, + bg: colors.SURFACE0, hoverBg: colors.SURFACE2 } } diff --git a/packages/bruno-app/src/themes/dark/catppuccin-mocha.js b/packages/bruno-app/src/themes/dark/catppuccin-mocha.js index 09136cca1..46fe43b2d 100644 --- a/packages/bruno-app/src/themes/dark/catppuccin-mocha.js +++ b/packages/bruno-app/src/themes/dark/catppuccin-mocha.js @@ -271,15 +271,13 @@ const catppuccinMochaTheme = { }, notifications: { - bg: colors.SURFACE0, + bg: colors.BASE, list: { - bg: colors.SURFACE0, - borderRight: colors.SURFACE2, + bg: colors.BASE, borderBottom: colors.SURFACE1, hoverBg: colors.SURFACE1, active: { - border: colors.BLUE, - bg: colors.SURFACE2, + bg: colors.SURFACE0, hoverBg: colors.SURFACE2 } } diff --git a/packages/bruno-app/src/themes/dark/dark-monochrome.js b/packages/bruno-app/src/themes/dark/dark-monochrome.js index b9136d29b..be292a499 100644 --- a/packages/bruno-app/src/themes/dark/dark-monochrome.js +++ b/packages/bruno-app/src/themes/dark/dark-monochrome.js @@ -258,16 +258,14 @@ const darkMonochromeTheme = { }, notifications: { - bg: colors.GRAY_3, + bg: colors.BG, list: { - bg: '3D3D3D', - borderRight: '#4f4f4f', + bg: colors.BG, borderBottom: '#545454', - hoverBg: '#434343', + hoverBg: colors.GRAY_3, active: { - border: '#a3a3a3', - bg: '#4f4f4f', - hoverBg: '#4f4f4f' + bg: colors.GRAY_2, + hoverBg: colors.GRAY_4 } } }, diff --git a/packages/bruno-app/src/themes/dark/dark-pastel.js b/packages/bruno-app/src/themes/dark/dark-pastel.js index 06790f357..29ba300bb 100644 --- a/packages/bruno-app/src/themes/dark/dark-pastel.js +++ b/packages/bruno-app/src/themes/dark/dark-pastel.js @@ -274,16 +274,14 @@ const darkPastelTheme = { }, notifications: { - bg: colors.GRAY_3, + bg: colors.BG, list: { - bg: colors.GRAY_2, - borderRight: colors.GRAY_4, + bg: colors.BG, borderBottom: colors.GRAY_4, - hoverBg: colors.GRAY_3, + hoverBg: colors.GRAY_4, active: { - border: colors.BRAND, - bg: colors.GRAY_4, - hoverBg: colors.GRAY_4 + bg: colors.GRAY_3, + hoverBg: colors.GRAY_5 } } }, diff --git a/packages/bruno-app/src/themes/dark/dark.js b/packages/bruno-app/src/themes/dark/dark.js index 480768ae2..237d7d0cc 100644 --- a/packages/bruno-app/src/themes/dark/dark.js +++ b/packages/bruno-app/src/themes/dark/dark.js @@ -303,16 +303,14 @@ const darkTheme = { }, notifications: { - bg: colors.GRAY_3, + bg: palette.background.BASE, list: { - bg: '3D3D3D', - borderRight: '#4f4f4f', - borderBottom: '#545454', - hoverBg: '#434343', + bg: palette.background.BASE, + borderBottom: palette.border.BORDER0, + hoverBg: colors.GRAY_3, active: { - border: '#569cd6', - bg: '#4f4f4f', - hoverBg: '#4f4f4f' + bg: palette.background.SURFACE0, + hoverBg: colors.GRAY_4 } } }, diff --git a/packages/bruno-app/src/themes/dark/nord.js b/packages/bruno-app/src/themes/dark/nord.js index 371a1b040..83c512141 100644 --- a/packages/bruno-app/src/themes/dark/nord.js +++ b/packages/bruno-app/src/themes/dark/nord.js @@ -273,16 +273,14 @@ const nordTheme = { }, notifications: { - bg: colors.NORD2, + bg: colors.NORD0, list: { - bg: colors.NORD1, - borderRight: colors.NORD3, + bg: colors.NORD0, borderBottom: colors.NORD3, - hoverBg: colors.NORD2, + hoverBg: colors.NORD3, active: { - border: colors.NORD8, bg: colors.NORD2, - hoverBg: colors.NORD2 + hoverBg: '#5d6b83' } } }, diff --git a/packages/bruno-app/src/themes/dark/vscode.js b/packages/bruno-app/src/themes/dark/vscode.js index accfa70c9..8cc0af4c3 100644 --- a/packages/bruno-app/src/themes/dark/vscode.js +++ b/packages/bruno-app/src/themes/dark/vscode.js @@ -276,16 +276,14 @@ const vscodeDarkTheme = { }, notifications: { - bg: colors.GRAY_3, + bg: colors.EDITOR_BG, list: { - bg: colors.GRAY_2, - borderRight: colors.BORDER, + bg: colors.EDITOR_BG, borderBottom: colors.BORDER, - hoverBg: colors.GRAY_3, + hoverBg: colors.GRAY_4, active: { - border: colors.BRAND, bg: colors.GRAY_3, - hoverBg: colors.GRAY_3 + hoverBg: colors.GRAY_5 } } }, diff --git a/packages/bruno-app/src/themes/light/catppuccin-latte.js b/packages/bruno-app/src/themes/light/catppuccin-latte.js index 681644d79..f1c3edb9b 100644 --- a/packages/bruno-app/src/themes/light/catppuccin-latte.js +++ b/packages/bruno-app/src/themes/light/catppuccin-latte.js @@ -271,14 +271,12 @@ const catppuccinLatteTheme = { notifications: { bg: colors.BASE, list: { - bg: colors.MANTLE, - borderRight: colors.SURFACE1, + bg: colors.BASE, borderBottom: colors.SURFACE1, - hoverBg: colors.SURFACE0, + hoverBg: colors.SURFACE1, active: { - border: colors.BLUE, - bg: colors.SURFACE1, - hoverBg: colors.SURFACE1 + bg: colors.SURFACE0, + hoverBg: colors.SURFACE2 } } }, diff --git a/packages/bruno-app/src/themes/light/light-monochrome.js b/packages/bruno-app/src/themes/light/light-monochrome.js index d4a495544..353bc0977 100644 --- a/packages/bruno-app/src/themes/light/light-monochrome.js +++ b/packages/bruno-app/src/themes/light/light-monochrome.js @@ -257,16 +257,14 @@ const lightMonochromeTheme = { }, notifications: { - bg: 'white', + bg: colors.BACKGROUND, list: { - bg: '#eaeaea', - borderRight: 'transparent', + bg: colors.BACKGROUND, borderBottom: '#d3d3d3', - hoverBg: '#e4e4e4', + hoverBg: colors.GRAY_4, active: { - border: '#525252', - bg: '#dcdcdc', - hoverBg: '#dcdcdc' + bg: colors.GRAY_3, + hoverBg: colors.GRAY_5 } } }, diff --git a/packages/bruno-app/src/themes/light/light-pastel.js b/packages/bruno-app/src/themes/light/light-pastel.js index 4cecf81ce..2b824d434 100644 --- a/packages/bruno-app/src/themes/light/light-pastel.js +++ b/packages/bruno-app/src/themes/light/light-pastel.js @@ -271,16 +271,14 @@ const lightPastelTheme = { }, notifications: { - bg: colors.WHITE, + bg: colors.BACKGROUND, list: { - bg: colors.GRAY_2, - borderRight: 'transparent', + bg: colors.BACKGROUND, borderBottom: colors.GRAY_4, - hoverBg: colors.GRAY_3, + hoverBg: colors.GRAY_4, active: { - border: colors.BRAND, bg: colors.GRAY_3, - hoverBg: colors.GRAY_3 + hoverBg: colors.GRAY_5 } } }, diff --git a/packages/bruno-app/src/themes/light/light.js b/packages/bruno-app/src/themes/light/light.js index 1ee38b9f8..60929cb34 100644 --- a/packages/bruno-app/src/themes/light/light.js +++ b/packages/bruno-app/src/themes/light/light.js @@ -295,13 +295,11 @@ const lightTheme = { notifications: { bg: palette.background.BASE, list: { - bg: palette.background.SURFACE0, - borderRight: 'transparent', - borderBottom: palette.border.BORDER2, + bg: palette.background.BASE, + borderBottom: palette.border.BORDER0, hoverBg: palette.background.SURFACE1, active: { - border: palette.hues.BLUE, - bg: palette.background.SURFACE1, + bg: palette.background.SURFACE0, hoverBg: palette.background.SURFACE2 } } diff --git a/packages/bruno-app/src/themes/light/vscode.js b/packages/bruno-app/src/themes/light/vscode.js index 84da83302..9c258d615 100644 --- a/packages/bruno-app/src/themes/light/vscode.js +++ b/packages/bruno-app/src/themes/light/vscode.js @@ -275,16 +275,14 @@ const vscodeLightTheme = { }, notifications: { - bg: colors.WHITE, + bg: colors.EDITOR_BG, list: { - bg: colors.GRAY_2, - borderRight: 'transparent', + bg: colors.EDITOR_BG, borderBottom: colors.BORDER, - hoverBg: colors.GRAY_3, + hoverBg: colors.GRAY_4, active: { - border: colors.BRAND, bg: colors.GRAY_3, - hoverBg: colors.GRAY_3 + hoverBg: colors.GRAY_5 } } }, diff --git a/packages/bruno-app/src/themes/schema/oss.js b/packages/bruno-app/src/themes/schema/oss.js index 10d9e27a2..42351eec4 100644 --- a/packages/bruno-app/src/themes/schema/oss.js +++ b/packages/bruno-app/src/themes/schema/oss.js @@ -394,21 +394,19 @@ export const ossSchema = { type: 'object', properties: { bg: { type: 'string' }, - borderRight: { type: 'string' }, borderBottom: { type: 'string' }, hoverBg: { type: 'string' }, active: { type: 'object', properties: { - border: { type: 'string' }, bg: { type: 'string' }, hoverBg: { type: 'string' } }, - required: ['border', 'bg', 'hoverBg'], + required: ['bg', 'hoverBg'], additionalProperties: false } }, - required: ['bg', 'borderRight', 'borderBottom', 'hoverBg', 'active'], + required: ['bg', 'borderBottom', 'hoverBg', 'active'], additionalProperties: false } }, diff --git a/packages/bruno-electron/src/ipc/notifications.js b/packages/bruno-electron/src/ipc/notifications.js index c49e87fed..5a2edd0c3 100644 --- a/packages/bruno-electron/src/ipc/notifications.js +++ b/packages/bruno-electron/src/ipc/notifications.js @@ -2,26 +2,24 @@ require('dotenv').config(); const { ipcMain } = require('electron'); const fetch = require('node-fetch'); +const fetchNotifications = async () => { + const url = process.env.BRUNO_INFO_ENDPOINT || 'https://appinfo.usebruno.com'; + const data = await fetch(url).then((res) => res.json()); + return data?.notifications || []; +}; + +const pushNotifications = async (mainWindow) => { + try { + const notifications = await fetchNotifications(); + mainWindow.webContents.send('main:load-notifications', notifications); + } catch (error) { + console.error('Error while fetching notifications!', error); + } +}; + const registerNotificationsIpc = (mainWindow, watcher) => { - ipcMain.handle('renderer:fetch-notifications', async () => { - try { - const notifications = await fetchNotifications(); - return Promise.resolve(notifications); - } catch (error) { - return Promise.reject(error); - } - }); + mainWindow.webContents.on('did-finish-load', () => pushNotifications(mainWindow)); + ipcMain.on('renderer:notifications-opened', () => pushNotifications(mainWindow)); }; module.exports = registerNotificationsIpc; - -const fetchNotifications = async () => { - try { - let url = process.env.BRUNO_INFO_ENDPOINT || 'https://appinfo.usebruno.com'; - const data = await fetch(url).then((res) => res.json()); - - return data?.notifications || []; - } catch (error) { - return Promise.reject('Error while fetching notifications!', error); - } -}; diff --git a/tests/footer/notifications/notifications.spec.js b/tests/footer/notifications/notifications.spec.js index 51da3805b..1737e1327 100644 --- a/tests/footer/notifications/notifications.spec.js +++ b/tests/footer/notifications/notifications.spec.js @@ -14,7 +14,7 @@ test.describe('Notifications Modal', () => { // Verify modal is visible and has the correct title await expect(notificationsModal).toBeVisible(); - await expect(notificationsModal.locator('.bruno-modal-header-title')).toContainText('NOTIFICATIONS'); + await expect(notificationsModal.locator('.bruno-modal-header-title')).toContainText('Notifications'); // Click the close button await modalCloseButton.click();