mirror of
https://github.com/usememos/memos.git
synced 2025-02-16 03:12:13 +01:00
feat: use @github/relative-time-element to display time
This commit is contained in:
parent
e795149186
commit
ded4da07a3
@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@github/relative-time-element": "^4.3.1",
|
||||
"@matejmazur/react-katex": "^3.1.3",
|
||||
"@mui/joy": "5.0.0-beta.30",
|
||||
"@reduxjs/toolkit": "^1.9.7",
|
||||
|
7
web/pnpm-lock.yaml
generated
7
web/pnpm-lock.yaml
generated
@ -14,6 +14,9 @@ dependencies:
|
||||
'@emotion/styled':
|
||||
specifier: ^11.11.0
|
||||
version: 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.63)(react@18.2.0)
|
||||
'@github/relative-time-element':
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
'@matejmazur/react-katex':
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3(katex@0.16.9)(react@18.2.0)
|
||||
@ -905,6 +908,10 @@ packages:
|
||||
resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
|
||||
dev: false
|
||||
|
||||
/@github/relative-time-element@4.3.1:
|
||||
resolution: {integrity: sha512-zL79nlhZVCg7x2Pf/HT5MB0mowmErE71VXpF10/3Wy8dQwkninNO1M9aOizh2wKC5LkSpDXqNYjDZwbH0/bcSg==}
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.14:
|
||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const BetaBadge: React.FC<Props> = (props: Props) => {
|
||||
const { className } = props;
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`mx-1 px-1 leading-5 text-xs border font-normal dark:border-zinc-600 rounded-full text-gray-500 dark:text-gray-400 ${
|
||||
className ?? ""
|
||||
}`}
|
||||
>
|
||||
{t("common.beta")}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default BetaBadge;
|
@ -1,7 +1,9 @@
|
||||
import { Dropdown, Menu, MenuButton, MenuItem } from "@mui/joy";
|
||||
import classNames from "classnames";
|
||||
import toast from "react-hot-toast";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import Icon from "@/components/Icon";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { useMemoStore } from "@/store/v1";
|
||||
import { RowStatus } from "@/types/proto/api/v2/common";
|
||||
import { Memo } from "@/types/proto/api/v2/memo_service";
|
||||
@ -14,14 +16,15 @@ interface Props {
|
||||
memo: Memo;
|
||||
className?: string;
|
||||
hiddenActions?: ("edit" | "archive" | "delete" | "share" | "pin")[];
|
||||
onArchived?: () => void;
|
||||
onDeleted?: () => void;
|
||||
}
|
||||
|
||||
const MemoActionMenu = (props: Props) => {
|
||||
const { memo, hiddenActions } = props;
|
||||
const t = useTranslate();
|
||||
const location = useLocation();
|
||||
const navigateTo = useNavigateTo();
|
||||
const memoStore = useMemoStore();
|
||||
const isInMemoDetailPage = location.pathname.startsWith(`/m/${memo.name}`);
|
||||
|
||||
const handleTogglePinMemoBtnClick = async () => {
|
||||
try {
|
||||
@ -66,9 +69,12 @@ const MemoActionMenu = (props: Props) => {
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toast.error(error.response.data.message);
|
||||
return;
|
||||
}
|
||||
if (props.onArchived) {
|
||||
props.onArchived();
|
||||
|
||||
toast.success("Archived successfully");
|
||||
if (isInMemoDetailPage) {
|
||||
navigateTo("/archived");
|
||||
}
|
||||
};
|
||||
|
||||
@ -80,8 +86,9 @@ const MemoActionMenu = (props: Props) => {
|
||||
dialogName: "delete-memo-dialog",
|
||||
onConfirm: async () => {
|
||||
await memoStore.deleteMemo(memo.id);
|
||||
if (props.onDeleted) {
|
||||
props.onDeleted();
|
||||
toast.success("Deleted successfully");
|
||||
if (isInMemoDetailPage) {
|
||||
navigateTo("/");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -2,7 +2,6 @@ import { useContext, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import Icon from "@/components/Icon";
|
||||
import MemoResourceListView from "@/components/MemoResourceListView";
|
||||
import { getDateTimeString } from "@/helpers/datetime";
|
||||
import useLoading from "@/hooks/useLoading";
|
||||
import { useMemoStore } from "@/store/v1";
|
||||
import MemoContent from "..";
|
||||
@ -51,7 +50,9 @@ const EmbeddedMemo = ({ resourceId, params: paramsStr }: Props) => {
|
||||
return (
|
||||
<div className="relative flex flex-col justify-start items-start w-full p-4 pt-3 !my-2 bg-white dark:bg-zinc-800 rounded-lg border border-gray-200 dark:border-zinc-700 hover:shadow">
|
||||
<div className="w-full mb-1 flex flex-row justify-between items-center">
|
||||
<span className="text-sm text-gray-400 select-none">{getDateTimeString(memo.displayTime)}</span>
|
||||
<div className="text-sm leading-6 text-gray-400 select-none">
|
||||
<relative-time datetime={memo.displayTime?.toISOString()} tense="past"></relative-time>
|
||||
</div>
|
||||
<Link className="hover:opacity-80" to={`/m/${memo.name}`} unstable_viewTransition>
|
||||
<Icon.ArrowUpRight className="w-5 h-auto opacity-80 text-gray-400" />
|
||||
</Link>
|
||||
|
@ -74,7 +74,7 @@ const MemoContent: React.FC<Props> = (props: Props) => {
|
||||
<div
|
||||
ref={memoContentContainerRef}
|
||||
className={classNames(
|
||||
"w-full max-w-full word-break text-base leading-6 space-y-1 whitespace-pre-wrap",
|
||||
"w-full max-w-full word-break text-base leading-7 space-y-1 whitespace-pre-wrap",
|
||||
showCompactMode && "line-clamp-6",
|
||||
)}
|
||||
onClick={handleMemoContentClick}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { Tooltip } from "@mui/joy";
|
||||
import classNames from "classnames";
|
||||
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { getRelativeTimeString, getTimeStampByDate } from "@/helpers/datetime";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { useUserStore, extractUsernameFromName } from "@/store/v1";
|
||||
@ -25,6 +23,7 @@ import VisibilityIcon from "./VisibilityIcon";
|
||||
|
||||
interface Props {
|
||||
memo: Memo;
|
||||
compact?: boolean;
|
||||
showVisibility?: boolean;
|
||||
showPinned?: boolean;
|
||||
className?: string;
|
||||
@ -33,12 +32,11 @@ interface Props {
|
||||
const MemoView: React.FC<Props> = (props: Props) => {
|
||||
const { memo, className } = props;
|
||||
const t = useTranslate();
|
||||
const location = useLocation();
|
||||
const navigateTo = useNavigateTo();
|
||||
const { i18n } = useTranslation();
|
||||
const currentUser = useCurrentUser();
|
||||
const userStore = useUserStore();
|
||||
const user = useCurrentUser();
|
||||
const [displayTime, setDisplayTime] = useState<string>(getRelativeTimeString(getTimeStampByDate(memo.displayTime)));
|
||||
const [creator, setCreator] = useState(userStore.getUserByUsername(extractUsernameFromName(memo.creator)));
|
||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||
const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);
|
||||
@ -46,6 +44,7 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
||||
(relation) => relation.type === MemoRelation_Type.COMMENT && relation.relatedMemoId === memo.id,
|
||||
).length;
|
||||
const readonly = memo.creator !== user?.name;
|
||||
const isInMemoDetailPage = location.pathname.startsWith(`/m/${memo.name}`);
|
||||
|
||||
// Initial related data: creator.
|
||||
useEffect(() => {
|
||||
@ -55,20 +54,6 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// Update display time string.
|
||||
useEffect(() => {
|
||||
let intervalFlag: any = -1;
|
||||
if (Date.now() - getTimeStampByDate(memo.displayTime) < 1000 * 60 * 60 * 24) {
|
||||
intervalFlag = setInterval(() => {
|
||||
setDisplayTime(getRelativeTimeString(getTimeStampByDate(memo.displayTime)));
|
||||
}, 1000 * 1);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalFlag);
|
||||
};
|
||||
}, [i18n.language]);
|
||||
|
||||
const handleGotoMemoDetailPage = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (event.altKey) {
|
||||
showChangeMemoCreatedTsDialog(memo.id);
|
||||
@ -124,8 +109,8 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-1">
|
||||
<div className="w-auto invisible group-hover:visible flex flex-row justify-between items-center gap-1">
|
||||
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
|
||||
<div className="w-auto invisible group-hover:visible flex flex-row justify-between items-center gap-2">
|
||||
{props.showVisibility && memo.visibility !== Visibility.PRIVATE && (
|
||||
<Tooltip title={t(`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}` as any)} placement="top">
|
||||
<span className="flex justify-center items-center hover:opacity-70">
|
||||
@ -133,25 +118,27 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentUser && <ReactionSelector className="border-none" memo={memo} />}
|
||||
{currentUser && <ReactionSelector className="border-none w-auto h-auto" memo={memo} />}
|
||||
</div>
|
||||
<Link
|
||||
className={classNames(
|
||||
"flex flex-row justify-start items-center hover:opacity-70",
|
||||
commentAmount === 0 && "invisible group-hover:visible",
|
||||
)}
|
||||
to={`/m/${memo.name}#comments`}
|
||||
unstable_viewTransition
|
||||
>
|
||||
<Icon.MessageCircleMore className="w-4 h-4 mx-auto text-gray-500 dark:text-gray-400" />
|
||||
{commentAmount > 0 && <span className="text-xs text-gray-500 dark:text-gray-400">{commentAmount}</span>}
|
||||
</Link>
|
||||
{!isInMemoDetailPage && (
|
||||
<Link
|
||||
className={classNames(
|
||||
"flex flex-row justify-start items-center hover:opacity-70",
|
||||
commentAmount === 0 && "invisible group-hover:visible",
|
||||
)}
|
||||
to={`/m/${memo.name}#comments`}
|
||||
unstable_viewTransition
|
||||
>
|
||||
<Icon.MessageCircleMore className="w-4 h-4 mx-auto text-gray-500 dark:text-gray-400" />
|
||||
{commentAmount > 0 && <span className="text-xs text-gray-500 dark:text-gray-400">{commentAmount}</span>}
|
||||
</Link>
|
||||
)}
|
||||
{props.showPinned && memo.pinned && (
|
||||
<Tooltip title={"Pinned"} placement="top">
|
||||
<Icon.Bookmark className="ml-1 w-4 h-auto text-amber-500" />
|
||||
<Icon.Bookmark className="w-4 h-auto text-amber-500" />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!readonly && <MemoActionMenu memo={memo} hiddenActions={props.showPinned ? [] : ["pin"]} />}
|
||||
{!readonly && <MemoActionMenu className="-ml-1" memo={memo} hiddenActions={props.showPinned ? [] : ["pin"]} />}
|
||||
</div>
|
||||
</div>
|
||||
<MemoContent
|
||||
@ -160,13 +147,13 @@ const MemoView: React.FC<Props> = (props: Props) => {
|
||||
content={memo.content}
|
||||
readonly={readonly}
|
||||
onClick={handleMemoContentClick}
|
||||
compact={true}
|
||||
compact={props.compact ?? true}
|
||||
/>
|
||||
<MemoResourceListView resources={memo.resources} />
|
||||
<div className="w-full flex flex-row justify-between items-center mt-1">
|
||||
<span className="text-sm leading-6 text-gray-400 select-none" onClick={handleGotoMemoDetailPage}>
|
||||
{displayTime}
|
||||
</span>
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<div className="text-sm leading-6 text-gray-400 select-none">
|
||||
<relative-time datetime={memo.displayTime?.toISOString()} tense="past" onClick={handleGotoMemoDetailPage}></relative-time>
|
||||
</div>
|
||||
</div>
|
||||
<MemoRelationListView memo={memo} relations={referencedMemos} />
|
||||
<MemoReactionistView memo={memo} reactions={memo.reactions} />
|
||||
|
@ -4,13 +4,6 @@ export function getTimeStampByDate(t: Date | number | string | any): number {
|
||||
return new Date(t).getTime();
|
||||
}
|
||||
|
||||
export function getDateStampByDate(t?: Date | number | string): number {
|
||||
const tsFromDate = getTimeStampByDate(t ? t : Date.now());
|
||||
const d = new Date(tsFromDate);
|
||||
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a time string to provided time.
|
||||
*
|
||||
@ -57,56 +50,6 @@ export function getDateTimeString(t?: Date | number | string | any, locale = i18
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a localized relative time string to provided time.
|
||||
*
|
||||
* Possible outputs for "long" format and "en" locale:
|
||||
* - "x seconds ago"
|
||||
* - "x minutes ago"
|
||||
* - "x hours ago"
|
||||
* - "yesterday"
|
||||
* - "x days ago"
|
||||
* - "x weeks ago"
|
||||
* - "x months ago"
|
||||
* - "last year"
|
||||
* - "x years ago"
|
||||
*/
|
||||
export const getRelativeTimeString = (time: number, locale = i18n.language, formatStyle: "long" | "short" | "narrow" = "long"): string => {
|
||||
const pastTimeMillis = Date.now() - time;
|
||||
const secMillis = 1000;
|
||||
const minMillis = secMillis * 60;
|
||||
const hourMillis = minMillis * 60;
|
||||
const dayMillis = hourMillis * 24;
|
||||
// Show full date if more than 1 day ago.
|
||||
if (pastTimeMillis >= dayMillis) {
|
||||
return getDateTimeString(time, locale);
|
||||
}
|
||||
|
||||
// numeric: "auto" provides "yesterday" for 1 day ago, "always" provides "1 day ago"
|
||||
const formatOpts = { style: formatStyle, numeric: "auto" } as Intl.RelativeTimeFormatOptions;
|
||||
const relTime = new Intl.RelativeTimeFormat(locale, formatOpts);
|
||||
if (pastTimeMillis < minMillis) {
|
||||
return relTime.format(-Math.round(pastTimeMillis / secMillis), "second");
|
||||
}
|
||||
if (pastTimeMillis < hourMillis) {
|
||||
return relTime.format(-Math.round(pastTimeMillis / minMillis), "minute");
|
||||
}
|
||||
if (pastTimeMillis < dayMillis) {
|
||||
return relTime.format(-Math.round(pastTimeMillis / hourMillis), "hour");
|
||||
}
|
||||
if (pastTimeMillis < dayMillis * 7) {
|
||||
return relTime.format(-Math.round(pastTimeMillis / dayMillis), "day");
|
||||
}
|
||||
if (pastTimeMillis < dayMillis * 30) {
|
||||
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 7)), "week");
|
||||
}
|
||||
if (pastTimeMillis < dayMillis * 365) {
|
||||
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 30)), "month");
|
||||
}
|
||||
|
||||
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 365)), "year");
|
||||
};
|
||||
|
||||
/**
|
||||
* This returns the normalized date string of the provided date.
|
||||
* Format is always `YYYY-MM-DDT00:00`.
|
||||
@ -143,33 +86,6 @@ export function getNormalizedDateString(t?: Date | number | string): string {
|
||||
return `${yyyy}-${MM}-${dd}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the Unix timestamp (the number of **seconds** since the Unix Epoch) of the provided date.
|
||||
*
|
||||
* If no date is provided, the current date is used.
|
||||
* ```
|
||||
* getUnixTime("2019-01-25 00:00") // 1548381600
|
||||
* ```
|
||||
* This value is floored to the nearest second, and does not include a milliseconds component.
|
||||
*/
|
||||
export function getUnixTime(t?: Date | number | string): number {
|
||||
const date = new Date(t ? t : Date.now());
|
||||
return Math.floor(date.getTime() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided date or timestamp is in the future.
|
||||
*
|
||||
* If no date is provided, the current date is used.
|
||||
*
|
||||
* @param t - Date or timestamp to check.
|
||||
* @returns `true` if the date is in the future, `false` otherwise.
|
||||
*/
|
||||
export function isFutureDate(t?: Date | number | string): boolean {
|
||||
const timestamp = getTimeStampByDate(t ? t : Date.now());
|
||||
return timestamp > Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a new Date object by adjusting the provided date, timestamp, or date string
|
||||
* based on the current timezone offset.
|
||||
|
@ -1,3 +1,4 @@
|
||||
import "@github/relative-time-element";
|
||||
import { CssVarsProvider } from "@mui/joy";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
@ -11,7 +11,6 @@ import MobileHeader from "@/components/MobileHeader";
|
||||
import SearchBar from "@/components/SearchBar";
|
||||
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
|
||||
import { getTimeStampByDate } from "@/helpers/datetime";
|
||||
import { getDateTimeString } from "@/helpers/datetime";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import useFilterWithUrlParams from "@/hooks/useFilterWithUrlParams";
|
||||
import { useMemoList, useMemoStore } from "@/store/v1";
|
||||
@ -105,7 +104,9 @@ const Archived = () => {
|
||||
>
|
||||
<div className="w-full mb-1 flex flex-row justify-between items-center">
|
||||
<div className="w-full max-w-[calc(100%-20px)] flex flex-row justify-start items-center mr-1">
|
||||
<span className="text-sm text-gray-400 select-none">{getDateTimeString(memo.displayTime)}</span>
|
||||
<div className="text-sm leading-6 text-gray-400 select-none">
|
||||
<relative-time datetime={memo.displayTime?.toISOString()} tense="past"></relative-time>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end items-center gap-x-2">
|
||||
<Tooltip title={t("common.restore")} placement="top">
|
||||
|
@ -81,7 +81,14 @@ const MemoDetail = () => {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<MemoView key={`${memo.id}-${memo.displayTime}`} className="shadow hover:shadow-xl transition-all" memo={memo} />
|
||||
<MemoView
|
||||
key={`${memo.id}-${memo.displayTime}`}
|
||||
className="shadow hover:shadow-xl transition-all"
|
||||
memo={memo}
|
||||
compact={false}
|
||||
showVisibility
|
||||
showPinned
|
||||
/>
|
||||
<div className="pt-8 pb-16 w-full">
|
||||
<h2 id="comments" className="sr-only">
|
||||
Comments
|
||||
|
Loading…
x
Reference in New Issue
Block a user