fix: auth checks in reaction selector

This commit is contained in:
Johnny 2026-01-02 23:36:41 +08:00
parent ef8e3cfb99
commit d2acebcc53
6 changed files with 24 additions and 73 deletions

View File

@ -1,22 +1,28 @@
import { memo, useMemo, useRef, useState } from "react";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useUser } from "@/hooks/useUserQueries";
import { cn } from "@/lib/utils";
import { State } from "@/types/proto/api/v1/common_pb";
import { isSuperUser } from "@/utils/user";
import MemoEditor from "../MemoEditor";
import PreviewImageDialog from "../PreviewImageDialog";
import { MemoBody, MemoHeader } from "./components";
import { MEMO_CARD_BASE_CLASSES } from "./constants";
import { useImagePreview, useMemoActions, useMemoHandlers, useMemoViewDerivedState, useNsfwContent } from "./hooks";
import { useImagePreview, useMemoActions, useMemoHandlers, useNsfwContent } from "./hooks";
import { MemoViewContext } from "./MemoViewContext";
import type { MemoViewProps } from "./types";
const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
const { memo: memoData, className } = props;
const { memo: memoData, className, parentPage: parentPageProp } = props;
const cardRef = useRef<HTMLDivElement>(null);
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
const [showEditor, setShowEditor] = useState(false);
const currentUser = useCurrentUser();
const creator = useUser(memoData.creator).data;
const { isArchived, readonly, parentPage } = useMemoViewDerivedState(memoData, props.parentPage);
const isArchived = memoData.state === State.ARCHIVED;
const readonly = memoData.creator !== currentUser?.name && !isSuperUser(currentUser);
const parentPage = parentPageProp || "/";
const { nsfw, showNSFWContent, toggleNsfwVisibility } = useNsfwContent(memoData, props.showNsfwContent);
const { previewState, openPreview, setPreviewOpen } = useImagePreview();
const { unpinMemo } = useMemoActions(memoData, isArchived);
@ -37,11 +43,14 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
() => ({
memo: memoData,
creator,
currentUser,
parentPage,
isArchived,
readonly,
showNSFWContent,
nsfw,
}),
[memoData, creator, parentPage, showNSFWContent, nsfw],
[memoData, creator, currentUser, parentPage, isArchived, readonly, showNSFWContent, nsfw],
);
if (showEditor) {
@ -68,8 +77,6 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
onGotoDetail={handleGotoMemoDetailPage}
onUnpin={unpinMemo}
onToggleNsfwVisibility={toggleNsfwVisibility}
reactionSelectorOpen={reactionSelectorOpen}
onReactionSelectorOpenChange={setReactionSelectorOpen}
/>
<MemoBody

View File

@ -1,18 +1,18 @@
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { createContext, useContext } from "react";
import { useLocation } from "react-router-dom";
import useCurrentUser from "@/hooks/useCurrentUser";
import { State } from "@/types/proto/api/v1/common_pb";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import type { User } from "@/types/proto/api/v1/user_service_pb";
import { isSuperUser } from "@/utils/user";
import { RELATIVE_TIME_THRESHOLD_MS } from "./constants";
export interface MemoViewContextValue {
memo: Memo;
creator: User | undefined;
currentUser: User | undefined;
parentPage: string;
isArchived: boolean;
readonly: boolean;
showNSFWContent: boolean;
nsfw: boolean;
}
@ -28,12 +28,9 @@ export const useMemoViewContext = (): MemoViewContextValue => {
};
export const useMemoViewDerived = () => {
const { memo } = useMemoViewContext();
const { memo, isArchived, readonly } = useMemoViewContext();
const location = useLocation();
const currentUser = useCurrentUser();
const isArchived = memo.state === State.ARCHIVED;
const readonly = memo.creator !== currentUser?.name && !isSuperUser(currentUser);
const isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`);
const commentAmount = memo.relations.filter(

View File

@ -1,5 +1,6 @@
import { timestampDate } from "@bufbuild/protobuf/wkt";
import { BookmarkIcon, EyeOffIcon, MessageCircleMoreIcon } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import i18n from "@/i18n";
@ -23,13 +24,12 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
onGotoDetail,
onUnpin,
onToggleNsfwVisibility,
reactionSelectorOpen,
onReactionSelectorOpenChange,
}) => {
const t = useTranslate();
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
const { memo, creator, parentPage, showNSFWContent, nsfw } = useMemoViewContext();
const { isArchived, readonly, isInMemoDetailPage, commentAmount, relativeTimeFormat } = useMemoViewDerived();
const { memo, creator, currentUser, parentPage, isArchived, readonly, showNSFWContent, nsfw } = useMemoViewContext();
const { isInMemoDetailPage, commentAmount, relativeTimeFormat } = useMemoViewDerived();
const displayTime = isArchived ? (
(memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toLocaleString(i18n.language)
@ -43,7 +43,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
return (
<div className="w-full flex flex-row justify-between items-center gap-2">
{/* Left section: Creator info or time */}
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
{showCreator && creator ? (
<CreatorDisplay creator={creator} displayTime={displayTime} onGotoDetail={onGotoDetail} />
@ -52,18 +51,15 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
)}
</div>
{/* Right section: Actions */}
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
{/* Reaction selector */}
{!isArchived && (
{currentUser && !isArchived && (
<ReactionSelector
className={cn("border-none w-auto h-auto", reactionSelectorOpen && "block!", "hidden group-hover:block")}
memo={memo}
onOpenChange={onReactionSelectorOpenChange}
onOpenChange={setReactionSelectorOpen}
/>
)}
{/* Comment count link */}
{!isInMemoDetailPage && (
<Link
className={cn(
@ -79,7 +75,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
</Link>
)}
{/* Visibility icon */}
{showVisibility && memo.visibility !== Visibility.PRIVATE && (
<Tooltip>
<TooltipTrigger>
@ -93,7 +88,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
</Tooltip>
)}
{/* Pinned indicator */}
{showPinned && memo.pinned && (
<TooltipProvider>
<Tooltip>
@ -109,14 +103,12 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
</TooltipProvider>
)}
{/* NSFW hide button */}
{nsfw && showNSFWContent && onToggleNsfwVisibility && (
<span className="cursor-pointer">
<EyeOffIcon className="w-4 h-auto text-primary" onClick={onToggleNsfwVisibility} />
</span>
)}
{/* Action menu */}
<MemoActionMenu memo={memo} readonly={readonly} onEdit={onEdit} />
</div>
</div>

View File

@ -1,5 +1,4 @@
export { useImagePreview } from "./useImagePreview";
export { useMemoActions } from "./useMemoActions";
export { useMemoHandlers } from "./useMemoHandlers";
export { useMemoViewDerivedState } from "./useMemoViewDerivedState";
export { useNsfwContent } from "./useNsfwContent";

View File

@ -1,16 +0,0 @@
import { useLocation } from "react-router-dom";
import useCurrentUser from "@/hooks/useCurrentUser";
import { State } from "@/types/proto/api/v1/common_pb";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { isSuperUser } from "@/utils/user";
export const useMemoViewDerivedState = (memo: Memo, parentPageProp?: string) => {
const location = useLocation();
const user = useCurrentUser();
const isArchived = memo.state === State.ARCHIVED;
const readonly = memo.creator !== user?.name && !isSuperUser(user);
const parentPage = parentPageProp || location.pathname;
return { isArchived, readonly, parentPage };
};

View File

@ -1,56 +1,28 @@
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
/**
* Props for the MemoView component.
* MemoView is the main component for displaying a memo card with all its metadata,
* content, and interactive elements.
*/
export interface MemoViewProps {
/** The memo object to display */
memo: Memo;
/** Whether to show compact view (hides some metadata) */
compact?: boolean;
/** Whether to show the creator's profile information */
showCreator?: boolean;
/** Whether to show the visibility indicator */
showVisibility?: boolean;
/** Whether to show the pinned indicator */
showPinned?: boolean;
/** Whether to show NSFW content by default */
showNsfwContent?: boolean;
/** Additional CSS classes to apply to the root element */
className?: string;
/** The parent page URL for navigation context */
parentPage?: string;
}
/**
* Props for the MemoHeader component.
* Displays memo metadata like creator, timestamp, and action buttons.
*/
export interface MemoHeaderProps {
// Display options
showCreator?: boolean;
showVisibility?: boolean;
showPinned?: boolean;
// Callbacks
onEdit: () => void;
onGotoDetail: () => void;
onUnpin: () => void;
onToggleNsfwVisibility?: () => void;
// Reaction state
reactionSelectorOpen: boolean;
onReactionSelectorOpenChange: (open: boolean) => void;
}
/**
* Props for the MemoBody component.
* Displays memo content, attachments, and relations.
*/
export interface MemoBodyProps {
// Display options
compact?: boolean;
// Callbacks
onContentClick: (e: React.MouseEvent) => void;
onContentDoubleClick: (e: React.MouseEvent) => void;
onToggleNsfwVisibility: () => void;