mirror of
https://github.com/outline/outline.git
synced 2025-12-21 02:29:41 -06:00
138 lines
3.9 KiB
TypeScript
138 lines
3.9 KiB
TypeScript
import { observer } from "mobx-react";
|
|
import { MarkAsReadIcon } from "outline-icons";
|
|
import * as React from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import styled from "styled-components";
|
|
import { s, hover } from "@shared/styles";
|
|
import Notification from "~/models/Notification";
|
|
import { markNotificationsAsRead } from "~/actions/definitions/notifications";
|
|
import useStores from "~/hooks/useStores";
|
|
import NotificationMenu from "~/menus/NotificationMenu";
|
|
import Desktop from "~/utils/Desktop";
|
|
import Empty from "../Empty";
|
|
import ErrorBoundary from "../ErrorBoundary";
|
|
import Flex from "../Flex";
|
|
import NudeButton from "../NudeButton";
|
|
import PaginatedList from "../PaginatedList";
|
|
import Scrollable from "../Scrollable";
|
|
import Text from "../Text";
|
|
import Tooltip from "../Tooltip";
|
|
import NotificationListItem from "./NotificationListItem";
|
|
|
|
type Props = {
|
|
/** Callback when the notification panel wants to close. */
|
|
onRequestClose: () => void;
|
|
};
|
|
|
|
/**
|
|
* A panel containing a list of notifications and controls to manage them.
|
|
*/
|
|
function Notifications(
|
|
{ onRequestClose }: Props,
|
|
ref: React.RefObject<HTMLDivElement>
|
|
) {
|
|
const { notifications } = useStores();
|
|
const { t } = useTranslation();
|
|
const isEmpty = notifications.active.length === 0;
|
|
|
|
// Update the notification count in the dock icon, if possible.
|
|
React.useEffect(() => {
|
|
// Account for old versions of the desktop app that don't have the
|
|
// setNotificationCount method on the bridge.
|
|
if (Desktop.bridge && "setNotificationCount" in Desktop.bridge) {
|
|
void Desktop.bridge.setNotificationCount(
|
|
notifications.approximateUnreadCount
|
|
);
|
|
}
|
|
|
|
// PWA badging
|
|
if ("setAppBadge" in navigator) {
|
|
if (notifications.approximateUnreadCount) {
|
|
void navigator.setAppBadge(notifications.approximateUnreadCount);
|
|
} else {
|
|
void navigator.clearAppBadge();
|
|
}
|
|
}
|
|
}, [notifications.approximateUnreadCount]);
|
|
|
|
return (
|
|
<ErrorBoundary>
|
|
<Flex style={{ width: "100%" }} column>
|
|
<Header justify="space-between">
|
|
<Text weight="bold" as="span">
|
|
{t("Notifications")}
|
|
</Text>
|
|
<Flex gap={8}>
|
|
{notifications.approximateUnreadCount > 0 && (
|
|
<Tooltip content={t("Mark all as read")}>
|
|
<Button
|
|
action={markNotificationsAsRead}
|
|
aria-label={t("Mark all as read")}
|
|
>
|
|
<MarkAsReadIcon />
|
|
</Button>
|
|
</Tooltip>
|
|
)}
|
|
<NotificationMenu />
|
|
</Flex>
|
|
</Header>
|
|
<React.Suspense fallback={null}>
|
|
<Scrollable ref={ref} flex topShadow>
|
|
<PaginatedList<Notification>
|
|
fetch={notifications.fetchPage}
|
|
options={{ archived: false }}
|
|
items={notifications.active}
|
|
renderItem={(item) => (
|
|
<NotificationListItem
|
|
key={item.id}
|
|
notification={item}
|
|
onNavigate={onRequestClose}
|
|
/>
|
|
)}
|
|
/>
|
|
</Scrollable>
|
|
</React.Suspense>
|
|
{isEmpty && (
|
|
<EmptyNotifications>{t("You're all caught up")}.</EmptyNotifications>
|
|
)}
|
|
</Flex>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|
|
|
|
const EmptyNotifications = styled(Empty)`
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 200px;
|
|
`;
|
|
|
|
const Button = styled(NudeButton)`
|
|
color: ${s("textSecondary")};
|
|
|
|
&:${hover},
|
|
&:active {
|
|
color: ${s("text")};
|
|
background: ${s("sidebarControlHoverBackground")};
|
|
}
|
|
`;
|
|
|
|
const Header = styled(Flex)`
|
|
padding: 8px 12px 12px;
|
|
min-height: 44px;
|
|
|
|
${Button} {
|
|
opacity: 0.75;
|
|
transition: opacity 250ms ease-in-out;
|
|
}
|
|
|
|
&:${hover},
|
|
&:focus-within {
|
|
${Button} {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
`;
|
|
|
|
export default observer(React.forwardRef(Notifications));
|