memos/web/src/components/MemoView/hooks/useMemoViewState.ts

126 lines
4.1 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { instanceStore, memoStore, userStore } from "@/store";
import { State } from "@/types/proto/api/v1/common_pb";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
import { KEYBOARD_SHORTCUTS, TEXT_INPUT_TYPES } from "../constants";
interface ImagePreviewState {
open: boolean;
urls: string[];
index: number;
}
interface UseKeyboardShortcutsOptions {
enabled: boolean;
readonly: boolean;
showEditor: boolean;
isArchived: boolean;
onEdit: () => void;
onArchive: () => Promise<void>;
}
export const useMemoActions = (memo: Memo) => {
const t = useTranslate();
const isArchived = memo.state === State.ARCHIVED;
const archiveMemo = async () => {
if (isArchived) return;
try {
await memoStore.updateMemo({ name: memo.name, state: State.ARCHIVED }, ["state"]);
toast.success(t("message.archived-successfully"));
userStore.setStatsStateId();
} catch (error: unknown) {
console.error(error);
const err = error as { details?: string };
toast.error(err?.details || "Failed to archive memo");
}
};
const unpinMemo = async () => {
if (!memo.pinned) return;
await memoStore.updateMemo({ name: memo.name, pinned: false }, ["pinned"]);
};
return { archiveMemo, unpinMemo };
};
const isTextInputElement = (element: HTMLElement | null): boolean => {
if (!element) return false;
if (element.isContentEditable) return true;
if (element instanceof HTMLTextAreaElement) return true;
if (element instanceof HTMLInputElement) {
return TEXT_INPUT_TYPES.includes(element.type as (typeof TEXT_INPUT_TYPES)[number]);
}
return false;
};
export const useKeyboardShortcuts = (cardRef: React.RefObject<HTMLDivElement | null>, options: UseKeyboardShortcutsOptions) => {
const { enabled, readonly, showEditor, isArchived, onEdit, onArchive } = options;
useEffect(() => {
if (!enabled || readonly || showEditor || !cardRef.current) return;
const cardEl = cardRef.current;
const handleKeyDown = (event: KeyboardEvent) => {
const target = event.target as HTMLElement | null;
if (!cardEl.contains(target) || isTextInputElement(target)) return;
if (event.metaKey || event.ctrlKey || event.altKey) return;
const key = event.key.toLowerCase();
if (key === KEYBOARD_SHORTCUTS.EDIT) {
event.preventDefault();
onEdit();
} else if (key === KEYBOARD_SHORTCUTS.ARCHIVE && !isArchived) {
event.preventDefault();
onArchive();
}
};
cardEl.addEventListener("keydown", handleKeyDown);
return () => cardEl.removeEventListener("keydown", handleKeyDown);
}, [enabled, readonly, showEditor, isArchived, onEdit, onArchive, cardRef]);
};
export const useNsfwContent = (memo: Memo, initialShowNsfw?: boolean) => {
const [showNSFWContent, setShowNSFWContent] = useState(initialShowNsfw ?? false);
const instanceMemoRelatedSetting = instanceStore.state.memoRelatedSetting;
const nsfw =
instanceMemoRelatedSetting.enableBlurNsfwContent &&
memo.tags?.some((tag) => instanceMemoRelatedSetting.nsfwTags.some((nsfwTag) => tag === nsfwTag || tag.startsWith(`${nsfwTag}/`)));
return {
nsfw: nsfw ?? false,
showNSFWContent,
toggleNsfwVisibility: () => setShowNSFWContent((prev) => !prev),
};
};
export const useImagePreview = () => {
const [previewState, setPreviewState] = useState<ImagePreviewState>({ open: false, urls: [], index: 0 });
return {
previewState,
openPreview: (url: string) => setPreviewState({ open: true, urls: [url], index: 0 }),
setPreviewOpen: (open: boolean) => setPreviewState((prev) => ({ ...prev, open })),
};
};
export const useMemoCreator = (creatorName: string) => {
const [creator, setCreator] = useState(userStore.getUserByName(creatorName));
const fetchedRef = useRef(false);
useEffect(() => {
if (fetchedRef.current) return;
fetchedRef.current = true;
(async () => {
const user = await userStore.getOrFetchUserByName(creatorName);
setCreator(user);
})();
}, [creatorName]);
return creator;
};