diff --git a/packages/chat/src/components/emoji/emojiPicker.tsx b/packages/chat/src/components/emojiPicker.tsx similarity index 100% rename from packages/chat/src/components/emoji/emojiPicker.tsx rename to packages/chat/src/components/emojiPicker.tsx diff --git a/packages/chat/src/components/emoji/emojiRanks.ts b/packages/chat/src/components/emojiRanks.ts similarity index 100% rename from packages/chat/src/components/emoji/emojiRanks.ts rename to packages/chat/src/components/emojiRanks.ts diff --git a/packages/chat/src/components/media/gifPicker.tsx b/packages/chat/src/components/gifPicker.tsx similarity index 83% rename from packages/chat/src/components/media/gifPicker.tsx rename to packages/chat/src/components/gifPicker.tsx index 0907d12..e44742b 100644 --- a/packages/chat/src/components/media/gifPicker.tsx +++ b/packages/chat/src/components/gifPicker.tsx @@ -8,11 +8,9 @@ import { TabsList, TabsTrigger, } from "@methanium/ui"; -import type { ChatPickerMediaGroup } from "@tensamin/shared/data"; import { useStorage } from "@tensamin/storage/context"; -import { ArrowLeft, Loader2, Search } from "lucide-react"; +import { Loader2, Search } from "lucide-react"; import MediaSaveButton from "./mediaSaveButton"; -import { getUngroupedMedia } from "./mediaGroups"; import React, { useEffect, useLayoutEffect, @@ -445,34 +443,21 @@ function KlipyPanel({ } function SavedPanel({ - groups, onSelect, onSavedMediaChange, - onSavedMediaGroupsChange, resizeWidth, urls, }: { - groups: ChatPickerMediaGroup[]; onSelect: (url: string) => void; onSavedMediaChange: (savedMedia: string[]) => void; - onSavedMediaGroupsChange: (groups: ChatPickerMediaGroup[]) => void; resizeWidth?: number; urls: string[]; }) { - const [selectedGroupId, setSelectedGroupId] = useState(null); const [scrollRef, measuredWidth] = useMeasuredWidth(); - const selectedGroup = groups.find((group) => group.id === selectedGroupId); - const sortedGroups = [...groups].sort((left, right) => - left.name.localeCompare(right.name), - ); - const visibleUrls = selectedGroup - ? selectedGroup.media.filter((url) => urls.includes(url)) - : getUngroupedMedia(urls, groups); - const imageSizes = useImageSizes(visibleUrls); + const imageSizes = useImageSizes(urls); const columnWidth = resizeWidth ?? measuredWidth; - const columnCount = getColumnCount(columnWidth, visibleUrls.length); - const groupColumnCount = getColumnCount(columnWidth, groups.length); - const columns = distributeByHeight(visibleUrls, columnCount, (url) => { + const columnCount = getColumnCount(columnWidth, urls.length); + const columns = distributeByHeight(urls, columnCount, (url) => { const size = imageSizes[url]; if (!size?.width || !size.height) return 1; @@ -490,50 +475,6 @@ function SavedPanel({ return (
- {selectedGroup ? ( -
- - {selectedGroup.name} -
- ) : groups.length > 0 ? ( -
- {sortedGroups.map((group) => ( - - ))} -
- ) : null} - - {visibleUrls.length === 0 ? ( -
- {selectedGroup ? "This group is empty." : "No ungrouped media."} -
- ) : null} -
{columns.map((column, columnIndex) => (
@@ -563,7 +504,6 @@ function SavedPanel({ ariaLabel="Unsave media" defaultSaved onSavedMediaChange={onSavedMediaChange} - onSavedMediaGroupsChange={onSavedMediaGroupsChange} url={url} />
@@ -590,9 +530,6 @@ export default function GifPicker({ const [searchString, setSearchString] = useState(""); const [debouncedSearchString, setDebouncedSearchString] = useState(""); const [savedMedia, setSavedMedia] = useState([]); - const [savedMediaGroups, setSavedMediaGroups] = useState< - ChatPickerMediaGroup[] - >([]); const didLoadLastTabRef = useRef(false); const searchDebouncerRef = useRef void> | null>( null, @@ -618,13 +555,7 @@ export default function GifPicker({ useEffect(() => { if (tab !== "saved") return; - void Promise.all([ - load("chat_picker_saved_media"), - load("chat_picker_saved_media_groups"), - ]).then(([nextSavedMedia, nextGroups]) => { - setSavedMedia(nextSavedMedia); - setSavedMediaGroups(nextGroups); - }); + void load("chat_picker_saved_media").then(setSavedMedia); }, [load, tab]); useEffect(() => { @@ -696,10 +627,8 @@ export default function GifPicker({ className="flex min-h-0 w-full flex-col overflow-hidden" > diff --git a/packages/chat/src/components/input.tsx b/packages/chat/src/components/input.tsx index 15c5c55..28752f0 100644 --- a/packages/chat/src/components/input.tsx +++ b/packages/chat/src/components/input.tsx @@ -18,9 +18,9 @@ import { cn, useIsMobile } from "@methanium/ui"; import { encryptChatText } from "@tensamin/crypto/chatSecret"; import { useSession } from "@tensamin/storage/session"; -import EmojiPicker from "./emoji/emojiPicker"; -import { useEmojiRanks, useRecordEmojiUse } from "./emoji/emojiRanks"; -import GifPicker from "./media/gifPicker"; +import GifPicker from "./gifPicker"; +import EmojiPicker from "./emojiPicker"; +import { useEmojiRanks, useRecordEmojiUse } from "./emojiRanks"; import ReplyBox from "./replyBox"; import { useHotkey } from "@tensamin/hotkeys"; import { editLastMessageHotkey } from "../hotkeys"; @@ -345,7 +345,7 @@ export default function InputComponent({ /> event.stopPropagation()} onWheelCapture={(event) => event.stopPropagation()} style={{ diff --git a/packages/chat/src/components/media/media.tsx b/packages/chat/src/components/media.tsx similarity index 100% rename from packages/chat/src/components/media/media.tsx rename to packages/chat/src/components/media.tsx diff --git a/packages/chat/src/components/media/mediaGroups.ts b/packages/chat/src/components/media/mediaGroups.ts deleted file mode 100644 index 1103781..0000000 --- a/packages/chat/src/components/media/mediaGroups.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { ChatPickerMediaGroup } from "@tensamin/shared/data"; - -function withoutMedia( - groups: ChatPickerMediaGroup[], - url: string, - keepGroupId?: string, -) { - return groups - .map((group) => ({ - ...group, - media: group.media.filter((item) => item !== url), - })) - .filter((group) => group.id === keepGroupId || group.media.length > 0); -} - -export function assignMediaToGroup( - groups: ChatPickerMediaGroup[], - groupId: string | null, - url: string, -) { - const nextGroups = withoutMedia(groups, url, groupId ?? undefined); - - if (groupId === null) return nextGroups; - - return nextGroups.map((group) => - group.id === groupId ? { ...group, media: [url, ...group.media] } : group, - ); -} - -export function createMediaGroup( - groups: ChatPickerMediaGroup[], - id: string, - name: string, - url: string, -) { - const trimmedName = name.trim(); - - if ( - !trimmedName || - groups.some( - (group) => - group.name.toLocaleLowerCase() === trimmedName.toLocaleLowerCase(), - ) - ) { - return groups; - } - - return [ - ...withoutMedia(groups, url), - { id, name: trimmedName, media: [url] }, - ]; -} - -export function getMediaGroupId(groups: ChatPickerMediaGroup[], url: string) { - return groups.find((group) => group.media.includes(url))?.id ?? null; -} - -export function getUngroupedMedia( - savedMedia: string[], - groups: ChatPickerMediaGroup[], -) { - const groupedMedia = new Set(groups.flatMap((group) => group.media)); - - return savedMedia.filter((url) => !groupedMedia.has(url)); -} diff --git a/packages/chat/src/components/media/mediaSaveButton.tsx b/packages/chat/src/components/media/mediaSaveButton.tsx deleted file mode 100644 index e585eae..0000000 --- a/packages/chat/src/components/media/mediaSaveButton.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { - Button, - cn, - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - Input, - Label, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - useIsMobile, -} from "@methanium/ui"; -import type { ChatPickerMediaGroup } from "@tensamin/shared/data"; -import { useStorage } from "@tensamin/storage/context"; -import { Ellipsis, Star } from "lucide-react"; -import { useEffect, useState } from "react"; -import { - assignMediaToGroup, - createMediaGroup, - getMediaGroupId, -} from "./mediaGroups"; - -export default function MediaSaveButton({ - ariaLabel, - className, - defaultSaved = false, - onSavedMediaChange, - onSavedMediaGroupsChange, - url, -}: { - ariaLabel?: string; - className?: string; - defaultSaved?: boolean; - onSavedMediaChange?: (savedMedia: string[]) => void; - onSavedMediaGroupsChange?: (groups: ChatPickerMediaGroup[]) => void; - url: string; -}) { - const { load, save } = useStorage(); - const isMobile = useIsMobile(); - const [savedMedia, setSavedMedia] = useState(null); - const [groups, setGroups] = useState(null); - const [dialogOpen, setDialogOpen] = useState(false); - const [selectedGroupId, setSelectedGroupId] = useState(null); - const [newGroupName, setNewGroupName] = useState(""); - const isSaved = savedMedia ? savedMedia.includes(url) : defaultSaved; - const sortedGroups = [...(groups ?? [])].sort((left, right) => - left.name.localeCompare(right.name), - ); - const normalizedNewGroupName = newGroupName.trim().toLocaleLowerCase(); - const canCreateGroup = - normalizedNewGroupName.length > 0 && - !(groups ?? []).some( - (group) => group.name.toLocaleLowerCase() === normalizedNewGroupName, - ); - - useEffect(() => { - void Promise.all([ - load("chat_picker_saved_media"), - load("chat_picker_saved_media_groups"), - ]).then(([nextSavedMedia, nextGroups]) => { - setSavedMedia(nextSavedMedia); - setGroups(nextGroups); - }); - }, [load]); - - async function handleSaveClick(event: React.MouseEvent) { - event.stopPropagation(); - - const [currentSavedMedia, currentGroups] = await Promise.all([ - load("chat_picker_saved_media"), - load("chat_picker_saved_media_groups"), - ]); - const willUnsave = currentSavedMedia.includes(url); - const nextSavedMedia = willUnsave - ? currentSavedMedia.filter((item) => item !== url) - : [url, ...currentSavedMedia]; - - setSavedMedia(nextSavedMedia); - onSavedMediaChange?.(nextSavedMedia); - if (willUnsave) { - const nextGroups = assignMediaToGroup(currentGroups, null, url); - setGroups(nextGroups); - onSavedMediaGroupsChange?.(nextGroups); - await Promise.all([ - save("chat_picker_saved_media", nextSavedMedia), - save("chat_picker_saved_media_groups", nextGroups), - ]); - return; - } - - await save("chat_picker_saved_media", nextSavedMedia); - } - - async function handleGroupClick(event: React.MouseEvent) { - event.stopPropagation(); - - const currentGroups = await load("chat_picker_saved_media_groups"); - setGroups(currentGroups); - setSelectedGroupId(getMediaGroupId(currentGroups, url)); - setNewGroupName(""); - setDialogOpen(true); - } - - async function handleGroupSave() { - const currentGroups = await load("chat_picker_saved_media_groups"); - const nextGroups = assignMediaToGroup(currentGroups, selectedGroupId, url); - - setGroups(nextGroups); - onSavedMediaGroupsChange?.(nextGroups); - await save("chat_picker_saved_media_groups", nextGroups); - setDialogOpen(false); - } - - async function handleCreateGroup() { - if (!canCreateGroup) return; - - const currentGroups = await load("chat_picker_saved_media_groups"); - const groupId = crypto.randomUUID(); - const nextGroups = createMediaGroup( - currentGroups, - groupId, - newGroupName, - url, - ); - - setGroups(nextGroups); - setSelectedGroupId(groupId); - setNewGroupName(""); - onSavedMediaGroupsChange?.(nextGroups); - await save("chat_picker_saved_media_groups", nextGroups); - setDialogOpen(false); - } - - return ( - <> -
- - {isSaved ? ( - - ) : null} -
- - - event.stopPropagation()}> - - Organize saved media - - -
-
- - -
- -
- -
- setNewGroupName(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Enter") void handleCreateGroup(); - }} - /> - -
-
-
- - - - - -
-
- - ); -} diff --git a/packages/chat/src/components/mediaSaveButton.tsx b/packages/chat/src/components/mediaSaveButton.tsx new file mode 100644 index 0000000..99fbcec --- /dev/null +++ b/packages/chat/src/components/mediaSaveButton.tsx @@ -0,0 +1,63 @@ +import { Button, cn, useIsMobile } from "@methanium/ui"; +import { useStorage } from "@tensamin/storage/context"; +import { Star } from "lucide-react"; +import { useEffect, useState } from "react"; + +export default function MediaSaveButton({ + ariaLabel, + className, + defaultSaved = false, + onSavedMediaChange, + url, +}: { + ariaLabel?: string; + className?: string; + defaultSaved?: boolean; + onSavedMediaChange?: (savedMedia: string[]) => void; + url: string; +}) { + const { load, save } = useStorage(); + const isMobile = useIsMobile(); + const [savedMedia, setSavedMedia] = useState(null); + const isSaved = savedMedia ? savedMedia.includes(url) : defaultSaved; + + useEffect(() => { + void load("chat_picker_saved_media").then(setSavedMedia); + }, [load]); + + async function handleClick(event: React.MouseEvent) { + event.stopPropagation(); + + const currentSavedMedia = + savedMedia ?? (await load("chat_picker_saved_media")); + const nextSavedMedia = currentSavedMedia.includes(url) + ? currentSavedMedia.filter((item) => item !== url) + : [url, ...currentSavedMedia]; + + setSavedMedia(nextSavedMedia); + onSavedMediaChange?.(nextSavedMedia); + void save("chat_picker_saved_media", nextSavedMedia); + } + + return ( + + ); +} diff --git a/packages/chat/src/components/message.tsx b/packages/chat/src/components/message.tsx index cbcd37e..4dd6624 100644 --- a/packages/chat/src/components/message.tsx +++ b/packages/chat/src/components/message.tsx @@ -13,14 +13,14 @@ import { Skeleton, } from "@methanium/ui"; import MessageContextMenu from "./messageContextMenu"; -import Media from "./media/media"; +import Media from "./media"; import { useStorage } from "@tensamin/storage/context"; import { useMTP } from "@tensamin/mtp"; import { getMessage, useChat } from "../context"; import { decryptChatText, encryptChatText } from "@tensamin/crypto/chatSecret"; import { log, toast } from "@tensamin/shared/log"; import { Emoji, Input, normalizeShortcode, Text } from "@methanium/ui/markdown"; -import { useRecordEmojiUse } from "./emoji/emojiRanks"; +import { useRecordEmojiUse } from "./emojiRanks"; import ReplyBox from "./replyBox"; import { useHotkey } from "@tensamin/hotkeys"; import { cancelMessageEditHotkey } from "../hotkeys"; @@ -381,8 +381,8 @@ function MessageComponent({
)} {editing ? ( -
- +
+