Commit d2acebcc authored by Johnny's avatar Johnny

fix: auth checks in reaction selector

parent ef8e3cfb
import { memo, useMemo, useRef, useState } from "react"; import { memo, useMemo, useRef, useState } from "react";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useUser } from "@/hooks/useUserQueries"; import { useUser } from "@/hooks/useUserQueries";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { State } from "@/types/proto/api/v1/common_pb";
import { isSuperUser } from "@/utils/user";
import MemoEditor from "../MemoEditor"; import MemoEditor from "../MemoEditor";
import PreviewImageDialog from "../PreviewImageDialog"; import PreviewImageDialog from "../PreviewImageDialog";
import { MemoBody, MemoHeader } from "./components"; import { MemoBody, MemoHeader } from "./components";
import { MEMO_CARD_BASE_CLASSES } from "./constants"; 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 { MemoViewContext } from "./MemoViewContext";
import type { MemoViewProps } from "./types"; import type { MemoViewProps } from "./types";
const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => { 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 cardRef = useRef<HTMLDivElement>(null);
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
const [showEditor, setShowEditor] = useState(false); const [showEditor, setShowEditor] = useState(false);
const currentUser = useCurrentUser();
const creator = useUser(memoData.creator).data; 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 { nsfw, showNSFWContent, toggleNsfwVisibility } = useNsfwContent(memoData, props.showNsfwContent);
const { previewState, openPreview, setPreviewOpen } = useImagePreview(); const { previewState, openPreview, setPreviewOpen } = useImagePreview();
const { unpinMemo } = useMemoActions(memoData, isArchived); const { unpinMemo } = useMemoActions(memoData, isArchived);
...@@ -37,11 +43,14 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => { ...@@ -37,11 +43,14 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
() => ({ () => ({
memo: memoData, memo: memoData,
creator, creator,
currentUser,
parentPage, parentPage,
isArchived,
readonly,
showNSFWContent, showNSFWContent,
nsfw, nsfw,
}), }),
[memoData, creator, parentPage, showNSFWContent, nsfw], [memoData, creator, currentUser, parentPage, isArchived, readonly, showNSFWContent, nsfw],
); );
if (showEditor) { if (showEditor) {
...@@ -68,8 +77,6 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => { ...@@ -68,8 +77,6 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
onGotoDetail={handleGotoMemoDetailPage} onGotoDetail={handleGotoMemoDetailPage}
onUnpin={unpinMemo} onUnpin={unpinMemo}
onToggleNsfwVisibility={toggleNsfwVisibility} onToggleNsfwVisibility={toggleNsfwVisibility}
reactionSelectorOpen={reactionSelectorOpen}
onReactionSelectorOpenChange={setReactionSelectorOpen}
/> />
<MemoBody <MemoBody
......
import { timestampDate } from "@bufbuild/protobuf/wkt"; import { timestampDate } from "@bufbuild/protobuf/wkt";
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import { useLocation } from "react-router-dom"; 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 type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { MemoRelation_Type } 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 type { User } from "@/types/proto/api/v1/user_service_pb";
import { isSuperUser } from "@/utils/user";
import { RELATIVE_TIME_THRESHOLD_MS } from "./constants"; import { RELATIVE_TIME_THRESHOLD_MS } from "./constants";
export interface MemoViewContextValue { export interface MemoViewContextValue {
memo: Memo; memo: Memo;
creator: User | undefined; creator: User | undefined;
currentUser: User | undefined;
parentPage: string; parentPage: string;
isArchived: boolean;
readonly: boolean;
showNSFWContent: boolean; showNSFWContent: boolean;
nsfw: boolean; nsfw: boolean;
} }
...@@ -28,12 +28,9 @@ export const useMemoViewContext = (): MemoViewContextValue => { ...@@ -28,12 +28,9 @@ export const useMemoViewContext = (): MemoViewContextValue => {
}; };
export const useMemoViewDerived = () => { export const useMemoViewDerived = () => {
const { memo } = useMemoViewContext(); const { memo, isArchived, readonly } = useMemoViewContext();
const location = useLocation(); 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 isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`);
const commentAmount = memo.relations.filter( const commentAmount = memo.relations.filter(
......
import { timestampDate } from "@bufbuild/protobuf/wkt"; import { timestampDate } from "@bufbuild/protobuf/wkt";
import { BookmarkIcon, EyeOffIcon, MessageCircleMoreIcon } from "lucide-react"; import { BookmarkIcon, EyeOffIcon, MessageCircleMoreIcon } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import i18n from "@/i18n"; import i18n from "@/i18n";
...@@ -23,13 +24,12 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ ...@@ -23,13 +24,12 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
onGotoDetail, onGotoDetail,
onUnpin, onUnpin,
onToggleNsfwVisibility, onToggleNsfwVisibility,
reactionSelectorOpen,
onReactionSelectorOpenChange,
}) => { }) => {
const t = useTranslate(); const t = useTranslate();
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
const { memo, creator, parentPage, showNSFWContent, nsfw } = useMemoViewContext(); const { memo, creator, currentUser, parentPage, isArchived, readonly, showNSFWContent, nsfw } = useMemoViewContext();
const { isArchived, readonly, isInMemoDetailPage, commentAmount, relativeTimeFormat } = useMemoViewDerived(); const { isInMemoDetailPage, commentAmount, relativeTimeFormat } = useMemoViewDerived();
const displayTime = isArchived ? ( const displayTime = isArchived ? (
(memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toLocaleString(i18n.language) (memo.displayTime ? timestampDate(memo.displayTime) : undefined)?.toLocaleString(i18n.language)
...@@ -43,7 +43,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ ...@@ -43,7 +43,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
return ( return (
<div className="w-full flex flex-row justify-between items-center gap-2"> <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"> <div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
{showCreator && creator ? ( {showCreator && creator ? (
<CreatorDisplay creator={creator} displayTime={displayTime} onGotoDetail={onGotoDetail} /> <CreatorDisplay creator={creator} displayTime={displayTime} onGotoDetail={onGotoDetail} />
...@@ -52,18 +51,15 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ ...@@ -52,18 +51,15 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
)} )}
</div> </div>
{/* Right section: Actions */}
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2"> <div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
{/* Reaction selector */} {currentUser && !isArchived && (
{!isArchived && (
<ReactionSelector <ReactionSelector
className={cn("border-none w-auto h-auto", reactionSelectorOpen && "block!", "hidden group-hover:block")} className={cn("border-none w-auto h-auto", reactionSelectorOpen && "block!", "hidden group-hover:block")}
memo={memo} memo={memo}
onOpenChange={onReactionSelectorOpenChange} onOpenChange={setReactionSelectorOpen}
/> />
)} )}
{/* Comment count link */}
{!isInMemoDetailPage && ( {!isInMemoDetailPage && (
<Link <Link
className={cn( className={cn(
...@@ -79,7 +75,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ ...@@ -79,7 +75,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
</Link> </Link>
)} )}
{/* Visibility icon */}
{showVisibility && memo.visibility !== Visibility.PRIVATE && ( {showVisibility && memo.visibility !== Visibility.PRIVATE && (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
...@@ -93,7 +88,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ ...@@ -93,7 +88,6 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
</Tooltip> </Tooltip>
)} )}
{/* Pinned indicator */}
{showPinned && memo.pinned && ( {showPinned && memo.pinned && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
...@@ -109,14 +103,12 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ ...@@ -109,14 +103,12 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({
</TooltipProvider> </TooltipProvider>
)} )}
{/* NSFW hide button */}
{nsfw && showNSFWContent && onToggleNsfwVisibility && ( {nsfw && showNSFWContent && onToggleNsfwVisibility && (
<span className="cursor-pointer"> <span className="cursor-pointer">
<EyeOffIcon className="w-4 h-auto text-primary" onClick={onToggleNsfwVisibility} /> <EyeOffIcon className="w-4 h-auto text-primary" onClick={onToggleNsfwVisibility} />
</span> </span>
)} )}
{/* Action menu */}
<MemoActionMenu memo={memo} readonly={readonly} onEdit={onEdit} /> <MemoActionMenu memo={memo} readonly={readonly} onEdit={onEdit} />
</div> </div>
</div> </div>
......
export { useImagePreview } from "./useImagePreview"; export { useImagePreview } from "./useImagePreview";
export { useMemoActions } from "./useMemoActions"; export { useMemoActions } from "./useMemoActions";
export { useMemoHandlers } from "./useMemoHandlers"; export { useMemoHandlers } from "./useMemoHandlers";
export { useMemoViewDerivedState } from "./useMemoViewDerivedState";
export { useNsfwContent } from "./useNsfwContent"; export { useNsfwContent } from "./useNsfwContent";
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 };
};
import type { Memo } from "@/types/proto/api/v1/memo_service_pb"; 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 { export interface MemoViewProps {
/** The memo object to display */
memo: Memo; memo: Memo;
/** Whether to show compact view (hides some metadata) */
compact?: boolean; compact?: boolean;
/** Whether to show the creator's profile information */
showCreator?: boolean; showCreator?: boolean;
/** Whether to show the visibility indicator */
showVisibility?: boolean; showVisibility?: boolean;
/** Whether to show the pinned indicator */
showPinned?: boolean; showPinned?: boolean;
/** Whether to show NSFW content by default */
showNsfwContent?: boolean; showNsfwContent?: boolean;
/** Additional CSS classes to apply to the root element */
className?: string; className?: string;
/** The parent page URL for navigation context */
parentPage?: string; parentPage?: string;
} }
/**
* Props for the MemoHeader component.
* Displays memo metadata like creator, timestamp, and action buttons.
*/
export interface MemoHeaderProps { export interface MemoHeaderProps {
// Display options
showCreator?: boolean; showCreator?: boolean;
showVisibility?: boolean; showVisibility?: boolean;
showPinned?: boolean; showPinned?: boolean;
// Callbacks
onEdit: () => void; onEdit: () => void;
onGotoDetail: () => void; onGotoDetail: () => void;
onUnpin: () => void; onUnpin: () => void;
onToggleNsfwVisibility?: () => 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 { export interface MemoBodyProps {
// Display options
compact?: boolean; compact?: boolean;
// Callbacks
onContentClick: (e: React.MouseEvent) => void; onContentClick: (e: React.MouseEvent) => void;
onContentDoubleClick: (e: React.MouseEvent) => void; onContentDoubleClick: (e: React.MouseEvent) => void;
onToggleNsfwVisibility: () => void; onToggleNsfwVisibility: () => void;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment