Unverified Commit 41778980 authored by memoclaw's avatar memoclaw Committed by GitHub

refactor(web): consolidate memo metadata components into MemoMetadata (#5755)

Co-authored-by: 's avatarmemoclaw <265580040+memoclaw@users.noreply.github.com>
Co-authored-by: 's avatarCopilot <223556219+Copilot@users.noreply.github.com>
parent ac077ac3
import { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentUrl, isMidiFile } from "@/utils/attachment";
import AttachmentIcon from "./AttachmentIcon";
interface Props {
attachment: Attachment;
className?: string;
}
const MemoAttachment: React.FC<Props> = (props: Props) => {
const { className, attachment } = props;
const attachmentUrl = getAttachmentUrl(attachment);
const handlePreviewBtnClick = () => {
window.open(attachmentUrl);
};
return (
<div
className={`w-auto flex flex-row justify-start items-center text-muted-foreground hover:text-foreground hover:bg-accent rounded px-2 py-1 transition-colors ${className}`}
>
{attachment.type.startsWith("audio") && !isMidiFile(attachment.type) ? (
<audio src={attachmentUrl} controls></audio>
) : (
<>
<AttachmentIcon className="w-4! h-4! mr-1" attachment={attachment} />
<span className="text-sm max-w-[256px] truncate cursor-pointer" onClick={handlePreviewBtnClick}>
{attachment.filename}
</span>
</>
)}
</div>
);
};
export default MemoAttachment;
......@@ -3,6 +3,7 @@ import { uniqBy } from "lodash-es";
import { FileIcon, LinkIcon, LoaderIcon, type LucideIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useDebounce } from "react-use";
import { LinkMemoDialog, LocationDialog } from "@/components/MemoMetadata";
import { useReverseGeocoding } from "@/components/map";
import { Button } from "@/components/ui/button";
import {
......@@ -17,7 +18,6 @@ import {
} from "@/components/ui/dropdown-menu";
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
import { LinkMemoDialog, LocationDialog } from "../components";
import { useFileUpload, useLinkMemo, useLocation } from "../hooks";
import { useEditorContext } from "../state";
import type { InsertMenuProps } from "../types";
......
import type { FC } from "react";
import { AttachmentListEditor, LocationDisplayEditor, RelationListEditor } from "@/components/MemoMetadata";
import { useEditorContext } from "../state";
import type { EditorMetadataProps } from "../types";
import AttachmentList from "./AttachmentList";
import LocationDisplay from "./LocationDisplay";
import RelationList from "./RelationList";
export const EditorMetadata: FC<EditorMetadataProps> = ({ memoName }) => {
const { state, actions, dispatch } = useEditorContext();
return (
<div className="w-full flex flex-col gap-2">
<AttachmentList
<AttachmentListEditor
attachments={state.metadata.attachments}
localFiles={state.localFiles}
onAttachmentsChange={(attachments) => dispatch(actions.setMetadata({ attachments }))}
onRemoveLocalFile={(previewUrl) => dispatch(actions.removeLocalFile(previewUrl))}
/>
<RelationList
<RelationListEditor
relations={state.metadata.relations}
onRelationsChange={(relations) => dispatch(actions.setMetadata({ relations }))}
memoName={memoName}
/>
{state.metadata.location && (
<LocationDisplay location={state.metadata.location} onRemove={() => dispatch(actions.setMetadata({ location: undefined }))} />
<LocationDisplayEditor location={state.metadata.location} onRemove={() => dispatch(actions.setMetadata({ location: undefined }))} />
)}
</div>
);
......
// UI components for MemoEditor
export { default as AttachmentList } from "./AttachmentList";
export * from "./EditorContent";
export * from "./EditorMetadata";
export * from "./EditorToolbar";
export { FocusModeExitButton, FocusModeOverlay } from "./FocusModeOverlay";
export { LinkMemoDialog } from "./LinkMemoDialog";
export { LocationDialog } from "./LocationDialog";
export { default as LocationDisplay } from "./LocationDisplay";
export { default as RelationList } from "./RelationList";
export { TimestampPopover } from "./TimestampPopover";
import type { LatLng } from "leaflet";
import type { Location, Memo, Visibility } from "@/types/proto/api/v1/memo_service_pb";
import type { EditorRefActions } from "../Editor";
import type { Command } from "../Editor/commands";
import type { LocationState } from "./insert-menu";
export interface MemoEditorProps {
className?: string;
......@@ -41,28 +39,6 @@ export interface FocusModeExitButtonProps {
title: string;
}
export interface LinkMemoDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
searchText: string;
onSearchChange: (text: string) => void;
filteredMemos: Memo[];
isFetching: boolean;
onSelectMemo: (memo: Memo) => void;
isAlreadyLinked: (memoName: string) => boolean;
}
export interface LocationDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
state: LocationState;
onPositionChange: (position: LatLng) => void;
onUpdateCoordinate: (type: "lat" | "lng", value: string) => void;
onPlaceholderChange: (placeholder: string) => void;
onCancel: () => void;
onConfirm: () => void;
}
export interface InsertMenuProps {
isUploading?: boolean;
location?: Location;
......
......@@ -8,8 +8,6 @@ export type {
FocusModeExitButtonProps,
FocusModeOverlayProps,
InsertMenuProps,
LinkMemoDialogProps,
LocationDialogProps,
MemoEditorProps,
SlashCommandsProps,
TagSuggestionsProps,
......
import { ChevronDownIcon, ChevronUpIcon, FileIcon, PaperclipIcon, XIcon } from "lucide-react";
import type { FC } from "react";
import type { LocalFile } from "@/components/MemoEditor/types/attachment";
import { toAttachmentItems } from "@/components/MemoEditor/types/attachment";
import { cn } from "@/lib/utils";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
import type { LocalFile } from "../types/attachment";
import { toAttachmentItems } from "../types/attachment";
import SectionHeader from "../SectionHeader";
interface AttachmentListProps {
interface AttachmentListEditorProps {
attachments: Attachment[];
localFiles?: LocalFile[];
onAttachmentsChange?: (attachments: Attachment[]) => void;
......@@ -100,7 +101,7 @@ const AttachmentItemCard: FC<{
);
};
const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [], onAttachmentsChange, onRemoveLocalFile }) => {
const AttachmentListEditor: FC<AttachmentListEditorProps> = ({ attachments, localFiles = [], onAttachmentsChange, onRemoveLocalFile }) => {
if (attachments.length === 0 && localFiles.length === 0) {
return null;
}
......@@ -139,10 +140,7 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [],
return (
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30">
<PaperclipIcon className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Attachments ({items.length})</span>
</div>
<SectionHeader icon={PaperclipIcon} title="Attachments" count={items.length} />
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
{items.map((item) => {
......@@ -166,4 +164,4 @@ const AttachmentList: FC<AttachmentListProps> = ({ attachments, localFiles = [],
);
};
export default AttachmentList;
export default AttachmentListEditor;
import { FileAudioIcon, FileIcon, PaperclipIcon } from "lucide-react";
import { useMemo } from "react";
import { cn } from "@/lib/utils";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
import { formatFileSize, getFileTypeLabel } from "@/utils/format";
import { useMemoViewContext } from "../../MemoViewContext";
import SectionHeader from "../SectionHeader";
import AttachmentCard from "./AttachmentCard";
import SectionHeader from "./SectionHeader";
interface AttachmentListProps {
interface AttachmentListViewProps {
attachments: Attachment[];
onImagePreview?: (urls: string[], index: number) => void;
}
const isImageAttachment = (attachment: Attachment): boolean => getAttachmentType(attachment) === "image/*";
......@@ -79,19 +80,24 @@ const AudioItem = ({ attachment }: { attachment: Attachment }) => {
interface VisualItemProps {
attachment: Attachment;
onImageClick: (url: string) => void;
onImageClick?: (url: string) => void;
}
const VisualItem = ({ attachment, onImageClick }: VisualItemProps) => {
const isInteractive = isImageAttachment(attachment) && Boolean(onImageClick);
const handleClick = () => {
if (isImageAttachment(attachment)) {
onImageClick(getAttachmentUrl(attachment));
if (isInteractive) {
onImageClick?.(getAttachmentUrl(attachment));
}
};
return (
<div
className="aspect-square rounded-lg overflow-hidden bg-muted/40 border border-border hover:border-accent/50 transition-all cursor-pointer group"
className={cn(
"aspect-square rounded-lg overflow-hidden bg-muted/40 border border-border hover:border-accent/50 transition-all group",
isInteractive && "cursor-pointer",
)}
onClick={handleClick}
>
<AttachmentCard attachment={attachment} className="rounded-none" />
......@@ -99,7 +105,7 @@ const VisualItem = ({ attachment, onImageClick }: VisualItemProps) => {
);
};
const VisualGrid = ({ attachments, onImageClick }: { attachments: Attachment[]; onImageClick: (url: string) => void }) => (
const VisualGrid = ({ attachments, onImageClick }: { attachments: Attachment[]; onImageClick?: (url: string) => void }) => (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
{attachments.map((attachment) => (
<VisualItem key={attachment.name} attachment={attachment} onImageClick={onImageClick} />
......@@ -127,9 +133,7 @@ const DocsList = ({ attachments }: { attachments: Attachment[] }) => (
const Divider = () => <div className="border-t mt-1 border-border opacity-60" />;
const AttachmentList = ({ attachments }: AttachmentListProps) => {
const { openPreview } = useMemoViewContext();
const AttachmentListView = ({ attachments, onImagePreview }: AttachmentListViewProps) => {
const { visual, audio, docs } = useMemo(() => separateAttachments(attachments), [attachments]);
const imageAttachments = useMemo(() => visual.filter(isImageAttachment), [visual]);
......@@ -141,7 +145,7 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
const handleImageClick = (imgUrl: string) => {
const index = imageUrls.findIndex((url) => url === imgUrl);
openPreview(imageUrls, index >= 0 ? index : 0);
onImagePreview?.(imageUrls, index >= 0 ? index : 0);
};
const sections = [visual.length > 0, audio.length > 0, docs.length > 0];
......@@ -166,4 +170,4 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
);
};
export default AttachmentList;
export default AttachmentListView;
export { default as AttachmentCard } from "./AttachmentCard";
export { default as AttachmentListEditor } from "./AttachmentListEditor";
export { default as AttachmentListView } from "./AttachmentListView";
import type { LatLng } from "leaflet";
import type { LocationState } from "@/components/MemoEditor/types/insert-menu";
import { LocationPicker } from "@/components/map";
import { Button } from "@/components/ui/button";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
......@@ -6,7 +8,17 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { VisuallyHidden } from "@/components/ui/visually-hidden";
import { useTranslate } from "@/utils/i18n";
import type { LocationDialogProps } from "../types";
interface LocationDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
state: LocationState;
onPositionChange: (position: LatLng) => void;
onUpdateCoordinate: (type: "lat" | "lng", value: string) => void;
onPlaceholderChange: (placeholder: string) => void;
onCancel: () => void;
onConfirm: () => void;
}
export const LocationDialog = ({
open,
......
......@@ -3,13 +3,13 @@ import type { FC } from "react";
import { cn } from "@/lib/utils";
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
interface LocationDisplayProps {
interface LocationDisplayEditorProps {
location: Location;
onRemove?: () => void;
className?: string;
}
const LocationDisplay: FC<LocationDisplayProps> = ({ location, onRemove, className }) => {
const LocationDisplayEditor: FC<LocationDisplayEditorProps> = ({ location, onRemove, className }) => {
const displayText = location.placeholder || `${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}`;
return (
......@@ -45,4 +45,4 @@ const LocationDisplay: FC<LocationDisplayProps> = ({ location, onRemove, classNa
);
};
export default LocationDisplay;
export default LocationDisplayEditor;
......@@ -2,16 +2,16 @@ import { LatLng } from "leaflet";
import { MapPinIcon } from "lucide-react";
import { useState } from "react";
import { LocationPicker } from "@/components/map";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
import { Popover, PopoverContent, PopoverTrigger } from "../../../ui/popover";
interface LocationDisplayProps {
interface LocationDisplayViewProps {
location?: Location;
className?: string;
}
const LocationDisplay = ({ location, className }: LocationDisplayProps) => {
const LocationDisplayView = ({ location, className }: LocationDisplayViewProps) => {
const [popoverOpen, setPopoverOpen] = useState<boolean>(false);
if (!location) {
......@@ -49,4 +49,4 @@ const LocationDisplay = ({ location, className }: LocationDisplayProps) => {
);
};
export default LocationDisplay;
export default LocationDisplayView;
export { LocationDialog } from "./LocationDialog";
export { default as LocationDisplayEditor } from "./LocationDisplayEditor";
export { default as LocationDisplayView } from "./LocationDisplayView";
......@@ -6,8 +6,19 @@ import { Input } from "@/components/ui/input";
import { VisuallyHidden } from "@/components/ui/visually-hidden";
import { extractMemoIdFromName } from "@/helpers/resource-names";
import { cn } from "@/lib/utils";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
import type { LinkMemoDialogProps } from "../types";
interface LinkMemoDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
searchText: string;
onSearchChange: (text: string) => void;
filteredMemos: Memo[];
isFetching: boolean;
onSelectMemo: (memo: Memo) => void;
isAlreadyLinked: (memoName: string) => boolean;
}
export const LinkMemoDialog = ({
open,
......
import MemoSnippetLink from "@/components/MemoView/components/MemoSnippetLink";
import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
import MemoSnippetLink from "../MemoSnippetLink";
interface RelationCardProps {
memo: MemoRelation_Memo;
......
import { create } from "@bufbuild/protobuf";
import { LinkIcon, XIcon } from "lucide-react";
import type { FC } from "react";
import { useEffect, useState } from "react";
import RelationCard from "@/components/MemoView/components/metadata/RelationCard";
import { useEffect, useMemo, useState } from "react";
import { memoServiceClient } from "@/connect";
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
import { MemoRelation_Memo, MemoRelation_MemoSchema, MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import SectionHeader from "../SectionHeader";
import RelationCard from "./RelationCard";
interface RelationListProps {
interface RelationListEditorProps {
relations: MemoRelation[];
onRelationsChange?: (relations: MemoRelation[]) => void;
parentPage?: string;
......@@ -38,9 +39,10 @@ const RelationItemCard: FC<{
);
};
const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, parentPage, memoName }) => {
const referenceRelations = relations.filter(
(r) => r.type === MemoRelation_Type.REFERENCE && (!memoName || !r.memo?.name || r.memo.name === memoName),
const RelationListEditor: FC<RelationListEditorProps> = ({ relations, onRelationsChange, parentPage, memoName }) => {
const referenceRelations = useMemo(
() => relations.filter((r) => r.type === MemoRelation_Type.REFERENCE && (!memoName || !r.memo?.name || r.memo.name === memoName)),
[relations, memoName],
);
const [fetchedMemos, setFetchedMemos] = useState<Record<string, MemoRelation_Memo>>({});
......@@ -76,10 +78,7 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par
return (
<div className="w-full rounded-lg border border-border bg-muted/20 overflow-hidden">
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border bg-muted/30">
<LinkIcon className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Relations ({referenceRelations.length})</span>
</div>
<SectionHeader icon={LinkIcon} title="Relations" count={referenceRelations.length} />
<div className="p-1 sm:p-1.5 flex flex-col gap-0.5">
{referenceRelations.map((relation) => {
......@@ -92,4 +91,4 @@ const RelationList: FC<RelationListProps> = ({ relations, onRelationsChange, par
);
};
export default RelationList;
export default RelationListEditor;
......@@ -4,17 +4,17 @@ import { cn } from "@/lib/utils";
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
import SectionHeader from "../SectionHeader";
import RelationCard from "./RelationCard";
import SectionHeader from "./SectionHeader";
interface RelationListProps {
interface RelationListViewProps {
relations: MemoRelation[];
currentMemoName?: string;
parentPage?: string;
className?: string;
}
function RelationList({ relations, currentMemoName, parentPage, className }: RelationListProps) {
function RelationListView({ relations, currentMemoName, parentPage, className }: RelationListViewProps) {
const t = useTranslate();
const [activeTab, setActiveTab] = useState<"referencing" | "referenced">("referencing");
......@@ -81,4 +81,4 @@ function RelationList({ relations, currentMemoName, parentPage, className }: Rel
);
}
export default RelationList;
export default RelationListView;
export { LinkMemoDialog } from "./LinkMemoDialog";
export { default as RelationCard } from "./RelationCard";
export { default as RelationListEditor } from "./RelationListEditor";
export { default as RelationListView } from "./RelationListView";
// Consolidated memo metadata components for attachments, locations, and relations.
export { AttachmentCard, AttachmentListEditor, AttachmentListView } from "./Attachment";
export { LocationDialog, LocationDisplayEditor, LocationDisplayView } from "./Location";
export { LinkMemoDialog, RelationCard, RelationListEditor, RelationListView } from "./Relation";
export { default as SectionHeader } from "./SectionHeader";
import { AttachmentListView, LocationDisplayView, RelationListView } from "@/components/MemoMetadata";
import { cn } from "@/lib/utils";
import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
......@@ -6,7 +7,6 @@ import { MemoReactionListView } from "../../MemoReactionListView";
import { useMemoHandlers } from "../hooks";
import { useMemoViewContext } from "../MemoViewContext";
import type { MemoBodyProps } from "../types";
import { AttachmentList, LocationDisplay, RelationList } from "./metadata";
const NsfwOverlay: React.FC<{ onClick?: () => void }> = ({ onClick }) => {
const t = useTranslate();
......@@ -44,9 +44,9 @@ const MemoBody: React.FC<MemoBodyProps> = ({ compact }) => {
onDoubleClick={handleMemoContentDoubleClick}
compact={memo.pinned ? false : compact} // Always show full content when pinned
/>
<AttachmentList attachments={memo.attachments} />
<RelationList relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />
{memo.location && <LocationDisplay location={memo.location} />}
<AttachmentListView attachments={memo.attachments} onImagePreview={openPreview} />
<RelationListView relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />
{memo.location && <LocationDisplayView location={memo.location} />}
<MemoReactionListView memo={memo} reactions={memo.reactions} />
</div>
......
export { default as AttachmentCard } from "./AttachmentCard";
export { default as AttachmentList } from "./AttachmentList";
export { default as LocationDisplay } from "./LocationDisplay";
export { default as RelationCard } from "./RelationCard";
export { default as RelationList } from "./RelationList";
......@@ -4,7 +4,9 @@ import { Code, ConnectError } from "@connectrpc/connect";
import { AlertCircleIcon } from "lucide-react";
import { useParams } from "react-router-dom";
import MemoContent from "@/components/MemoContent";
import AttachmentList from "@/components/MemoView/components/metadata/AttachmentList";
import { AttachmentListView } from "@/components/MemoMetadata";
import { useImagePreview } from "@/components/MemoView/hooks";
import PreviewImageDialog from "@/components/PreviewImageDialog";
import UserAvatar from "@/components/UserAvatar";
import { useSharedMemo } from "@/hooks/useMemoShareQueries";
import { useUser } from "@/hooks/useUserQueries";
......@@ -22,6 +24,7 @@ function withShareAttachmentLinks(attachments: Attachment[], token: string): Att
const SharedMemo = () => {
const t = useTranslate();
const { token = "" } = useParams<{ token: string }>();
const { previewState, openPreview, setPreviewOpen } = useImagePreview();
const { data: memo, error, isLoading } = useSharedMemo(token, { enabled: !!token });
const { data: creator } = useUser(memo?.creator ?? "", { enabled: !!memo?.creator });
......@@ -64,8 +67,17 @@ const SharedMemo = () => {
<div className="relative flex flex-col items-start gap-2 rounded-lg border border-border bg-card px-4 py-3 text-card-foreground">
<MemoContent content={memo.content} />
{memo.attachments.length > 0 && <AttachmentList attachments={withShareAttachmentLinks(memo.attachments, token)} />}
{memo.attachments.length > 0 && (
<AttachmentListView attachments={withShareAttachmentLinks(memo.attachments, token)} onImagePreview={openPreview} />
)}
</div>
<PreviewImageDialog
open={previewState.open}
onOpenChange={setPreviewOpen}
imgUrls={previewState.urls}
initialIndex={previewState.index}
/>
</div>
);
};
......
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