feat: Badge documents in sidebar that have been newly shared with you

This commit is contained in:
Tom Moor
2024-02-02 23:09:18 -05:00
parent 1bf0788de6
commit abaa56c8f1
7 changed files with 63 additions and 14 deletions

View File

@@ -14,6 +14,7 @@ import { AvatarSize } from "../Avatar/Avatar";
import Flex from "../Flex"; import Flex from "../Flex";
import Text from "../Text"; import Text from "../Text";
import Time from "../Time"; import Time from "../Time";
import { UnreadBadge } from "../UnreadBadge";
type Props = { type Props = {
notification: Notification; notification: Notification;
@@ -65,7 +66,7 @@ function NotificationListItem({ notification, onNavigate }: Props) {
/> />
)} )}
</Flex> </Flex>
{notification.viewedAt ? null : <Unread />} {notification.viewedAt ? null : <UnreadBadge style={{ right: 20 }} />}
</Container> </Container>
</StyledLink> </StyledLink>
); );
@@ -100,14 +101,4 @@ const Container = styled(Flex)<{ $unread: boolean }>`
} }
`; `;
const Unread = styled.div`
width: 8px;
height: 8px;
background: ${s("accent")};
border-radius: 8px;
align-self: center;
position: absolute;
right: 20px;
`;
export default observer(NotificationListItem); export default observer(NotificationListItem);

View File

@@ -2,6 +2,7 @@ import fractionalIndex from "fractional-index";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { NotificationEventType } from "@shared/types";
import UserMembership from "~/models/UserMembership"; import UserMembership from "~/models/UserMembership";
import Fade from "~/components/Fade"; import Fade from "~/components/Fade";
import useBoolean from "~/hooks/useBoolean"; import useBoolean from "~/hooks/useBoolean";
@@ -99,7 +100,7 @@ function SharedWithMeLink({ userMembership }: Props) {
<SidebarLink <SidebarLink
depth={0} depth={0}
to={{ to={{
pathname: document.url, pathname: document.path,
state: { starred: true }, state: { starred: true },
}} }}
expanded={hasChildDocuments && !isDragging ? expanded : undefined} expanded={hasChildDocuments && !isDragging ? expanded : undefined}
@@ -107,6 +108,12 @@ function SharedWithMeLink({ userMembership }: Props) {
icon={icon} icon={icon}
label={label} label={label}
exact={false} exact={false}
unreadBadge={
document.unreadNotifications.filter(
(notification) =>
notification.event === NotificationEventType.AddUserToDocument
).length > 0
}
showActions={menuOpen} showActions={menuOpen}
menu={ menu={
document && !isDragging ? ( document && !isDragging ? (

View File

@@ -7,6 +7,7 @@ import { NavigationNode } from "@shared/types";
import EventBoundary from "~/components/EventBoundary"; import EventBoundary from "~/components/EventBoundary";
import EmojiIcon from "~/components/Icons/EmojiIcon"; import EmojiIcon from "~/components/Icons/EmojiIcon";
import NudeButton from "~/components/NudeButton"; import NudeButton from "~/components/NudeButton";
import { UnreadBadge } from "~/components/UnreadBadge";
import useUnmount from "~/hooks/useUnmount"; import useUnmount from "~/hooks/useUnmount";
import { undraggableOnDesktop } from "~/styles"; import { undraggableOnDesktop } from "~/styles";
import Disclosure from "./Disclosure"; import Disclosure from "./Disclosure";
@@ -29,6 +30,7 @@ type Props = Omit<NavLinkProps, "to"> & {
emoji?: string | null; emoji?: string | null;
label?: React.ReactNode; label?: React.ReactNode;
menu?: React.ReactNode; menu?: React.ReactNode;
unreadBadge?: boolean;
showActions?: boolean; showActions?: boolean;
disabled?: boolean; disabled?: boolean;
active?: boolean; active?: boolean;
@@ -64,6 +66,7 @@ function SidebarLink(
expanded, expanded,
onDisclosureClick, onDisclosureClick,
disabled, disabled,
unreadBadge,
...rest ...rest
}: Props, }: Props,
ref: React.RefObject<HTMLAnchorElement> ref: React.RefObject<HTMLAnchorElement>
@@ -141,6 +144,7 @@ function SidebarLink(
{icon && <IconWrapper>{icon}</IconWrapper>} {icon && <IconWrapper>{icon}</IconWrapper>}
{emoji && <EmojiIcon emoji={emoji} />} {emoji && <EmojiIcon emoji={emoji} />}
<Label>{label}</Label> <Label>{label}</Label>
{unreadBadge && <UnreadBadge />}
</Content> </Content>
</Link> </Link>
{menu && <Actions showActions={showActions}>{menu}</Actions>} {menu && <Actions showActions={showActions}>{menu}</Actions>}

View File

@@ -0,0 +1,12 @@
import styled from "styled-components";
import { s } from "@shared/styles";
export const UnreadBadge = styled.div`
width: 8px;
height: 8px;
background: ${s("accent")};
border-radius: 8px;
align-self: center;
position: absolute;
right: 4px;
`;

View File

@@ -2,7 +2,7 @@ import { addDays, differenceInDays } from "date-fns";
import i18n, { t } from "i18next"; import i18n, { t } from "i18next";
import floor from "lodash/floor"; import floor from "lodash/floor";
import { action, autorun, computed, observable, set } from "mobx"; import { action, autorun, computed, observable, set } from "mobx";
import { ExportContentType } from "@shared/types"; import { ExportContentType, NotificationEventType } from "@shared/types";
import type { JSONObject, NavigationNode } from "@shared/types"; import type { JSONObject, NavigationNode } from "@shared/types";
import Storage from "@shared/utils/Storage"; import Storage from "@shared/utils/Storage";
import { isRTL } from "@shared/utils/rtl"; import { isRTL } from "@shared/utils/rtl";
@@ -13,6 +13,7 @@ import type { Properties } from "~/types";
import { client } from "~/utils/ApiClient"; import { client } from "~/utils/ApiClient";
import { settingsPath } from "~/utils/routeHelpers"; import { settingsPath } from "~/utils/routeHelpers";
import Collection from "./Collection"; import Collection from "./Collection";
import Notification from "./Notification";
import View from "./View"; import View from "./View";
import ParanoidModel from "./base/ParanoidModel"; import ParanoidModel from "./base/ParanoidModel";
import Field from "./decorators/Field"; import Field from "./decorators/Field";
@@ -160,6 +161,24 @@ export default class Document extends ParanoidModel {
@observable @observable
isCollectionDeleted: boolean; isCollectionDeleted: boolean;
/**
* Returns the notifications associated with this document.
*/
@computed
get notifications(): Notification[] {
return this.store.rootStore.notifications.filter(
(notification: Notification) => notification.documentId === this.id
);
}
/**
* Returns the unread notifications associated with this document.
*/
@computed
get unreadNotifications(): Notification[] {
return this.notifications.filter((notification) => !notification.viewedAt);
}
/** /**
* Returns the direction of the document text, either "rtl" or "ltr" * Returns the direction of the document text, either "rtl" or "ltr"
*/ */
@@ -391,6 +410,20 @@ export default class Document extends ParanoidModel {
return; return;
} }
// Mark associated unread notifications as read when the document is viewed
this.store.rootStore.notifications
.filter(
(notification: Notification) =>
!notification.viewedAt &&
notification.documentId === this.id &&
[
NotificationEventType.AddUserToDocument,
NotificationEventType.UpdateDocument,
NotificationEventType.PublishDocument,
].includes(notification.event)
)
.forEach((notification) => notification.markAsRead());
this.lastViewedAt = new Date().toString(); this.lastViewedAt = new Date().toString();
return this.store.rootStore.views.create({ return this.store.rootStore.views.create({

View File

@@ -1,5 +1,5 @@
import { TFunction } from "i18next"; import { TFunction } from "i18next";
import { action, observable } from "mobx"; import { action, computed, observable } from "mobx";
import { NotificationEventType } from "@shared/types"; import { NotificationEventType } from "@shared/types";
import { import {
collectionPath, collectionPath,
@@ -154,6 +154,7 @@ class Notification extends Model {
* *
* @returns The router path. * @returns The router path.
*/ */
@computed
get path() { get path() {
switch (this.event) { switch (this.event) {
case NotificationEventType.PublishDocument: case NotificationEventType.PublishDocument:

View File

@@ -34,6 +34,7 @@ export default class ViewsStore extends Store<View> {
if (!view) { if (!view) {
return; return;
} }
view.touch(); view.touch();
} }
} }