feat: Badge documents in sidebar that have been newly shared with you
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
12
app/components/UnreadBadge.tsx
Normal file
12
app/components/UnreadBadge.tsx
Normal 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;
|
||||||
|
`;
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export default class ViewsStore extends Store<View> {
|
|||||||
if (!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
view.touch();
|
view.touch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user