chore: move memo-metadata components to MemoView and MemoEditor

- Remove shared memo-metadata folder
- Move metadata display components (AttachmentList, LocationDisplay, RelationList) to MemoView/components/metadata
- Move attachment types and utilities (LocalFile, AttachmentItem, toAttachmentItems) to MemoEditor/types/attachment
- Simplify AttachmentList and AttachmentCard to work directly with Attachment proto
- Update all imports across MemoEditor and MemoView components
- Better separation of concerns: MemoView handles display, MemoEditor handles local files + attachments
This commit is contained in:
Johnny 2026-01-03 13:07:53 +08:00
parent a6e8ba7fb2
commit e761ef8684
17 changed files with 56 additions and 54 deletions

View File

@ -4,7 +4,6 @@ import { FileIcon, LinkIcon, LoaderIcon, MapPinIcon, Maximize2Icon, MoreHorizont
import { useEffect, useState } from "react";
import { useDebounce } from "react-use";
import { useReverseGeocoding } from "@/components/map";
import type { LocalFile } from "@/components/memo-metadata";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@ -22,6 +21,7 @@ import { LinkMemoDialog, LocationDialog } from "../components";
import { useFileUpload, useLinkMemo, useLocation } from "../hooks";
import { useEditorContext } from "../state";
import type { InsertMenuProps } from "../types";
import type { LocalFile } from "../types/attachment";
const InsertMenu = (props: InsertMenuProps) => {
const t = useTranslate();

View File

@ -1,10 +1,10 @@
import { ChevronDownIcon, ChevronUpIcon, FileIcon, Loader2Icon, PaperclipIcon, XIcon } from "lucide-react";
import type { FC } from "react";
import type { LocalFile } from "@/components/memo-metadata/types";
import { toAttachmentItems } from "@/components/memo-metadata/types";
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";
interface AttachmentListProps {
attachments: Attachment[];

View File

@ -1,9 +1,9 @@
import { forwardRef } from "react";
import type { LocalFile } from "@/components/memo-metadata";
import Editor, { type EditorRefActions } from "../Editor";
import { useBlobUrls, useDragAndDrop } from "../hooks";
import { useEditorContext } from "../state";
import type { EditorContentProps } from "../types";
import type { LocalFile } from "../types/attachment";
export const EditorContent = forwardRef<EditorRefActions, EditorContentProps>(({ placeholder }, ref) => {
const { state, actions, dispatch } = useEditorContext();

View File

@ -1,5 +1,5 @@
import { useRef } from "react";
import type { LocalFile } from "@/components/memo-metadata";
import type { LocalFile } from "../types/attachment";
export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void) => {
const fileInputRef = useRef<HTMLInputElement>(null);

View File

@ -1,8 +1,8 @@
import { create } from "@bufbuild/protobuf";
import type { LocalFile } from "@/components/memo-metadata";
import { attachmentServiceClient } from "@/connect";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { AttachmentSchema } from "@/types/proto/api/v1/attachment_service_pb";
import type { LocalFile } from "../types/attachment";
export const uploadService = {
async uploadFiles(localFiles: LocalFile[]): Promise<Attachment[]> {

View File

@ -1,6 +1,6 @@
import type { LocalFile } from "@/components/memo-metadata";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
import type { LocalFile } from "../types/attachment";
import type { EditorAction, EditorState, LoadingKey } from "./types";
export const editorActions = {

View File

@ -1,7 +1,7 @@
import type { LocalFile } from "@/components/memo-metadata";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import type { Location, MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
import { Visibility } from "@/types/proto/api/v1/memo_service_pb";
import type { LocalFile } from "../types/attachment";
export type LoadingKey = "saving" | "uploading" | "loading";

View File

@ -1,16 +1,9 @@
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
export type DisplayMode = "edit" | "view";
export interface BaseMetadataProps {
mode: DisplayMode;
className?: string;
}
export type FileCategory = "image" | "video" | "document";
// Pure view model for rendering attachments and local files
// Unified view model for rendering attachments and local files
export interface AttachmentItem {
readonly id: string;
readonly filename: string;
@ -22,6 +15,12 @@ export interface AttachmentItem {
readonly isLocal: boolean;
}
// For MemoEditor: local files being uploaded
export interface LocalFile {
readonly file: File;
readonly previewUrl: string;
}
function categorizeFile(mimeType: string): FileCategory {
if (mimeType.startsWith("image/")) return "image";
if (mimeType.startsWith("video/")) return "video";
@ -46,7 +45,7 @@ export function attachmentToItem(attachment: Attachment): AttachmentItem {
export function fileToItem(file: File, blobUrl: string): AttachmentItem {
return {
id: blobUrl, // Use blob URL as unique ID since we don't have a server ID yet
id: blobUrl,
filename: file.name,
category: categorizeFile(file.type),
mimeType: file.type,
@ -57,11 +56,6 @@ export function fileToItem(file: File, blobUrl: string): AttachmentItem {
};
}
export interface LocalFile {
readonly file: File;
readonly previewUrl: string;
}
export function toAttachmentItems(attachments: Attachment[], localFiles: LocalFile[] = []): AttachmentItem[] {
return [...attachments.map(attachmentToItem), ...localFiles.map(({ file, previewUrl }) => fileToItem(file, previewUrl))];
}

View File

@ -1,7 +1,7 @@
import { createContext } from "react";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
import type { LocalFile } from "../../memo-metadata";
import type { LocalFile } from "./attachment";
export interface MemoEditorContextValue {
attachmentList: Attachment[];

View File

@ -3,9 +3,9 @@ import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n";
import MemoContent from "../../MemoContent";
import { MemoReactionListView } from "../../MemoReactionListView";
import { AttachmentList, LocationDisplay, RelationList } from "../../memo-metadata";
import { useMemoViewContext } from "../MemoViewContext";
import type { MemoBodyProps } from "../types";
import { AttachmentList, LocationDisplay, RelationList } from "./metadata";
const MemoBody: React.FC<MemoBodyProps> = ({ compact, onContentClick, onContentDoubleClick, onToggleNsfwVisibility }) => {
const t = useTranslate();

View File

@ -1,21 +1,22 @@
import { cn } from "@/lib/utils";
import type { AttachmentItem } from "./types";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
interface AttachmentCardProps {
item: AttachmentItem;
mode: "view";
attachment: Attachment;
onClick?: () => void;
className?: string;
}
const AttachmentCard = ({ item, onClick, className }: AttachmentCardProps) => {
const { category, filename, sourceUrl } = item;
const AttachmentCard = ({ attachment, onClick, className }: AttachmentCardProps) => {
const attachmentType = getAttachmentType(attachment);
const sourceUrl = getAttachmentUrl(attachment);
if (category === "image") {
if (attachmentType === "image/*") {
return (
<img
src={sourceUrl}
alt={filename}
alt={attachment.filename}
className={cn("w-full h-full object-cover rounded-lg cursor-pointer", className)}
onClick={onClick}
loading="lazy"
@ -23,7 +24,7 @@ const AttachmentCard = ({ item, onClick, className }: AttachmentCardProps) => {
);
}
if (category === "video") {
if (attachmentType === "video/*") {
return <video src={sourceUrl} className={cn("w-full h-full object-cover rounded-lg", className)} controls preload="metadata" />;
}

View File

@ -1,15 +1,30 @@
import { useState } from "react";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
import MemoAttachment from "../MemoAttachment";
import PreviewImageDialog from "../PreviewImageDialog";
import MemoAttachment from "../../../MemoAttachment";
import PreviewImageDialog from "../../../PreviewImageDialog";
import AttachmentCard from "./AttachmentCard";
import { separateMediaAndDocs, toAttachmentItems } from "./types";
interface AttachmentListProps {
attachments: Attachment[];
}
function separateMediaAndDocs(attachments: Attachment[]): { media: Attachment[]; docs: Attachment[] } {
const media: Attachment[] = [];
const docs: Attachment[] = [];
for (const attachment of attachments) {
const attachmentType = getAttachmentType(attachment);
if (attachmentType === "image/*" || attachmentType === "video/*") {
media.push(attachment);
} else {
docs.push(attachment);
}
}
return { media, docs };
}
const AttachmentList = ({ attachments }: AttachmentListProps) => {
const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number }>({
open: false,
@ -25,8 +40,7 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
setPreviewImage({ open: true, urls: imgUrls, index });
};
const items = toAttachmentItems(attachments, []);
const { media: mediaItems, docs: docItems } = separateMediaAndDocs(items);
const { media: mediaItems, docs: docItems } = separateMediaAndDocs(attachments);
if (attachments.length === 0) {
return null;
@ -36,13 +50,12 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
<>
{mediaItems.length > 0 && (
<div className="w-full flex flex-row justify-start overflow-auto gap-2">
{mediaItems.map((item) => (
<div key={item.id} className="max-w-[60%] w-fit flex flex-col justify-start items-start shrink-0">
{mediaItems.map((attachment) => (
<div key={attachment.name} className="max-w-[60%] w-fit flex flex-col justify-start items-start shrink-0">
<AttachmentCard
item={item}
mode="view"
attachment={attachment}
onClick={() => {
handleImageClick(item.sourceUrl, attachments);
handleImageClick(getAttachmentUrl(attachment), mediaItems);
}}
className="max-h-64 grow"
/>
@ -53,16 +66,15 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
{docItems.length > 0 && (
<div className="w-full flex flex-row justify-start overflow-auto gap-2">
{docItems.map((item) => {
const attachment = attachments.find((a) => a.name === item.id);
return attachment ? <MemoAttachment key={item.id} attachment={attachment} /> : null;
})}
{docItems.map((attachment) => (
<MemoAttachment key={attachment.name} attachment={attachment} />
))}
</div>
)}
<PreviewImageDialog
open={previewImage.open}
onOpenChange={(open) => setPreviewImage((prev) => ({ ...prev, open }))}
onOpenChange={(open: boolean) => setPreviewImage((prev) => ({ ...prev, open }))}
imgUrls={previewImage.urls}
initialIndex={previewImage.index}
/>

View File

@ -4,7 +4,7 @@ import { useState } from "react";
import { LocationPicker } from "@/components/map";
import { cn } from "@/lib/utils";
import type { Location } from "@/types/proto/api/v1/memo_service_pb";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { Popover, PopoverContent, PopoverTrigger } from "../../../ui/popover";
interface LocationDisplayProps {
location?: Location;

View File

@ -1,11 +1,6 @@
export { default as AttachmentCard } from "./AttachmentCard";
export { default as AttachmentList } from "./AttachmentList";
export { default as LocationDisplay } from "./LocationDisplay";
// Base components (can be used for other metadata types)
export { default as MetadataCard } from "./MetadataCard";
export { default as RelationCard } from "./RelationCard";
export { default as RelationList } from "./RelationList";
// Types
export type { AttachmentItem, FileCategory, LocalFile } from "./types";
export { attachmentToItem, fileToItem, filterByCategory, separateMediaAndDocs, toAttachmentItems } from "./types";