diff --git a/app/components/DocumentViews.tsx b/app/components/DocumentViews.tsx index 363ac1cbd..20d306815 100644 --- a/app/components/DocumentViews.tsx +++ b/app/components/DocumentViews.tsx @@ -1,8 +1,8 @@ -import { formatDistanceToNow } from "date-fns"; import { sortBy } from "lodash"; import { observer } from "mobx-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; +import { dateToRelative } from "@shared/utils/date"; import Document from "~/models/Document"; import User from "~/models/User"; import Avatar from "~/components/Avatar"; @@ -53,7 +53,7 @@ function DocumentViews({ document, isOpen }: Props) { ? t("Currently editing") : t("Currently viewing") : t("Viewed {{ timeAgo }} ago", { - timeAgo: formatDistanceToNow( + timeAgo: dateToRelative( view ? Date.parse(view.lastViewedAt) : new Date() ), }); diff --git a/app/components/Editor.tsx b/app/components/Editor.tsx index a674f0367..1445d6de0 100644 --- a/app/components/Editor.tsx +++ b/app/components/Editor.tsx @@ -1,4 +1,3 @@ -import { formatDistanceToNow } from "date-fns"; import { deburr, difference, sortBy } from "lodash"; import { observer } from "mobx-react"; import { DOMParser as ProsemirrorDOMParser } from "prosemirror-model"; @@ -10,6 +9,7 @@ import { Optional } from "utility-types"; import insertFiles from "@shared/editor/commands/insertFiles"; import { AttachmentPreset } from "@shared/types"; import { Heading } from "@shared/utils/ProsemirrorHelper"; +import { dateLocale, dateToRelative } from "@shared/utils/date"; import { getDataTransferFiles } from "@shared/utils/files"; import parseDocumentSlug from "@shared/utils/parseDocumentSlug"; import { isInternalUrl } from "@shared/utils/urls"; @@ -23,6 +23,7 @@ import useDictionary from "~/hooks/useDictionary"; import useEmbeds from "~/hooks/useEmbeds"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; +import useUserLocale from "~/hooks/useUserLocale"; import { NotFoundError } from "~/utils/errors"; import { uploadFile } from "~/utils/files"; import { isModKey } from "~/utils/keyboard"; @@ -60,6 +61,8 @@ function Editor(props: Props, ref: React.RefObject | null) { onCreateCommentMark, onDeleteCommentMark, } = props; + const userLocale = useUserLocale(); + const locale = dateLocale(userLocale); const { auth, comments, documents } = useStores(); const { showToast } = useToasts(); const dictionary = useDictionary(); @@ -92,8 +95,10 @@ function Editor(props: Props, ref: React.RefObject | null) { try { const document = await documents.fetch(slug); - const time = formatDistanceToNow(Date.parse(document.updatedAt), { + const time = dateToRelative(Date.parse(document.updatedAt), { addSuffix: true, + shorten: true, + locale, }); return [ diff --git a/app/components/LocaleTime.tsx b/app/components/LocaleTime.tsx index ee6262d41..f32a3af50 100644 --- a/app/components/LocaleTime.tsx +++ b/app/components/LocaleTime.tsx @@ -1,6 +1,6 @@ -import { format as formatDate, formatDistanceToNow } from "date-fns"; +import { format as formatDate } from "date-fns"; import * as React from "react"; -import { dateLocale, locales } from "@shared/utils/date"; +import { dateLocale, dateToRelative, locales } from "@shared/utils/date"; import Tooltip from "~/components/Tooltip"; import useUserLocale from "~/hooks/useUserLocale"; @@ -60,26 +60,21 @@ const LocaleTime: React.FC = ({ }; }, []); + const date = new Date(Date.parse(dateTime)); const locale = dateLocale(userLocale); - let relativeContent = formatDistanceToNow(Date.parse(dateTime), { + const relativeContent = dateToRelative(date, { addSuffix, locale, + shorten, }); - if (shorten) { - relativeContent = relativeContent - .replace("about", "") - .replace("less than a minute ago", "just now") - .replace("minute", "min"); - } - - const tooltipContent = formatDate(Date.parse(dateTime), formatLocaleLong, { + const tooltipContent = formatDate(date, formatLocaleLong, { locale, }); const content = relative !== false ? relativeContent - : formatDate(Date.parse(dateTime), formatLocale, { + : formatDate(date, formatLocale, { locale, }); diff --git a/app/components/Time.tsx b/app/components/Time.tsx index 29247531e..99336de4c 100644 --- a/app/components/Time.tsx +++ b/app/components/Time.tsx @@ -1,5 +1,5 @@ -import { formatDistanceToNow } from "date-fns"; import * as React from "react"; +import { dateToRelative } from "@shared/utils/date"; import lazyWithRetry from "~/utils/lazyWithRetry"; const LocaleTime = lazyWithRetry(() => import("~/components/LocaleTime")); @@ -9,17 +9,11 @@ type Props = React.ComponentProps & { }; function Time({ onClick, ...props }: Props) { - let content = formatDistanceToNow(Date.parse(props.dateTime), { + const content = dateToRelative(Date.parse(props.dateTime), { addSuffix: props.addSuffix, + shorten: props.shorten, }); - if (props.shorten) { - content = content - .replace("about", "") - .replace("less than a minute ago", "just now") - .replace("minute", "min"); - } - return ( (); const inputRef = React.useRef(null); + const inputReplaceRef = React.useRef(null); const { t } = useTranslation(); const theme = useTheme(); const [showReplace, setShowReplace] = React.useState(false); @@ -102,10 +103,10 @@ export default function FindAndReplace({ readOnly }: Props) { ); // Callbacks - const handleMore = React.useCallback( - () => setShowReplace((state) => !state), - [] - ); + const handleMore = React.useCallback(() => { + setShowReplace((state) => !state); + setTimeout(() => inputReplaceRef.current?.focus(), 100); + }, []); const handleCaseSensitive = React.useCallback(() => { setCaseSensitive((state) => { @@ -306,12 +307,12 @@ export default function FindAndReplace({ readOnly }: Props) { {navigation} {!readOnly && ( - + )} @@ -322,6 +323,7 @@ export default function FindAndReplace({ readOnly }: Props) { ) : ( - + {item.label && } {item.icon} diff --git a/app/editor/menus/divider.tsx b/app/editor/menus/divider.tsx index 8a9576004..495b56942 100644 --- a/app/editor/menus/divider.tsx +++ b/app/editor/menus/divider.tsx @@ -12,13 +12,6 @@ export default function dividerMenuItems( const { schema } = state; return [ - { - name: "hr", - tooltip: dictionary.pageBreak, - attrs: { markup: "***" }, - active: isNodeActive(schema.nodes.hr, { markup: "***" }), - icon: , - }, { name: "hr", tooltip: dictionary.hr, @@ -26,5 +19,12 @@ export default function dividerMenuItems( active: isNodeActive(schema.nodes.hr, { markup: "---" }), icon: , }, + { + name: "hr", + tooltip: dictionary.pageBreak, + attrs: { markup: "***" }, + active: isNodeActive(schema.nodes.hr, { markup: "***" }), + icon: , + }, ]; } diff --git a/app/scenes/Document/components/CommentThreadItem.tsx b/app/scenes/Document/components/CommentThreadItem.tsx index 12ada7914..204ff0132 100644 --- a/app/scenes/Document/components/CommentThreadItem.tsx +++ b/app/scenes/Document/components/CommentThreadItem.tsx @@ -1,4 +1,4 @@ -import { differenceInMilliseconds, formatDistanceToNow } from "date-fns"; +import { differenceInMilliseconds } from "date-fns"; import { toJS } from "mobx"; import { observer } from "mobx-react"; import { darken } from "polished"; @@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next"; import styled, { css } from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { s } from "@shared/styles"; +import { dateToRelative } from "@shared/utils/date"; import { Minute } from "@shared/utils/time"; import Comment from "~/models/Comment"; import Avatar from "~/components/Avatar"; @@ -37,9 +38,9 @@ function useShowTime( } const previousTimeStamp = previousCreatedAt - ? formatDistanceToNow(Date.parse(previousCreatedAt)) + ? dateToRelative(Date.parse(previousCreatedAt)) : undefined; - const currentTimeStamp = formatDistanceToNow(Date.parse(createdAt)); + const currentTimeStamp = dateToRelative(Date.parse(createdAt)); const msSincePreviousComment = previousCreatedAt ? differenceInMilliseconds( diff --git a/app/scenes/Document/components/SharePopover.tsx b/app/scenes/Document/components/SharePopover.tsx index 6e3f6ca55..a20775f36 100644 --- a/app/scenes/Document/components/SharePopover.tsx +++ b/app/scenes/Document/components/SharePopover.tsx @@ -1,4 +1,3 @@ -import { formatDistanceToNow } from "date-fns"; import invariant from "invariant"; import { debounce, isEmpty } from "lodash"; import { observer } from "mobx-react"; @@ -8,7 +7,7 @@ import { useTranslation, Trans } from "react-i18next"; import { Link } from "react-router-dom"; import styled from "styled-components"; import { s } from "@shared/styles"; -import { dateLocale } from "@shared/utils/date"; +import { dateLocale, dateToRelative } from "@shared/utils/date"; import { SHARE_URL_SLUG_REGEX } from "@shared/utils/urlHelpers"; import Document from "~/models/Document"; import Share from "~/models/Share"; @@ -193,13 +192,10 @@ function SharePopover({ <> .{" "} {t("The shared link was last accessed {{ timeAgo }}.", { - timeAgo: formatDistanceToNow( - Date.parse(share?.lastAccessedAt), - { - addSuffix: true, - locale, - } - ), + timeAgo: dateToRelative(Date.parse(share?.lastAccessedAt), { + addSuffix: true, + locale, + }), })} )} diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 78253923f..28f561fa3 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -253,7 +253,7 @@ "Find": "Find", "Match case": "Match case", "Enable regex": "Enable regex", - "More options": "More options", + "Replace options": "Replace options", "Replacement": "Replacement", "Replace": "Replace", "Replace all": "Replace all", @@ -552,6 +552,7 @@ "All users see the same publicly shared view": "All users see the same publicly shared view", "Custom link": "Custom link", "The document will be accessible at <2>{{url}}": "The document will be accessible at <2>{{url}}", + "More options": "More options", "Close": "Close", "{{ teamName }} is using {{ appName }} to share documents, please login to continue.": "{{ teamName }} is using {{ appName }} to share documents, please login to continue.", "Are you sure you want to delete the {{ documentTitle }} template?": "Are you sure you want to delete the {{ documentTitle }} template?", diff --git a/shared/utils/date.ts b/shared/utils/date.ts index eed171f37..9e0f99f66 100644 --- a/shared/utils/date.ts +++ b/shared/utils/date.ts @@ -1,5 +1,12 @@ /* eslint-disable import/no-duplicates */ -import { subDays, subMonths, subWeeks, subYears } from "date-fns"; +import { + addSeconds, + formatDistanceToNow, + subDays, + subMonths, + subWeeks, + subYears, +} from "date-fns"; import { cs, de, @@ -40,6 +47,44 @@ export function subtractDate(date: Date, period: DateFilter) { } } +/** + * Returns a humanized relative time string for the given date. + * + * @param date The date to convert + * @param options The options to pass to date-fns + * @returns The relative time string + */ +export function dateToRelative( + date: Date | number, + options?: { + includeSeconds?: boolean; + addSuffix?: boolean; + locale?: Locale | undefined; + shorten?: boolean; + } +) { + const now = new Date(); + const parsedDateTime = new Date(date); + + // Protect against "in less than a minute" when users computer clock is off. + const normalizedDateTime = + parsedDateTime > now && parsedDateTime < addSeconds(now, 60) + ? now + : parsedDateTime; + + const output = formatDistanceToNow(normalizedDateTime, options); + + // Some tweaks to make english language shorter. + if (options?.shorten) { + return output + .replace("about", "") + .replace("less than a minute ago", "just now") + .replace("minute", "min"); + } + + return output; +} + /** * Converts a locale string from Unicode CLDR format to BCP47 format. *