Commit e761ef86 authored by Johnny's avatar Johnny

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
parent a6e8ba7f
...@@ -4,7 +4,6 @@ import { FileIcon, LinkIcon, LoaderIcon, MapPinIcon, Maximize2Icon, MoreHorizont ...@@ -4,7 +4,6 @@ import { FileIcon, LinkIcon, LoaderIcon, MapPinIcon, Maximize2Icon, MoreHorizont
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDebounce } from "react-use"; import { useDebounce } from "react-use";
import { useReverseGeocoding } from "@/components/map"; import { useReverseGeocoding } from "@/components/map";
import type { LocalFile } from "@/components/memo-metadata";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
...@@ -22,6 +21,7 @@ import { LinkMemoDialog, LocationDialog } from "../components"; ...@@ -22,6 +21,7 @@ import { LinkMemoDialog, LocationDialog } from "../components";
import { useFileUpload, useLinkMemo, useLocation } from "../hooks"; import { useFileUpload, useLinkMemo, useLocation } from "../hooks";
import { useEditorContext } from "../state"; import { useEditorContext } from "../state";
import type { InsertMenuProps } from "../types"; import type { InsertMenuProps } from "../types";
import type { LocalFile } from "../types/attachment";
const InsertMenu = (props: InsertMenuProps) => { const InsertMenu = (props: InsertMenuProps) => {
const t = useTranslate(); const t = useTranslate();
......
import { ChevronDownIcon, ChevronUpIcon, FileIcon, Loader2Icon, PaperclipIcon, XIcon } from "lucide-react"; import { ChevronDownIcon, ChevronUpIcon, FileIcon, Loader2Icon, PaperclipIcon, XIcon } from "lucide-react";
import type { FC } from "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 { cn } from "@/lib/utils";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { formatFileSize, getFileTypeLabel } from "@/utils/format"; import { formatFileSize, getFileTypeLabel } from "@/utils/format";
import type { LocalFile } from "../types/attachment";
import { toAttachmentItems } from "../types/attachment";
interface AttachmentListProps { interface AttachmentListProps {
attachments: Attachment[]; attachments: Attachment[];
......
import { forwardRef } from "react"; import { forwardRef } from "react";
import type { LocalFile } from "@/components/memo-metadata";
import Editor, { type EditorRefActions } from "../Editor"; import Editor, { type EditorRefActions } from "../Editor";
import { useBlobUrls, useDragAndDrop } from "../hooks"; import { useBlobUrls, useDragAndDrop } from "../hooks";
import { useEditorContext } from "../state"; import { useEditorContext } from "../state";
import type { EditorContentProps } from "../types"; import type { EditorContentProps } from "../types";
import type { LocalFile } from "../types/attachment";
export const EditorContent = forwardRef<EditorRefActions, EditorContentProps>(({ placeholder }, ref) => { export const EditorContent = forwardRef<EditorRefActions, EditorContentProps>(({ placeholder }, ref) => {
const { state, actions, dispatch } = useEditorContext(); const { state, actions, dispatch } = useEditorContext();
......
import { useRef } from "react"; import { useRef } from "react";
import type { LocalFile } from "@/components/memo-metadata"; import type { LocalFile } from "../types/attachment";
export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void) => { export const useFileUpload = (onFilesSelected: (localFiles: LocalFile[]) => void) => {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
......
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import type { LocalFile } from "@/components/memo-metadata";
import { attachmentServiceClient } from "@/connect"; import { attachmentServiceClient } from "@/connect";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { AttachmentSchema } 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 = { export const uploadService = {
async uploadFiles(localFiles: LocalFile[]): Promise<Attachment[]> { async uploadFiles(localFiles: LocalFile[]): Promise<Attachment[]> {
......
import type { LocalFile } from "@/components/memo-metadata";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import type { MemoRelation } from "@/types/proto/api/v1/memo_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"; import type { EditorAction, EditorState, LoadingKey } from "./types";
export const editorActions = { export const editorActions = {
......
import type { LocalFile } from "@/components/memo-metadata";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import type { Location, MemoRelation } from "@/types/proto/api/v1/memo_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 { Visibility } from "@/types/proto/api/v1/memo_service_pb";
import type { LocalFile } from "../types/attachment";
export type LoadingKey = "saving" | "uploading" | "loading"; export type LoadingKey = "saving" | "uploading" | "loading";
......
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentThumbnailUrl, getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; 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"; 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 { export interface AttachmentItem {
readonly id: string; readonly id: string;
readonly filename: string; readonly filename: string;
...@@ -22,6 +15,12 @@ export interface AttachmentItem { ...@@ -22,6 +15,12 @@ export interface AttachmentItem {
readonly isLocal: boolean; readonly isLocal: boolean;
} }
// For MemoEditor: local files being uploaded
export interface LocalFile {
readonly file: File;
readonly previewUrl: string;
}
function categorizeFile(mimeType: string): FileCategory { function categorizeFile(mimeType: string): FileCategory {
if (mimeType.startsWith("image/")) return "image"; if (mimeType.startsWith("image/")) return "image";
if (mimeType.startsWith("video/")) return "video"; if (mimeType.startsWith("video/")) return "video";
...@@ -46,7 +45,7 @@ export function attachmentToItem(attachment: Attachment): AttachmentItem { ...@@ -46,7 +45,7 @@ export function attachmentToItem(attachment: Attachment): AttachmentItem {
export function fileToItem(file: File, blobUrl: string): AttachmentItem { export function fileToItem(file: File, blobUrl: string): AttachmentItem {
return { return {
id: blobUrl, // Use blob URL as unique ID since we don't have a server ID yet id: blobUrl,
filename: file.name, filename: file.name,
category: categorizeFile(file.type), category: categorizeFile(file.type),
mimeType: file.type, mimeType: file.type,
...@@ -57,11 +56,6 @@ export function fileToItem(file: File, blobUrl: string): AttachmentItem { ...@@ -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[] { export function toAttachmentItems(attachments: Attachment[], localFiles: LocalFile[] = []): AttachmentItem[] {
return [...attachments.map(attachmentToItem), ...localFiles.map(({ file, previewUrl }) => fileToItem(file, previewUrl))]; return [...attachments.map(attachmentToItem), ...localFiles.map(({ file, previewUrl }) => fileToItem(file, previewUrl))];
} }
......
import { createContext } from "react"; import { createContext } from "react";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import type { MemoRelation } from "@/types/proto/api/v1/memo_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 { export interface MemoEditorContextValue {
attachmentList: Attachment[]; attachmentList: Attachment[];
......
...@@ -3,9 +3,9 @@ import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb"; ...@@ -3,9 +3,9 @@ import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import MemoContent from "../../MemoContent"; import MemoContent from "../../MemoContent";
import { MemoReactionListView } from "../../MemoReactionListView"; import { MemoReactionListView } from "../../MemoReactionListView";
import { AttachmentList, LocationDisplay, RelationList } from "../../memo-metadata";
import { useMemoViewContext } from "../MemoViewContext"; import { useMemoViewContext } from "../MemoViewContext";
import type { MemoBodyProps } from "../types"; import type { MemoBodyProps } from "../types";
import { AttachmentList, LocationDisplay, RelationList } from "./metadata";
const MemoBody: React.FC<MemoBodyProps> = ({ compact, onContentClick, onContentDoubleClick, onToggleNsfwVisibility }) => { const MemoBody: React.FC<MemoBodyProps> = ({ compact, onContentClick, onContentDoubleClick, onToggleNsfwVisibility }) => {
const t = useTranslate(); const t = useTranslate();
......
import { cn } from "@/lib/utils"; 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 { interface AttachmentCardProps {
item: AttachmentItem; attachment: Attachment;
mode: "view";
onClick?: () => void; onClick?: () => void;
className?: string; className?: string;
} }
const AttachmentCard = ({ item, onClick, className }: AttachmentCardProps) => { const AttachmentCard = ({ attachment, onClick, className }: AttachmentCardProps) => {
const { category, filename, sourceUrl } = item; const attachmentType = getAttachmentType(attachment);
const sourceUrl = getAttachmentUrl(attachment);
if (category === "image") { if (attachmentType === "image/*") {
return ( return (
<img <img
src={sourceUrl} src={sourceUrl}
alt={filename} alt={attachment.filename}
className={cn("w-full h-full object-cover rounded-lg cursor-pointer", className)} className={cn("w-full h-full object-cover rounded-lg cursor-pointer", className)}
onClick={onClick} onClick={onClick}
loading="lazy" loading="lazy"
...@@ -23,7 +24,7 @@ const AttachmentCard = ({ item, onClick, className }: AttachmentCardProps) => { ...@@ -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" />; return <video src={sourceUrl} className={cn("w-full h-full object-cover rounded-lg", className)} controls preload="metadata" />;
} }
......
import { useState } from "react"; import { useState } from "react";
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
import MemoAttachment from "../MemoAttachment"; import MemoAttachment from "../../../MemoAttachment";
import PreviewImageDialog from "../PreviewImageDialog"; import PreviewImageDialog from "../../../PreviewImageDialog";
import AttachmentCard from "./AttachmentCard"; import AttachmentCard from "./AttachmentCard";
import { separateMediaAndDocs, toAttachmentItems } from "./types";
interface AttachmentListProps { interface AttachmentListProps {
attachments: Attachment[]; 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 AttachmentList = ({ attachments }: AttachmentListProps) => {
const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number }>({ const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number }>({
open: false, open: false,
...@@ -25,8 +40,7 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => { ...@@ -25,8 +40,7 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
setPreviewImage({ open: true, urls: imgUrls, index }); setPreviewImage({ open: true, urls: imgUrls, index });
}; };
const items = toAttachmentItems(attachments, []); const { media: mediaItems, docs: docItems } = separateMediaAndDocs(attachments);
const { media: mediaItems, docs: docItems } = separateMediaAndDocs(items);
if (attachments.length === 0) { if (attachments.length === 0) {
return null; return null;
...@@ -36,13 +50,12 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => { ...@@ -36,13 +50,12 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
<> <>
{mediaItems.length > 0 && ( {mediaItems.length > 0 && (
<div className="w-full flex flex-row justify-start overflow-auto gap-2"> <div className="w-full flex flex-row justify-start overflow-auto gap-2">
{mediaItems.map((item) => ( {mediaItems.map((attachment) => (
<div key={item.id} className="max-w-[60%] w-fit flex flex-col justify-start items-start shrink-0"> <div key={attachment.name} className="max-w-[60%] w-fit flex flex-col justify-start items-start shrink-0">
<AttachmentCard <AttachmentCard
item={item} attachment={attachment}
mode="view"
onClick={() => { onClick={() => {
handleImageClick(item.sourceUrl, attachments); handleImageClick(getAttachmentUrl(attachment), mediaItems);
}} }}
className="max-h-64 grow" className="max-h-64 grow"
/> />
...@@ -53,16 +66,15 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => { ...@@ -53,16 +66,15 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => {
{docItems.length > 0 && ( {docItems.length > 0 && (
<div className="w-full flex flex-row justify-start overflow-auto gap-2"> <div className="w-full flex flex-row justify-start overflow-auto gap-2">
{docItems.map((item) => { {docItems.map((attachment) => (
const attachment = attachments.find((a) => a.name === item.id); <MemoAttachment key={attachment.name} attachment={attachment} />
return attachment ? <MemoAttachment key={item.id} attachment={attachment} /> : null; ))}
})}
</div> </div>
)} )}
<PreviewImageDialog <PreviewImageDialog
open={previewImage.open} open={previewImage.open}
onOpenChange={(open) => setPreviewImage((prev) => ({ ...prev, open }))} onOpenChange={(open: boolean) => setPreviewImage((prev) => ({ ...prev, open }))}
imgUrls={previewImage.urls} imgUrls={previewImage.urls}
initialIndex={previewImage.index} initialIndex={previewImage.index}
/> />
......
...@@ -4,7 +4,7 @@ import { useState } from "react"; ...@@ -4,7 +4,7 @@ import { useState } from "react";
import { LocationPicker } from "@/components/map"; import { LocationPicker } from "@/components/map";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Location } from "@/types/proto/api/v1/memo_service_pb"; 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 { interface LocationDisplayProps {
location?: Location; location?: Location;
......
export { default as AttachmentCard } from "./AttachmentCard";
export { default as AttachmentList } from "./AttachmentList"; export { default as AttachmentList } from "./AttachmentList";
export { default as LocationDisplay } from "./LocationDisplay"; export { default as LocationDisplay } from "./LocationDisplay";
// Base components (can be used for other metadata types)
export { default as MetadataCard } from "./MetadataCard"; export { default as MetadataCard } from "./MetadataCard";
export { default as RelationCard } from "./RelationCard"; export { default as RelationCard } from "./RelationCard";
export { default as RelationList } from "./RelationList"; export { default as RelationList } from "./RelationList";
// Types
export type { AttachmentItem, FileCategory, LocalFile } from "./types";
export { attachmentToItem, fileToItem, filterByCategory, separateMediaAndDocs, toAttachmentItems } from "./types";
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