diff --git a/src/features/labels/Ago.test.ts b/src/features/labels/Ago.test.ts index b355332034..1c7c5479be 100644 --- a/src/features/labels/Ago.test.ts +++ b/src/features/labels/Ago.test.ts @@ -1,9 +1,9 @@ import { subDays, subMinutes, subMonths, subSeconds, subYears } from "date-fns"; import { describe, expect, it } from "vitest"; -import { formatRelative } from "./Ago"; +import { formatRelativeToNow } from "./Ago"; -describe("formatRelative Function", () => { +describe("formatRelativeToNow Function", () => { const currentTime = { date: new Date(), expected: { @@ -82,17 +82,17 @@ describe("formatRelative Function", () => { testCases.forEach(({ name, date, expected }) => { it(`should format ${name} correctly in ultrashort format`, () => { - const result = formatRelative(date, "ultrashort"); + const result = formatRelativeToNow(date, "ultrashort"); expect(result).toBe(expected.ultrashort); }); it(`should format ${name} correctly in short format`, () => { - const result = formatRelative(date, "short"); + const result = formatRelativeToNow(date, "short"); expect(result).toBe(expected.short); }); it(`should format ${name} correctly in verbose format`, () => { - const result = formatRelative(date, "verbose"); + const result = formatRelativeToNow(date, "verbose"); expect(result).toBe(expected.verbose); }); }); diff --git a/src/features/labels/Ago.tsx b/src/features/labels/Ago.tsx index 32d36c0100..e824e42526 100644 --- a/src/features/labels/Ago.tsx +++ b/src/features/labels/Ago.tsx @@ -4,25 +4,48 @@ import { round } from "es-toolkit"; import { formatDistanceShort } from "./formatDistanceShort"; interface AgoProps { + /** @default now */ + to?: string; date: string; + as?: "ultrashort" | "short" | "verbose"; shorthand?: boolean; + className?: string; } -export default function Ago({ date, className, as = "ultrashort" }: AgoProps) { - return ( - {formatRelative(new Date(date), as)} - ); +export default function Ago({ + date: dateStr, + className, + as = "ultrashort", + to: toStr, +}: AgoProps) { + const to = toStr ? new Date(toStr) : new Date(); + const date = new Date(dateStr); + + const duration = intervalToDuration({ start: date, end: to }); + + return {formatRelative(duration, as)}; } -export function formatRelative( +export function formatRelativeToNow( date: Date, + ...args: Parameters extends [unknown, ...infer Rest] + ? Rest + : never +) { + const duration = intervalToDuration({ + start: date, + end: new Date(), + }); + + return formatRelative(duration, ...args); +} + +function formatRelative( + duration: Duration, as: AgoProps["as"] = "ultrashort", ): string { - const now = new Date(); - const duration = intervalToDuration({ start: date, end: now }); - let distance: string; switch (as) { diff --git a/src/features/labels/Edited.tsx b/src/features/labels/Edited.tsx index 0952749cba..9248c45636 100644 --- a/src/features/labels/Edited.tsx +++ b/src/features/labels/Edited.tsx @@ -5,7 +5,7 @@ import { MouseEvent } from "react"; import Stat from "#/features/post/detail/Stat"; -import { formatRelative } from "./Ago"; +import { formatRelativeToNow } from "./Ago"; import styles from "./Edited.module.css"; @@ -24,8 +24,8 @@ export default function Edited({ item, showDate, className }: EditedProps) { if (!edited) return; if (!showDate) return; - const createdLabel = formatRelative(new Date(item.counts.published)); - const editedLabel = formatRelative(new Date(edited)); + const createdLabel = formatRelativeToNow(new Date(item.counts.published)); + const editedLabel = formatRelativeToNow(new Date(edited)); if (createdLabel === editedLabel) return; @@ -42,7 +42,7 @@ export default function Edited({ item, showDate, className }: EditedProps) { const date = new Date(edited); present({ - header: `Edited ${formatRelative(date)} Ago`, + header: `Edited ${formatRelativeToNow(date)} Ago`, message: `Last edited on ${date.toDateString()} at ${date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`, buttons: ["OK"], }); diff --git a/src/features/moderation/logs/ModRolePersonLink.module.css b/src/features/moderation/logs/ModRolePersonLink.module.css index 3f606334b2..bbb4b8e6e0 100644 --- a/src/features/moderation/logs/ModRolePersonLink.module.css +++ b/src/features/moderation/logs/ModRolePersonLink.module.css @@ -1,5 +1,5 @@ .icon { font-size: 1.2em; vertical-align: middle; - transform: translateY(-1.5px); + transform: translateY(-1px); } diff --git a/src/features/moderation/logs/ModlogItem.module.css b/src/features/moderation/logs/ModlogItem.module.css index e15cf51bc2..2c589945e5 100644 --- a/src/features/moderation/logs/ModlogItem.module.css +++ b/src/features/moderation/logs/ModlogItem.module.css @@ -68,3 +68,7 @@ color: var(--ion-color-medium2); } } + +.agoIcon { + font-size: 1.2em; +} diff --git a/src/features/moderation/logs/ModlogItem.tsx b/src/features/moderation/logs/ModlogItem.tsx index 0d75e97519..adc7a3943d 100644 --- a/src/features/moderation/logs/ModlogItem.tsx +++ b/src/features/moderation/logs/ModlogItem.tsx @@ -1,16 +1,23 @@ import { IonIcon, IonItem } from "@ionic/react"; import { timerOutline } from "ionicons/icons"; import { Person } from "lemmy-js-client"; +import { useCallback } from "react"; +import { useRef } from "react"; +import { useLongPress } from "use-long-press"; import Ago from "#/features/labels/Ago"; import { cx } from "#/helpers/css"; import { isTouchDevice } from "#/helpers/device"; +import { stopIonicTapClick } from "#/helpers/ionic"; +import { filterEvents } from "#/helpers/longPress"; import { useBuildGeneralBrowseLink } from "#/helpers/routes"; import { ModeratorRole } from "../useCanModerate"; import useIsAdmin from "../useIsAdmin"; import { ModlogItemType } from "./helpers"; -import ModlogItemMoreActions from "./ModlogItemMoreActions"; +import ModlogItemMoreActions, { + ModlogItemMoreActionsHandle, +} from "./ModlogItemMoreActions"; import ModRolePersonLink from "./ModRolePersonLink"; import addCommunity from "./types/addCommunity"; import addInstance from "./types/addInstance"; @@ -116,6 +123,19 @@ export function ModlogItem({ item }: ModLogItemProps) { return role_ ?? "mod"; })(); + const ellipsisHandleRef = useRef(null); + + const onCommentLongPress = useCallback(() => { + ellipsisHandleRef.current?.present(); + stopIonicTapClick(); + }, []); + + const bind = useLongPress(onCommentLongPress, { + threshold: 800, + cancelOnMovement: 15, + filterEvents, + }); + return (
@@ -135,7 +156,11 @@ export function ModlogItem({ item }: ModLogItemProps) {
{title}
@@ -145,7 +170,8 @@ export function ModlogItem({ item }: ModLogItemProps) { {by && } {expires && ( )}
diff --git a/src/features/moderation/logs/ModlogItemMoreActions.tsx b/src/features/moderation/logs/ModlogItemMoreActions.tsx index 1fca6be201..fe56bef764 100644 --- a/src/features/moderation/logs/ModlogItemMoreActions.tsx +++ b/src/features/moderation/logs/ModlogItemMoreActions.tsx @@ -5,6 +5,7 @@ import { peopleOutline, personOutline, } from "ionicons/icons"; +import { useCallback, useImperativeHandle } from "react"; import { getHandle } from "#/helpers/lemmy"; import useAppNavigation from "#/helpers/useAppNavigation"; @@ -17,11 +18,17 @@ import styles from "./ModlogItemMoreActions.module.css"; interface ModlogItemMoreActions { item: ModlogItemType; role: ModeratorRole; + ref: React.RefObject; +} + +export interface ModlogItemMoreActionsHandle { + present: () => void; } export default function ModlogItemMoreActions({ item, role, + ref, }: ModlogItemMoreActions) { const { navigateToCommunity, navigateToUser } = useAppNavigation(); const [presentActionSheet] = useIonActionSheet(); @@ -39,7 +46,7 @@ export default function ModlogItemMoreActions({ if ("admin" in item) return item.admin; })(); - function presentMoreActions() { + const present = useCallback(() => { presentActionSheet({ cssClass: "left-align-buttons", buttons: compact([ @@ -77,7 +84,23 @@ export default function ModlogItemMoreActions({ }, ]), }); - } + }, [ + community, + moderator, + navigateToCommunity, + navigateToUser, + person, + presentActionSheet, + role, + ]); + + useImperativeHandle( + ref, + () => ({ + present, + }), + [present], + ); return ( diff --git a/src/features/user/Scores.tsx b/src/features/user/Scores.tsx index af94359c44..2b9c309ac6 100644 --- a/src/features/user/Scores.tsx +++ b/src/features/user/Scores.tsx @@ -1,7 +1,7 @@ import { useIonAlert } from "@ionic/react"; import { PersonAggregates } from "lemmy-js-client"; -import Ago, { formatRelative } from "#/features/labels/Ago"; +import Ago, { formatRelativeToNow } from "#/features/labels/Ago"; import { formatNumber } from "#/helpers/number"; import styles from "./Scores.module.css"; @@ -66,7 +66,7 @@ export default function Scores({ aggregates, accountCreated }: ScoreProps) { className={styles.score} onClick={() => { present({ - header: `Account is ${formatRelative(creationDate, "verbose")} old`, + header: `Account is ${formatRelativeToNow(creationDate, "verbose")} old`, message: `Created on ${creationDate.toDateString()} at ${creationDate.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`, buttons: [{ text: "OK" }], });