Compare commits

..

2 Commits

Author SHA1 Message Date
Tom Moor 7dd3c96b14 doc 2024-12-03 21:59:46 -05:00
Tom Moor f98d1203ea fix: Made the document header components more responsive to the available space 2024-12-03 21:59:05 -05:00
44 changed files with 185 additions and 481 deletions
+6 -1
View File
@@ -144,7 +144,12 @@ function DocumentCard(props: Props) {
</Heading>
<DocumentMeta size="xsmall">
<Clock size={18} />
<Time dateTime={document.updatedAt} addSuffix shorten />
<Time
dateTime={document.updatedAt}
tooltipDelay={500}
addSuffix
shorten
/>
</DocumentMeta>
</div>
</Content>
+5 -1
View File
@@ -111,7 +111,11 @@ function DocumentListItem(
<Badge yellow>{t("New")}</Badge>
)}
{document.isDraft && showDraft && (
<Tooltip content={t("Only visible to you")} placement="top">
<Tooltip
content={t("Only visible to you")}
delay={500}
placement="top"
>
<Badge>{t("Draft")}</Badge>
</Tooltip>
)}
+1
View File
@@ -140,6 +140,7 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
title={
<Time
dateTime={event.createdAt}
tooltipDelay={500}
format={{
en_US: "MMM do, h:mm a",
fr_FR: "'Le 'd MMMM 'à' H:mm",
+2 -2
View File
@@ -61,8 +61,8 @@ function Header(
});
}, []);
const setBreadcrumbRef = React.useCallback((node: HTMLDivElement | null) => {
breadcrumbsRef.current = node?.firstElementChild as HTMLDivElement;
const setBreadcrumbRef = React.useCallback((node: HTMLDivElement) => {
breadcrumbsRef.current = node.firstElementChild as HTMLDivElement;
}, []);
const size = useComponentSize(internalRef);
+3 -1
View File
@@ -23,6 +23,7 @@ function eachMinute(fn: () => void) {
export type Props = {
children?: React.ReactNode;
dateTime: string;
tooltipDelay?: number;
addSuffix?: boolean;
shorten?: boolean;
relative?: boolean;
@@ -36,6 +37,7 @@ const LocaleTime: React.FC<Props> = ({
shorten,
format,
relative,
tooltipDelay,
}: Props) => {
const userLocale = useUserLocale();
const dateFormatLong: Record<string, string> = {
@@ -80,7 +82,7 @@ const LocaleTime: React.FC<Props> = ({
});
return (
<Tooltip content={tooltipContent} placement="bottom">
<Tooltip content={tooltipContent} delay={tooltipDelay} placement="bottom">
<time dateTime={dateTime}>{children || content}</time>
</Tooltip>
);
@@ -52,7 +52,11 @@ function NotificationListItem({ notification, onNavigate }: Props) {
<Text weight="bold">{notification.subject}</Text>
</Text>
<Text type="tertiary" size="xsmall">
<Time dateTime={notification.createdAt} addSuffix />{" "}
<Time
dateTime={notification.createdAt}
tooltipDelay={1000}
addSuffix
/>{" "}
{collection && <>&middot; {collection.name}</>}
</Text>
{notification.comment && (
@@ -60,7 +60,7 @@ function Notifications(
</Text>
<Flex gap={8}>
{notifications.approximateUnreadCount > 0 && (
<Tooltip content={t("Mark all as read")}>
<Tooltip delay={500} content={t("Mark all as read")}>
<Button action={markNotificationsAsRead} context={context}>
<MarkAsReadIcon />
</Button>
+1 -1
View File
@@ -128,7 +128,7 @@ const Reaction: React.FC<Props> = ({
);
return tooltipContent ? (
<Tooltip content={tooltipContent} placement="bottom">
<Tooltip content={tooltipContent} delay={250} placement="bottom">
{DisplayedEmoji}
</Tooltip>
) : (
+6 -1
View File
@@ -98,7 +98,12 @@ const ReactionPicker: React.FC<Props> = ({
<>
<PopoverDisclosure {...popover}>
{(props) => (
<Tooltip content={t("Add reaction")} placement="top" hideOnClick>
<Tooltip
content={t("Add reaction")}
placement="top"
delay={500}
hideOnClick
>
<NudeButton
{...props}
aria-label={t("Reaction picker")}
@@ -119,7 +119,7 @@ function PublicAccess({ document, share, sharedParent }: Props) {
: share?.url ?? "";
const copyButton = (
<Tooltip content={t("Copy public link")} placement="top">
<Tooltip content={t("Copy public link")} delay={500} placement="top">
<CopyToClipboard text={shareUrl} onCopy={handleCopied}>
<NudeButton type="button" disabled={!share} style={{ marginRight: 3 }}>
<CopyIcon color={theme.placeholder} size={18} />
@@ -31,7 +31,7 @@ export function CopyLinkButton({
}, [onCopy, t]);
return (
<Tooltip content={t("Copy link")} placement="top">
<Tooltip content={t("Copy link")} delay={500} placement="top">
<CopyToClipboard text={url} onCopy={handleCopied}>
<NudeButton type="button">
<LinkIcon size={20} />
+1
View File
@@ -80,6 +80,7 @@ function AppSidebar() {
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<ToggleButton
position="bottom"
+5 -1
View File
@@ -42,7 +42,11 @@ function SettingsSidebar() {
image={<StyledBackIcon />}
onClick={returnToApp}
>
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
+5 -1
View File
@@ -81,7 +81,11 @@ const ToggleSidebar = () => {
const { ui } = useStores();
return (
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
<Tooltip
content={t("Toggle sidebar")}
shortcut={`${metaDisplay}+.`}
delay={500}
>
<ToggleButton
position="bottom"
image={<SidebarIcon />}
@@ -278,7 +278,7 @@ function InnerDocumentLink(
!isDraggingAnyDocument ? (
<Fade>
{can.createChildDocument && (
<Tooltip content={t("New doc")}>
<Tooltip content={t("New doc")} delay={500}>
<NudeButton
type={undefined}
aria-label={t("New nested document")}
@@ -43,12 +43,12 @@ function HistoryNavigation(props: React.ComponentProps<typeof Flex>) {
return (
<Navigation gap={4} {...props}>
<Tooltip content={t("Go back")}>
<Tooltip content={t("Go back")} delay={500}>
<NudeButton onClick={() => Desktop.bridge?.goBack()}>
<Back $active={back} />
</NudeButton>
</Tooltip>
<Tooltip content={t("Go forward")}>
<Tooltip content={t("Go forward")} delay={500}>
<NudeButton onClick={() => Desktop.bridge?.goForward()}>
<Forward $active={forward} />
</NudeButton>
+1 -1
View File
@@ -22,7 +22,7 @@ function Time({ onClick, ...props }: Props) {
<time dateTime={props.dateTime}>{props.children || content}</time>
}
>
<LocaleTime {...props} />
<LocaleTime tooltipDelay={250} {...props} />
</React.Suspense>
</span>
);
+12 -5
View File
@@ -1,9 +1,9 @@
import Tippy, { TippyProps } from "@tippyjs/react";
import * as React from "react";
import styled, { createGlobalStyle } from "styled-components";
import { roundArrow } from "tippy.js";
import { s } from "@shared/styles";
import useMobile from "~/hooks/useMobile";
import { useTooltipContext } from "./TooltipContext";
export type Props = Omit<TippyProps, "content" | "theme"> & {
/** The content to display in the tooltip. */
@@ -12,9 +12,8 @@ export type Props = Omit<TippyProps, "content" | "theme"> & {
shortcut?: React.ReactNode;
};
function Tooltip({ shortcut, content: tooltip, delay = 500, ...rest }: Props) {
function Tooltip({ shortcut, content: tooltip, delay = 50, ...rest }: Props) {
const isMobile = useMobile();
const singleton = useTooltipContext();
let content = <>{tooltip}</>;
@@ -31,7 +30,15 @@ function Tooltip({ shortcut, content: tooltip, delay = 500, ...rest }: Props) {
}
return (
<Tippy content={content} delay={delay} singleton={singleton} {...rest} />
<Tippy
arrow={roundArrow}
animation="shift-away"
content={content}
delay={delay}
duration={[200, 150]}
inertia
{...rest}
/>
);
}
@@ -125,7 +132,7 @@ export const TooltipStyles = createGlobalStyle`
padding:5px 9px;
z-index:1
}
/* Arrow Styles */
.tippy-box[data-placement^=top]>.tippy-svg-arrow{
bottom:0
-36
View File
@@ -1,36 +0,0 @@
import Tippy, { useSingleton, TippyProps } from "@tippyjs/react";
import * as React from "react";
import { roundArrow } from "tippy.js";
export const TooltipContext =
React.createContext<TippyProps["singleton"]>(undefined);
export function useTooltipContext() {
return React.useContext(TooltipContext);
}
type Props = {
children: React.ReactNode;
tippyProps?: TippyProps;
};
export function TooltipProvider({ children, tippyProps }: Props) {
const [source, target] = useSingleton();
return (
<>
<Tippy
delay={500}
arrow={roundArrow}
animation="shift-away"
singleton={source}
duration={[200, 150]}
inertia
{...tippyProps}
/>
<TooltipContext.Provider value={target}>
{children}
</TooltipContext.Provider>
</>
);
}
+19 -45
View File
@@ -25,18 +25,10 @@ import { altDisplay, isModKey, metaDisplay } from "~/utils/keyboard";
import { useEditor } from "./EditorContext";
type Props = {
/** Whether the find and replace popover is open */
open: boolean;
/** Callback when the find and replace popover is opened */
onOpen: () => void;
/** Callback when the find and replace popover is closed */
onClose: () => void;
/** Whether the editor is in read-only mode */
readOnly?: boolean;
/** The current highlighted index in the search results */
currentIndex: number;
/** The total number of search results */
totalResults: number;
};
export default function FindAndReplace({
@@ -44,8 +36,6 @@ export default function FindAndReplace({
open,
onOpen,
onClose,
currentIndex,
totalResults,
}: Props) {
const editor = useEditor();
const finalFocusRef = React.useRef<HTMLElement>(
@@ -280,26 +270,25 @@ export default function FindAndReplace({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [popover.visible]);
const disabled = totalResults === 0;
const navigation = (
<>
<Tooltip
content={t("Previous match")}
shortcut="shift+enter"
delay={500}
placement="bottom"
>
<ButtonLarge
disabled={disabled}
onClick={() => editor.commands.prevSearchMatch()}
>
<ButtonLarge onClick={() => editor.commands.prevSearchMatch()}>
<CaretUpIcon />
</ButtonLarge>
</Tooltip>
<Tooltip content={t("Next match")} shortcut="enter" placement="bottom">
<ButtonLarge
disabled={disabled}
onClick={() => editor.commands.nextSearchMatch()}
>
<Tooltip
content={t("Next match")}
shortcut="enter"
delay={500}
placement="bottom"
>
<ButtonLarge onClick={() => editor.commands.nextSearchMatch()}>
<CaretDownIcon />
</ButtonLarge>
</Tooltip>
@@ -317,7 +306,7 @@ export default function FindAndReplace({
width={420}
>
<Content column>
<Flex gap={4}>
<Flex gap={8}>
<StyledInput
ref={inputRef}
maxLength={255}
@@ -330,6 +319,7 @@ export default function FindAndReplace({
<Tooltip
content={t("Match case")}
shortcut={`${altDisplay}+${metaDisplay}+c`}
delay={500}
placement="bottom"
>
<ButtonSmall onClick={handleCaseSensitive}>
@@ -341,6 +331,7 @@ export default function FindAndReplace({
<Tooltip
content={t("Enable regex")}
shortcut={`${altDisplay}+${metaDisplay}+r`}
delay={500}
placement="bottom"
>
<ButtonSmall onClick={handleRegex}>
@@ -353,15 +344,16 @@ export default function FindAndReplace({
</StyledInput>
{navigation}
{!readOnly && (
<Tooltip content={t("Replace options")} placement="bottom">
<Tooltip
content={t("Replace options")}
delay={500}
placement="bottom"
>
<ButtonLarge onClick={handleMore}>
<ReplaceIcon color={theme.textSecondary} />
</ButtonLarge>
</Tooltip>
)}
<Results>
{totalResults > 0 ? currentIndex + 1 : 0} / {totalResults}
</Results>
</Flex>
<ResizingHeightContainer>
{showReplace && !readOnly && (
@@ -375,10 +367,10 @@ export default function FindAndReplace({
onRequestSubmit={handleReplaceAll}
onChange={(ev) => setReplaceTerm(ev.currentTarget.value)}
/>
<Button onClick={handleReplace} disabled={disabled} neutral>
<Button onClick={handleReplace} neutral>
{t("Replace")}
</Button>
<Button onClick={handleReplaceAll} disabled={disabled} neutral>
<Button onClick={handleReplaceAll} neutral>
{t("Replace all")}
</Button>
</Flex>
@@ -404,12 +396,6 @@ const ButtonSmall = styled(NudeButton)`
&[aria-expanded="true"] {
background: ${s("sidebarControlHoverBackground")};
}
&:disabled {
color: ${s("textTertiary")};
background: none;
cursor: default;
}
`;
const ButtonLarge = styled(ButtonSmall)`
@@ -422,15 +408,3 @@ const Content = styled(Flex)`
margin-bottom: -16px;
position: static;
`;
const Results = styled.span`
color: ${s("textSecondary")};
font-size: 12px;
font-weight: 500;
font-variant-numeric: tabular-nums;
line-height: 32px;
min-width: 32px;
letter-spacing: -0.5px;
text-align: right;
user-select: none;
`;
+29 -35
View File
@@ -1,4 +1,3 @@
import { TippyProps } from "@tippyjs/react";
import * as React from "react";
import { useMenuState } from "reakit";
import { MenuButton } from "reakit/Menu";
@@ -8,7 +7,6 @@ import { MenuItem } from "@shared/editor/types";
import { s } from "@shared/styles";
import ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template";
import { TooltipProvider } from "~/components/TooltipContext";
import { MenuItem as TMenuItem } from "~/types";
import { useEditor } from "./EditorContext";
import ToolbarButton from "./ToolbarButton";
@@ -77,8 +75,6 @@ function ToolbarDropdown(props: { active: boolean; item: MenuItem }) {
);
}
const tippyProps = { placement: "top" } as TippyProps;
function ToolbarMenu(props: Props) {
const { commands, view } = useEditor();
const { items } = props;
@@ -95,38 +91,36 @@ function ToolbarMenu(props: Props) {
};
return (
<TooltipProvider tippyProps={tippyProps}>
<FlexibleWrapper>
{items.map((item, index) => {
if (item.name === "separator" && item.visible !== false) {
return <ToolbarSeparator key={index} />;
}
if (item.visible === false || !item.icon) {
return null;
}
const isActive = item.active ? item.active(state) : false;
<FlexibleWrapper>
{items.map((item, index) => {
if (item.name === "separator" && item.visible !== false) {
return <ToolbarSeparator key={index} />;
}
if (item.visible === false || !item.icon) {
return null;
}
const isActive = item.active ? item.active(state) : false;
return (
<Tooltip
content={item.label === item.tooltip ? undefined : item.tooltip}
key={index}
>
{item.children ? (
<ToolbarDropdown active={isActive && !item.label} item={item} />
) : (
<ToolbarButton
onClick={handleClick(item)}
active={isActive && !item.label}
>
{item.label && <Label>{item.label}</Label>}
{item.icon}
</ToolbarButton>
)}
</Tooltip>
);
})}
</FlexibleWrapper>
</TooltipProvider>
return (
<Tooltip
content={item.label === item.tooltip ? undefined : item.tooltip}
key={index}
>
{item.children ? (
<ToolbarDropdown active={isActive && !item.label} item={item} />
) : (
<ToolbarButton
onClick={handleClick(item)}
active={isActive && !item.label}
>
{item.label && <Label>{item.label}</Label>}
{item.icon}
</ToolbarButton>
)}
</Tooltip>
);
})}
</FlexibleWrapper>
);
}
+2 -12
View File
@@ -8,18 +8,8 @@ type Props = {
children?: React.ReactNode;
};
const WrappedTooltip: React.FC<Props> = ({
children,
content,
...rest
}: Props) => (
<Tooltip
offset={[0, 16]}
delay={150}
content={content}
placement="top"
{...rest}
>
const WrappedTooltip: React.FC<Props> = ({ children, content }: Props) => (
<Tooltip offset={[0, 16]} delay={150} content={content} placement="top">
<TooltipContent>{children}</TooltipContent>
</Tooltip>
);
-6
View File
@@ -332,8 +332,6 @@ export default class FindAndReplaceExtension extends Extension {
public widget = ({ readOnly }: WidgetProps) => (
<FindAndReplace
currentIndex={this.currentResultIndex}
totalResults={this.results.length}
readOnly={readOnly}
open={this.open}
onOpen={() => {
@@ -348,11 +346,7 @@ export default class FindAndReplaceExtension extends Extension {
@observable
private open = false;
@observable
private results: { from: number; to: number }[] = [];
@observable
private currentResultIndex = 0;
private searchTerm = "";
}
+15 -18
View File
@@ -20,7 +20,6 @@ import { initI18n } from "~/utils/i18n";
import Desktop from "./components/DesktopEventHandler";
import LazyPolyfill from "./components/LazyPolyfills";
import PageScroll from "./components/PageScroll";
import { TooltipProvider } from "./components/TooltipContext";
import Routes from "./routes";
import Logger from "./utils/Logger";
import { PluginManager } from "./utils/PluginManager";
@@ -56,23 +55,21 @@ if (element) {
<Theme>
<ErrorBoundary showTitle>
<KBarProvider actions={[]} options={commandBarOptions}>
<TooltipProvider>
<LazyPolyfill>
<LazyMotion features={loadFeatures}>
<Router history={history}>
<PageScroll>
<PageTheme />
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
<Dialogs />
<Desktop />
</PageScroll>
</Router>
</LazyMotion>
</LazyPolyfill>
</TooltipProvider>
<LazyPolyfill>
<LazyMotion features={loadFeatures}>
<Router history={history}>
<PageScroll>
<PageTheme />
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
<Dialogs />
<Desktop />
</PageScroll>
</Router>
</LazyMotion>
</LazyPolyfill>
</KBarProvider>
</ErrorBoundary>
</Theme>
+6 -1
View File
@@ -19,7 +19,12 @@ function NewDocumentMenu() {
}
return (
<Tooltip content={t("New document")} shortcut="n" placement="bottom">
<Tooltip
content={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button as={Link} to={newDocumentPath()} icon={<PlusIcon />}>
{t("New doc")}
</Button>
-3
View File
@@ -119,8 +119,6 @@ class Notification extends Model {
return t("mentioned you in");
case NotificationEventType.CreateComment:
return t("left a comment on");
case NotificationEventType.ResolveComment:
return t("resolved a comment on");
case NotificationEventType.AddUserToDocument:
return t("shared");
case NotificationEventType.AddUserToCollection:
@@ -172,7 +170,6 @@ class Notification extends Model {
return this.document?.path;
}
case NotificationEventType.MentionedInComment:
case NotificationEventType.ResolveComment:
case NotificationEventType.CreateComment: {
return this.document && this.comment
? commentPath(this.document, this.comment)
@@ -27,6 +27,7 @@ function Actions({ collection }: Props) {
<Tooltip
content={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button
@@ -316,7 +316,7 @@ function CommentForm({
{t("Cancel")}
</ButtonSmall>
</Flex>
<Tooltip content={t("Upload image")} placement="top">
<Tooltip delay={500} content={t("Upload image")} placement="top">
<NudeButton onClick={handleImageUpload}>
<ImageIcon color={theme.textTertiary} />
</NudeButton>
@@ -197,12 +197,21 @@ function CommentThreadItem({
{showAuthor && <em>{comment.createdBy.name}</em>}
{showAuthor && showTime && <> &middot; </>}
{showTime && (
<Time dateTime={comment.createdAt} addSuffix shorten />
<Time
dateTime={comment.createdAt}
tooltipDelay={500}
addSuffix
shorten
/>
)}
{showEdited && (
<>
{" "}
(<Time dateTime={comment.updatedAt}>{t("edited")}</Time>)
(
<Time dateTime={comment.updatedAt} tooltipDelay={500}>
{t("edited")}
</Time>
)
</>
)}
</Meta>
@@ -295,7 +304,12 @@ const ResolveButton = ({
const { t } = useTranslation();
return (
<Tooltip content={t("Mark as resolved")} placement="top" hideOnClick>
<Tooltip
content={t("Mark as resolved")}
placement="top"
delay={500}
hideOnClick
>
<Action
as={NudeButton}
context={context}
+4 -8
View File
@@ -94,18 +94,14 @@ function Comments() {
React.useEffect(() => {
// Handles: 1. on refresh 2. when switching sort setting
const readyToDisplay = Boolean(document && isEditorInitialized);
if (
readyToDisplay &&
sortOption.type === CommentSortType.MostRecent &&
!viewingResolved
) {
if (readyToDisplay && sortOption.type === CommentSortType.MostRecent) {
scrollToBottom();
}
}, [sortOption.type, document, isEditorInitialized, viewingResolved]);
}, [sortOption.type, document, isEditorInitialized]);
React.useEffect(() => {
setShowJumpToRecentBtn(false);
if (sortOption.type === CommentSortType.MostRecent && !viewingResolved) {
if (sortOption.type === CommentSortType.MostRecent) {
const commentsAdded = threads.length > prevThreadCount.current;
if (commentsAdded) {
if (isAtBottom.current) {
@@ -116,7 +112,7 @@ function Comments() {
}
}
prevThreadCount.current = threads.length;
}, [sortOption.type, threads.length, viewingResolved]);
}, [sortOption.type, threads.length]);
if (!document || !isEditorInitialized) {
return null;
+10 -1
View File
@@ -133,6 +133,7 @@ function DocumentHeader({
: `${t("Show contents")} (${t("available when headings are added")})`
}
shortcut={`ctrl+${altDisplay}+h`}
delay={250}
placement="bottom"
>
<Button
@@ -150,6 +151,7 @@ function DocumentHeader({
noun: document.noun,
})}
shortcut="e"
delay={500}
placement="bottom"
>
<Button
@@ -169,6 +171,7 @@ function DocumentHeader({
content={
resolvedTheme === "light" ? t("Switch to dark") : t("Switch to light")
}
delay={500}
placement="bottom"
>
<Button
@@ -287,6 +290,7 @@ function DocumentHeader({
<Tooltip
content={t("Save")}
shortcut={`${metaDisplay}+enter`}
delay={500}
placement="bottom"
>
<Button
@@ -319,6 +323,7 @@ function DocumentHeader({
<Tooltip
content={t("New document")}
shortcut="n"
delay={500}
placement="bottom"
>
<Button icon={<PlusIcon />} {...props} neutral>
@@ -331,7 +336,11 @@ function DocumentHeader({
)}
{revision && revision.createdAt !== document.updatedAt && (
<Action>
<Tooltip content={t("Restore version")} placement="bottom">
<Tooltip
content={t("Restore version")}
delay={500}
placement="bottom"
>
<Button
action={restoreRevision}
context={context}
@@ -23,7 +23,7 @@ function KeyboardShortcutsButton() {
};
return (
<Tooltip content={t("Keyboard shortcuts")} shortcut="?">
<Tooltip content={t("Keyboard shortcuts")} shortcut="?" delay={500}>
<Button onClick={handleOpenKeyboardShortcuts} $hidden={isEditingFocus}>
<KeyboardIcon />
</Button>
@@ -32,7 +32,7 @@ function SidebarLayout({ title, onClose, children, scrollable = true }: Props) {
<>
<Header>
<Title>{title}</Title>
<Tooltip content={t("Close")} shortcut="Esc">
<Tooltip content={t("Close")} shortcut="Esc" delay={500}>
<Button
icon={<ForwardIcon />}
onClick={onClose}
@@ -17,7 +17,7 @@ export function DocumentFilter(props: Props) {
return (
<div>
<Tooltip content={t("Remove document filter")}>
<Tooltip content={t("Remove document filter")} delay={350}>
<StyledButton onClick={props.onClick} icon={<CloseIcon />} neutral>
{props.document.title}
</StyledButton>
@@ -33,7 +33,7 @@ function RecentSearchListItem({ searchQuery }: Props) {
{...rovingTabIndex}
>
{searchQuery.query}
<Tooltip content={t("Remove search")}>
<Tooltip content={t("Remove search")} delay={150}>
<RemoveButton
aria-label={t("Remove search")}
onClick={async (ev) => {
-9
View File
@@ -6,7 +6,6 @@ import {
CollectionIcon,
CommentIcon,
DocumentIcon,
DoneIcon,
EditIcon,
EmailIcon,
PublishIcon,
@@ -66,14 +65,6 @@ function Notifications() {
"Receive a notification when someone mentions you in a document or comment"
),
},
{
event: NotificationEventType.ResolveComment,
icon: <DoneIcon />,
title: t("Resolved"),
description: t(
"Receive a notification when a comment thread you were involved in is resolved"
),
},
{
event: NotificationEventType.CreateCollection,
icon: <CollectionIcon />,
+1 -1
View File
@@ -223,7 +223,7 @@
"sequelize-typescript": "^2.1.6",
"slug": "^5.3.0",
"slugify": "^1.6.6",
"socket.io": "^4.8.1",
"socket.io": "^4.7.5",
"socket.io-client": "^4.8.0",
"socket.io-redis": "^6.1.1",
"sonner": "^1.0.3",
@@ -32,7 +32,8 @@ type BeforeSend = {
type Props = InputProps & BeforeSend;
/**
* Email sent to a user when they are mentioned in a comment.
* Email sent to a user when a new comment is created in a document they are
* subscribed to.
*/
export default class CommentMentionedEmail extends BaseEmail<
InputProps,
@@ -1,163 +0,0 @@
import * as React from "react";
import { NotificationEventType } from "@shared/types";
import { Collection, Comment, Document } from "@server/models";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import { can } from "@server/policies";
import BaseEmail, { EmailMessageCategory, EmailProps } from "./BaseEmail";
import Body from "./components/Body";
import Button from "./components/Button";
import Diff from "./components/Diff";
import EmailTemplate from "./components/EmailLayout";
import EmptySpace from "./components/EmptySpace";
import Footer from "./components/Footer";
import Header from "./components/Header";
import Heading from "./components/Heading";
type InputProps = EmailProps & {
userId: string;
documentId: string;
actorName: string;
commentId: string;
teamUrl: string;
};
type BeforeSend = {
document: Document;
collection: Collection;
body: string | undefined;
unsubscribeUrl: string;
};
type Props = InputProps & BeforeSend;
/**
* Email sent to a user when a comment they are involved in was resolved.
*/
export default class CommentResolvedEmail extends BaseEmail<
InputProps,
BeforeSend
> {
protected get category() {
return EmailMessageCategory.Notification;
}
protected async beforeSend(props: InputProps) {
const { documentId, commentId } = props;
const document = await Document.unscoped().findByPk(documentId);
if (!document) {
return false;
}
const collection = await document.$get("collection");
if (!collection) {
return false;
}
const [comment, team] = await Promise.all([
Comment.findByPk(commentId),
document.$get("team"),
]);
if (!comment || !team) {
return false;
}
const body = await this.htmlForData(
team,
ProsemirrorHelper.toProsemirror(comment.data)
);
return {
document,
collection,
body,
unsubscribeUrl: this.unsubscribeUrl(props),
};
}
protected unsubscribeUrl({ userId }: InputProps) {
return NotificationSettingsHelper.unsubscribeUrl(
userId,
NotificationEventType.ResolveComment
);
}
protected replyTo({ notification }: Props) {
if (notification?.user && notification.actor?.email) {
if (can(notification.user, "readEmail", notification.actor)) {
return notification.actor.email;
}
}
return;
}
protected subject({ document }: Props) {
return `Resolved a comment thread in “${document.title}`;
}
protected preview({ actorName }: Props): string {
return `${actorName} resolved a comment thread`;
}
protected fromName({ actorName }: Props): string {
return actorName;
}
protected renderAsText({
actorName,
teamUrl,
document,
commentId,
collection,
}: Props): string {
const t1 = `${actorName} resolved a comment thread on "${document.title}"`;
const t2 = collection.name ? ` in the ${collection.name} collection` : "";
const t3 = `Open Thread: ${teamUrl}${document.url}?commentId=${commentId}`;
return `${t1}${t2}.\n\n${t3}`;
}
protected render(props: Props) {
const {
document,
collection,
actorName,
teamUrl,
commentId,
unsubscribeUrl,
body,
} = props;
const threadLink = `${teamUrl}${document.url}?commentId=${commentId}&ref=notification-email`;
return (
<EmailTemplate
previewText={this.preview(props)}
goToAction={{ url: threadLink, name: "View Thread" }}
>
<Header />
<Body>
<Heading>{document.title}</Heading>
<p>
{actorName} resolved a comment on{" "}
<a href={threadLink}>{document.title}</a>{" "}
{collection.name ? `in the ${collection.name} collection` : ""}.
</p>
{body && (
<>
<EmptySpace height={20} />
<Diff>
<div dangerouslySetInnerHTML={{ __html: body }} />
</Diff>
<EmptySpace height={20} />
</>
)}
<p>
<Button href={threadLink}>Open Thread</Button>
</p>
</Body>
<Footer unsubscribeUrl={unsubscribeUrl} />
</EmailTemplate>
);
}
}
@@ -4,7 +4,6 @@ import CollectionCreatedEmail from "@server/emails/templates/CollectionCreatedEm
import CollectionSharedEmail from "@server/emails/templates/CollectionSharedEmail";
import CommentCreatedEmail from "@server/emails/templates/CommentCreatedEmail";
import CommentMentionedEmail from "@server/emails/templates/CommentMentionedEmail";
import CommentResolvedEmail from "@server/emails/templates/CommentResolvedEmail";
import DocumentMentionedEmail from "@server/emails/templates/DocumentMentionedEmail";
import DocumentPublishedOrUpdatedEmail from "@server/emails/templates/DocumentPublishedOrUpdatedEmail";
import DocumentSharedEmail from "@server/emails/templates/DocumentSharedEmail";
@@ -144,23 +143,6 @@ export default class EmailsProcessor extends BaseProcessor {
).schedule({
delay: Minute.ms,
});
return;
}
case NotificationEventType.ResolveComment: {
await new CommentResolvedEmail(
{
to: notification.user.email,
userId: notification.userId,
documentId: notification.documentId,
teamUrl: notification.team.url,
actorName: notification.actor.name,
commentId: notification.commentId,
},
{ notificationId: notification.id }
).schedule({
delay: Minute.ms,
});
}
}
}
@@ -1,5 +1,3 @@
import invariant from "invariant";
import { Op } from "sequelize";
import { NotificationEventType } from "@shared/types";
import { Comment, Document, Notification, User } from "@server/models";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
@@ -9,16 +7,6 @@ import BaseTask, { TaskPriority } from "./BaseTask";
export default class CommentUpdatedNotificationsTask extends BaseTask<CommentEvent> {
public async perform(event: CommentUpdateEvent) {
const isResolving =
event.changes?.previous?.resolvedAt === null &&
event.changes?.attributes?.resolvedAt !== null;
return isResolving
? await this.handleResolvedComment(event)
: await this.handleMentionedComment(event);
}
private async handleMentionedComment(event: CommentUpdateEvent) {
const newMentionIds = event.data?.newMentionIds;
if (!newMentionIds) {
return;
@@ -65,69 +53,6 @@ export default class CommentUpdatedNotificationsTask extends BaseTask<CommentEve
documentId: document.id,
commentId: comment.id,
});
userIdsMentioned.push(mention.modelId);
}
}
}
private async handleResolvedComment(event: CommentUpdateEvent) {
invariant(
!event.data?.newMentionIds,
"newMentionIds should not be present in resolved comment"
);
const [document, commentsAndReplies] = await Promise.all([
Document.scope("withCollection").findOne({
where: { id: event.documentId },
}),
Comment.findAll({
where: {
[Op.or]: [{ id: event.modelId }, { parentCommentId: event.modelId }],
},
}),
]);
if (!document || !commentsAndReplies) {
return;
}
const userIdsNotified: string[] = [];
// Don't notify resolver
userIdsNotified.push(event.actorId);
for (const item of commentsAndReplies) {
// Mentions:
const proseCommentData = ProsemirrorHelper.toProsemirror(item.data);
const mentions = ProsemirrorHelper.parseMentions(proseCommentData);
const userIds = mentions.map((mention) => mention.modelId);
// Comment author:
userIds.push(item.createdById);
for (const userId of userIds) {
if (userIdsNotified.includes(userId)) {
continue;
}
const user = await User.findByPk(userId);
if (
user &&
user.subscribedToEventType(NotificationEventType.ResolveComment) &&
(await canUserAccessDocument(user, document.id))
) {
await Notification.create({
event: NotificationEventType.ResolveComment,
userId: user.id,
actorId: event.actorId,
teamId: document.teamId,
documentId: document.id,
commentId: event.modelId,
});
userIdsNotified.push(userId);
}
}
}
}
@@ -557,7 +557,6 @@
"created the collection": "created the collection",
"mentioned you in": "mentioned you in",
"left a comment on": "left a comment on",
"resolved a comment on": "resolved a comment on",
"shared": "shared",
"invited you to": "invited you to",
"Choose a date": "Choose a date",
@@ -940,7 +939,6 @@
"Receive a notification when a document you are subscribed to or a thread you participated in receives a comment": "Receive a notification when a document you are subscribed to or a thread you participated in receives a comment",
"Mentioned": "Mentioned",
"Receive a notification when someone mentions you in a document or comment": "Receive a notification when someone mentions you in a document or comment",
"Receive a notification when a comment thread you were involved in is resolved": "Receive a notification when a comment thread you were involved in is resolved",
"Collection created": "Collection created",
"Receive a notification whenever a new collection is created": "Receive a notification whenever a new collection is created",
"Invite accepted": "Invite accepted",
-2
View File
@@ -277,7 +277,6 @@ export enum NotificationEventType {
CreateRevision = "revisions.create",
CreateCollection = "collections.create",
CreateComment = "comments.create",
ResolveComment = "comments.resolve",
MentionedInDocument = "documents.mentioned",
MentionedInComment = "comments.mentioned",
InviteAccepted = "emails.invite_accepted",
@@ -306,7 +305,6 @@ export const NotificationEventDefaults: Record<NotificationEventType, boolean> =
[NotificationEventType.UpdateDocument]: true,
[NotificationEventType.CreateCollection]: false,
[NotificationEventType.CreateComment]: true,
[NotificationEventType.ResolveComment]: true,
[NotificationEventType.CreateRevision]: false,
[NotificationEventType.MentionedInDocument]: true,
[NotificationEventType.MentionedInComment]: true,
+15 -15
View File
@@ -6915,10 +6915,10 @@ cookie@^0.7.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.0.tgz#2148f68a77245d5c2c0005d264bc3e08cfa0655d"
integrity sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ==
cookie@~0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
cookie@~0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
integrity "sha1-r9cT/ibr0hupXOth+agRblClN9E= sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
cookies@~0.9.0:
version "0.9.1"
@@ -7959,21 +7959,21 @@ engine.io-parser@~5.2.1:
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb"
integrity "sha1-nyE8d1Ev8abMDHqGEIp//OsW/Ps= sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ=="
engine.io@~6.6.0:
version "6.6.2"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.2.tgz#32bd845b4db708f8c774a4edef4e5c8a98b3da72"
integrity sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==
engine.io@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.2.tgz#769348ced9d56bd47bd83d308ec1c3375e85937c"
integrity "sha1-dpNIztnVa9R72D0wjsHDN16Fk3w= sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA=="
dependencies:
"@types/cookie" "^0.4.1"
"@types/cors" "^2.8.12"
"@types/node" ">=10.0.0"
accepts "~1.3.4"
base64id "2.0.0"
cookie "~0.7.2"
cookie "~0.4.1"
cors "~2.8.5"
debug "~4.3.1"
engine.io-parser "~5.2.1"
ws "~8.17.1"
ws "~8.11.0"
enhanced-resolve@^5.15.0:
version "5.17.1"
@@ -14185,16 +14185,16 @@ socket.io-redis@^6.1.1:
socket.io-adapter "~2.2.0"
uid2 "0.0.3"
socket.io@^4.8.1:
version "4.8.1"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a"
integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==
socket.io@^4.7.5:
version "4.7.5"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.5.tgz#56eb2d976aef9d1445f373a62d781a41c7add8f8"
integrity sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==
dependencies:
accepts "~1.3.4"
base64id "~2.0.0"
cors "~2.8.5"
debug "~4.3.2"
engine.io "~6.6.0"
engine.io "~6.5.2"
socket.io-adapter "~2.5.2"
socket.io-parser "~4.2.4"