diff --git a/src/cljs/athens/views/app_toolbar.cljs b/src/cljs/athens/views/app_toolbar.cljs index 41b4004bd3..881a865c14 100644 --- a/src/cljs/athens/views/app_toolbar.cljs +++ b/src/cljs/athens/views/app_toolbar.cljs @@ -117,7 +117,7 @@ :presenceDetails (when (electron.utils/remote-db? @selected-db) (r/as-element [toolbar-presence-el]))} (when (notifications/enabled?) - {:notificationPopover (r/as-element [notifications-popover]) + {:notificationPopover (r/as-element [:f> notifications-popover]) :isNotificationsPopoverOpen @notificationsPopoverOpen?}) (when (comments/enabled?) {:isShowComments @show-comments? diff --git a/src/cljs/athens/views/notifications/popover.cljs b/src/cljs/athens/views/notifications/popover.cljs index b0ffdd8553..072c01b15c 100644 --- a/src/cljs/athens/views/notifications/popover.cljs +++ b/src/cljs/athens/views/notifications/popover.cljs @@ -1,12 +1,13 @@ (ns athens.views.notifications.popover (:require ["/components/Empty/Empty" :refer [Empty EmptyTitle EmptyIcon EmptyMessage]] - ["/components/Icons/Icons" :refer [BellFillIcon ArrowRightIcon]] - ["/components/Inbox/Inbox" :refer [InboxItemsList]] + ["/components/Icons/Icons" :refer [BellIcon ArrowRightIcon]] + ["/components/Notifications/NotificationItem" :refer [NotificationItem]] ["/timeAgo.js" :refer [timeAgo]] - ["@chakra-ui/react" :refer [Badge Box IconButton Flex PopoverBody PopoverTrigger Popover PopoverContent PopoverCloseButton PopoverHeader Button]] + ["@chakra-ui/react" :refer [Badge Text Box Heading VStack IconButton PopoverBody PopoverTrigger Popover PopoverContent PopoverCloseButton PopoverHeader Button]] [athens.common-db :as common-db] [athens.db :as db] + [athens.parse-renderer :as parse-renderer] [athens.reactive :as reactive] [athens.router :as router] [athens.views.notifications.actions :as actions] @@ -94,6 +95,13 @@ (not (get inbox-notif "isArchived"))) +(def event-verb + {"Comments" "commented on" + "Mentions" "mentioned you in" + "Assignments" "assigned you to" + "Created" "created"}) + + (defn get-inbox-items-for-popover [db at-username] (let [inbox-uid (get-inbox-uid-for-user db at-username) @@ -118,50 +126,80 @@ [] (let [username (rf/subscribe [:username])] (fn [] - (when (notifications/enabled?) - (let [user-page-title (str "@" @username) - notification-list (get-inbox-items-for-popover @db/dsdb user-page-title) - navigate-user-page #(router/navigate-page user-page-title) - num-notifications (count notification-list)] - [:> Popover {:closeOnBlur true :size "md"} - - [:> PopoverTrigger - [:> Box {:position "relative"} - [:> IconButton {"aria-label" "Notifications" - :onDoubleClick navigate-user-page - :onClick (fn [e] - (when (.. e -shiftKey) - (rf/dispatch [:right-sidebar/open-item [:node/title user-page-title]]))) - :icon (r/as-element [:> BellFillIcon])}] - (when (> num-notifications 0) - [:> Badge {:position "absolute" - :bg "gold" - :color "goldContrast" - :right "-3px" - :bottom "-1px" - :zIndex 1} num-notifications])]] - - - [:> PopoverContent {:maxHeight "calc(100vh - 4rem)"} - [:> PopoverCloseButton] - [:> PopoverHeader [:> Button {:onClick navigate-user-page :rightIcon (r/as-element [:> ArrowRightIcon])} "Notifications"]] - [:> Flex {:p 0 - :as PopoverBody + (let [user-page-title (str "@" @username) + notification-list (get-inbox-items-for-popover @db/dsdb user-page-title) + navigate-user-page #(router/navigate-page user-page-title) + notifications-grouped-by-object (group-by #(get % "object") notification-list) + num-notifications (count notification-list)] + + [:> Popover {:closeOnBlur false + :isLazy true + :size "lg"} + [:> PopoverTrigger + [:> Box {:position "relative"} + [:> IconButton {"aria-label" "Notifications" + :onDoubleClick navigate-user-page + :onClick (fn [e] + (when (.. e -shiftKey) + (rf/dispatch [:right-sidebar/open-item [:node/title user-page-title]]))) + :icon (r/as-element [:> BellIcon])}] + (when (> num-notifications 0) + [:> Badge {:position "absolute" + :bg "gold" + :pointerEvents "none" + :color "goldContrast" + :right "-3px" + :bottom "-1px" + :zIndex 1} num-notifications])]] + + [:> PopoverContent {:maxHeight "calc(100vh - 4rem)"} + [:> PopoverCloseButton] + [:> PopoverHeader + [:> Button {:onClick navigate-user-page :rightIcon (r/as-element [:> ArrowRightIcon])} + "Notifications"]] + [:> VStack {:as PopoverBody :flexDirection "column" - :overflow "hidden"} - (if (seq notification-list) - [:> InboxItemsList - - {:onOpenItem on-click-notification-item - :onMarkAsRead #(rf/dispatch (actions/update-state-prop % "athens/notification/is-read" "true")) - :onMarkAsUnread #(rf/dispatch (actions/update-state-prop % "athens/notification/is-read" "false")) - :onArchive (fn [e uid] - (.. e stopPropagation) - (rf/dispatch (actions/update-state-prop uid "athens/notification/is-archived" "true"))) - ;; :onUnarchive #(rf/dispatch (actions/update-state-prop % "athens/notification/is-read" "false")) - :notificationsList notification-list}] + :align "stretch" + :overflowY "auto" + :overscrollBehavior "contain" + :spacing 6 + :p 2} + + (doall + (if (seq notifications-grouped-by-object) + (for [[object notifs] notifications-grouped-by-object] + + ^{:key (get object "parentUid")} + [:> VStack {:align "stretch" + :key (str (:parentUid object))} + [:> Heading {:size "xs" + :fontWeight "normal" + :noOfLines 1 + :color "foreground.secondary" + :lineHeight "base" + :px 2 + :pt 2} + [parse-renderer/parse-and-render (or (get object "name") (get object "string")) (get object "parentUid")]] + + (for [notification notifs] + + ^{:key (get notification "id")} + [:> NotificationItem + {:notification notification + :onOpenItem on-click-notification-item + :onMarkAsRead #(rf/dispatch (actions/update-state-prop % "athens/notification/is-read" "true")) + :onMarkAsUnread #(rf/dispatch (actions/update-state-prop % "athens/notification/is-read" "false")) + :onArchive #(rf/dispatch (actions/update-state-prop % "athens/notification/is-archived" "true"))} + [:> Text {:fontWeight "bold" :noOfLines 2 :fontSize "sm"} + (str (get-in notification ["subject" "username"]) " " + (get event-verb (get notification "type")) " ") + [parse-renderer/parse-and-render (or + (get object "name") + (get object "string")) + (:id notification)]] + [:> Text [parse-renderer/parse-and-render (get notification "body")]]])]) [:> Empty {:size "sm" :py 8} [:> EmptyIcon] [:> EmptyTitle "All clear"] - [:> EmptyMessage "Unread notifications will appear here."]])]]]))))) + [:> EmptyMessage "Unread notifications will appear here."]]))]]])))) diff --git a/src/js/components/App/ContextMenuContext.tsx b/src/js/components/App/ContextMenuContext.tsx index 87ba5766c2..777f7f39d4 100644 --- a/src/js/components/App/ContextMenuContext.tsx +++ b/src/js/components/App/ContextMenuContext.tsx @@ -111,19 +111,16 @@ const useContextMenuState = () => { }, [menuState]); /** - * Returns true when the menu is open for this item - * @param ref: React.MutableRefObject, - * @returns + * Returns true when the menu is open + * If a ref is passed, it will return true if the ref is open + * @param ref?: React.MutableRefObject, + * @returns boolean */ - const getIsMenuOpen = (ref: React.MutableRefObject) => { - if (!menuState.sources.filter(Boolean).length) { - return false; - } else { - return menuState.sources?.includes(ref.current) - } + const getIsMenuOpen = (ref?: React.MutableRefObject) => { + if (!ref) return menuState?.isOpen; + return menuState?.sources?.includes(ref.current); }; - return { addToContextMenu, getIsMenuOpen, diff --git a/src/js/components/Icons/Icons.tsx b/src/js/components/Icons/Icons.tsx index 8d198b4a38..a6fe38ce44 100644 --- a/src/js/components/Icons/Icons.tsx +++ b/src/js/components/Icons/Icons.tsx @@ -819,4 +819,12 @@ export const GraphChildIcon = createIcon({ ), }) -export const EditIcon = PencilIcon \ No newline at end of file +export const UnreadIcon = createIcon({ + displayName: 'UnreadIcon', + viewBox: '0 0 14 14', + path: ( + + ), +}) + +export const EditIcon = PencilIcon diff --git a/src/js/components/Inbox/Inbox.tsx b/src/js/components/Inbox/Inbox.tsx deleted file mode 100644 index a54efd53aa..0000000000 --- a/src/js/components/Inbox/Inbox.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { - Text, -} from "@chakra-ui/react"; -import { InboxViewListItem, } from "./InboxViewListItem"; -import { InboxViewListBody, } from "./InboxViewListBody"; -import * as React from "react"; -import { - ArchiveIcon, -} from "@/Icons/Icons"; - -type PAGE = { - name: string - url: string - breadcrumb: string[] -} - -type BLOCK = { - string: string - url: string - breadcrumb: string[] -} - -type PROPERTY = { - name: string - breadcrumb: string[] -} - -type OBJECT = PAGE | PROPERTY | BLOCK; - -const notificationTypes = ["Created", "Edited", "Deleted", "Comments", "Mentions", "Assignments", "Completed"] -type NOTIFICATION_TYPE = typeof notificationTypes[number]; - -type NOTIFICATION = { - id: string - notificationTime: string - type: NOTIFICATION_TYPE - subject: Person - object: OBJECT - isRead: boolean, - isArchived: boolean -} - -const messageForNotification = (notification: NOTIFICATION): React.ReactNode => { - const { type, subject, object } = notification; - const subjectName = subject.username; - const objectName = object.string || object.name; - - if (type === "Created") { - return {subjectName} created {objectName}; - } else if (type === "Edited") { - return {subjectName} edited {objectName}; - } else if (type === "Deleted") { - return {subjectName} deleted {objectName}; - } else if (type === "Comments") { - return {subjectName} commented on {objectName}; - } else if (type === "Mentions") { - return {subjectName} mentioned you in {objectName}; - } else if (type === "Assignments") { - return {subjectName} assigned you to {objectName}; - } else if (type === "Completed") { - return {subjectName} completed {objectName}; - } -} - - -export const InboxItemsList = (props) => { - const { notificationsList, onOpenItem, onMarkAsRead, onMarkAsUnread, onArchive, onUnarchive } = props; - - - const getActionsForNotification = (notification) => { - const actions = []; - if (notification.isArchived) { - actions.push({ - label: "Unarchive", - fn: () => onUnarchive(notification.id), - icon: - }); - } else { - actions.push({ - label: "Archive", - fn: (e) => onArchive(e, notification.id), - icon: - }); - } - return actions; - } - - const itemsList = notificationsList.map((i) => { - return }); - - return - {itemsList} - -} - diff --git a/src/js/components/Inbox/InboxViewListBody.tsx b/src/js/components/Inbox/InboxViewListBody.tsx deleted file mode 100644 index 48ef46fcbc..0000000000 --- a/src/js/components/Inbox/InboxViewListBody.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { - Flex, -} from "@chakra-ui/react"; -import { LayoutGroup, AnimatePresence } from "framer-motion"; - -export const InboxViewListBody = ({ children }): JSX.Element => { - return - - - {children} - - - -} - diff --git a/src/js/components/Inbox/InboxViewListItem.tsx b/src/js/components/Inbox/InboxViewListItem.tsx deleted file mode 100644 index 204e987e9d..0000000000 --- a/src/js/components/Inbox/InboxViewListItem.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { - Box, - HStack, - ButtonGroup, - VStack, - Text -} from "@chakra-ui/react"; -import { motion } from "framer-motion"; -import { mapActionsToButtons } from '../utils/mapActionsToButtons'; - -const InboxItemStatusIndicator = ({ isRead, }) => { - return -} - -const InboxViewListItemBody = ({ isRead, message, body }) => { - return - - - {message} - {body && {body}} - - -} - -export const InboxViewListItem = (props): JSX.Element => { - const { - isSelected, onOpen, onMarkAsRead, onMarkAsUnread, onMarkAsArchived, onMarkAsUnarchived, onIncSelection, onDecSelection, onSelect, onDeselect, ...notificationProps } = props; - const { id, isRead, isArchived, message, body, actions, object, notificationTime } = notificationProps; - const ref = React.useRef(); - - - - return ( - <> - - onOpen(object.parentUid, id)} - > - - - {notificationTime} - e.stopPropagation()} - size="xs" - alignSelf="flex-end" - > - {mapActionsToButtons(actions, 1)} - - - - - ); -}; diff --git a/src/js/components/Notifications/NotificationItem.tsx b/src/js/components/Notifications/NotificationItem.tsx new file mode 100644 index 0000000000..fdaf3523cb --- /dev/null +++ b/src/js/components/Notifications/NotificationItem.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { BulletIcon, ArchiveIcon, ArrowLeftOnBoxIcon, CheckboxIcon, UnreadIcon } from "@/Icons/Icons"; +import { useTheme, Button, Flex, Box, ButtonGroup, HStack, MenuGroup, MenuItem, Text, VStack } from "@chakra-ui/react"; +import { ContextMenuContext } from "@/App/ContextMenuContext"; + +export const NotificationItem = (props) => { + const { notification, children, ...otherProps } = props; + const { id, isRead, object, notificationTime } = notification; + const { onOpenItem, onMarkAsRead, onMarkAsUnread, onArchive, onUnarchive, ...boxProps } = otherProps; + const { addToContextMenu, getIsMenuOpen } = React.useContext(ContextMenuContext); + const ref = React.useRef(null); + const isMenuOpen = getIsMenuOpen(ref); + const theme = useTheme(); + const indicatorHeight = `calc(${theme.lineHeights.base} * ${theme.fontSizes.md})`; + + const ContextMenuItems = () => { + return + onOpenItem(object.parentUid, id)} icon={}>Open {object.name ? "page" : "block"} + {isRead + ? onMarkAsUnread(id)} icon={}>Mark as unread + : onMarkAsRead(id)} icon={}>Mark as read} + onArchive(id)} icon={}>Archive + + } + + return ( { if (e?.button === 0) onOpenItem(object.parentUid, id) }} + onContextMenu={(e) => { + addToContextMenu({ event: e, component: ContextMenuItems, ref }); + }} + {...boxProps} + > + + + {isRead ? (null) : ( + + )} + + + {children} + + + + {notificationTime} + e.stopPropagation()} + size="xs" + alignSelf="flex-end" + > + + + + ) +} + diff --git a/src/js/theme/theme.js b/src/js/theme/theme.js index 700f81a5ce..e0f61c40e9 100644 --- a/src/js/theme/theme.js +++ b/src/js/theme/theme.js @@ -19,6 +19,7 @@ const shadows = { focusInsetDark: 'inset 0 0 0 3px #498eda', page: '0 0.25rem 1rem #00000055', + // popover: '0 0.25rem 3rem #00000055', } const fonts = { @@ -68,10 +69,11 @@ const semanticTokens = { }, menu: { default: "0 0.25rem 1rem #00000022", - _dark: "0 0.25rem 1rem #00000022", + _dark: "0 0.25rem 1rem #00000088", }, popover: { default: "0 0.25rem 1rem #00000055", + _dark: "0 0.25rem 1rem #00000088", }, tooltip: { default: "0 0.125rem 0.5rem #00000055", @@ -548,7 +550,7 @@ const components = { overflow: 'hidden', p: 0, bg: 'background.vibrancy', - borderColor: 'separator.divider', + borderColor: 'separator.border', backdropFilter: "blur(20px)", minWidth: '0', width: 'max-content', @@ -634,6 +636,26 @@ const components = { }, [$arrowBg.variable]: "colors.background.upper", } + }, + sizes: { + sm: { + content: { + borderRadius: "sm", + width: "10rem", + }, + }, + md: { + content: { + borderRadius: "md", + width: "20rem", + }, + }, + lg: { + content: { + borderRadius: "md", + width: "30rem", + }, + }, } }, Spinner: {