Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 065f036807 | |||
| f53cd49803 | |||
| 4e48815b91 | |||
| d9b3162f1c | |||
| 6384588122 | |||
| 978e94da90 | |||
| 34c18c13ae | |||
| cca6ea0e37 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "dumpsterChat",
|
"productName": "dumpsterChat",
|
||||||
"version": "0.2.9",
|
"version": "0.2.10",
|
||||||
"identifier": "coffee.dustin.dumpster",
|
"identifier": "coffee.dustin.dumpster",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ export function ChatArea() {
|
|||||||
);
|
);
|
||||||
const isLoading = useMessageStore((s) => s.isLoading);
|
const isLoading = useMessageStore((s) => s.isLoading);
|
||||||
const isLoadingOlder = useMessageStore((s) => s.isLoadingOlder);
|
const isLoadingOlder = useMessageStore((s) => s.isLoadingOlder);
|
||||||
|
const hasMore = useMessageStore((s) => activeChannelId ? s.hasMoreByChannel[activeChannelId] !== false : true);
|
||||||
const fetchMessages = useMessageStore((s) => s.fetchMessages);
|
const fetchMessages = useMessageStore((s) => s.fetchMessages);
|
||||||
const fetchOlderMessages = useMessageStore((s) => s.fetchOlderMessages);
|
const fetchOlderMessages = useMessageStore((s) => s.fetchOlderMessages);
|
||||||
const sendMessage = useMessageStore((s) => s.sendMessage);
|
const sendMessage = useMessageStore((s) => s.sendMessage);
|
||||||
@@ -447,17 +448,16 @@ export function ChatArea() {
|
|||||||
|
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
const el = scrollContainerRef.current;
|
const el = scrollContainerRef.current;
|
||||||
if (!el || !activeChannelId || isLoadingOlder) return;
|
if (!el || !activeChannelId || isLoadingOlder || !hasMore) return;
|
||||||
if (el.scrollTop < 100) {
|
if (el.scrollTop < 100) {
|
||||||
const prevHeight = el.scrollHeight;
|
const prevHeight = el.scrollHeight;
|
||||||
fetchOlderMessages(activeChannelId).then(() => {
|
fetchOlderMessages(activeChannelId).then(() => {
|
||||||
// ponytail: maintain scroll position after prepending older messages
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
el.scrollTop = el.scrollHeight - prevHeight;
|
el.scrollTop = el.scrollHeight - prevHeight;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [activeChannelId, isLoadingOlder, fetchOlderMessages]);
|
}, [activeChannelId, isLoadingOlder, hasMore, fetchOlderMessages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
bottomRef.current?.scrollIntoView({ behavior: "auto" });
|
bottomRef.current?.scrollIntoView({ behavior: "auto" });
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState, useCallback, memo } from "react";
|
import { useEffect, useRef, useState, useCallback, memo, Fragment } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useConversationStore, type ConversationMessage } from "../stores/conversation.ts";
|
import { useConversationStore, type ConversationMessage } from "../stores/conversation.ts";
|
||||||
import { useAuthStore } from "../stores/auth.ts";
|
import { useAuthStore } from "../stores/auth.ts";
|
||||||
@@ -175,6 +175,7 @@ export function DMChat() {
|
|||||||
|
|
||||||
const id = conversationId || activeId;
|
const id = conversationId || activeId;
|
||||||
const conversation = conversations.find((c) => c.id === id);
|
const conversation = conversations.find((c) => c.id === id);
|
||||||
|
const hasMore = useConversationStore((s) => id ? s.hasMoreByConversation[id] !== false : true);
|
||||||
|
|
||||||
const handleAddReaction = useCallback(async (messageId: string, emoji: string) => {
|
const handleAddReaction = useCallback(async (messageId: string, emoji: string) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -189,7 +190,7 @@ export function DMChat() {
|
|||||||
|
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
const el = scrollContainerRef.current;
|
const el = scrollContainerRef.current;
|
||||||
if (!el || !id || isLoadingOlder) return;
|
if (!el || !id || isLoadingOlder || !hasMore) return;
|
||||||
if (el.scrollTop < 100) {
|
if (el.scrollTop < 100) {
|
||||||
const prevHeight = el.scrollHeight;
|
const prevHeight = el.scrollHeight;
|
||||||
fetchOlderMessages(id).then(() => {
|
fetchOlderMessages(id).then(() => {
|
||||||
@@ -198,7 +199,7 @@ export function DMChat() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [id, isLoadingOlder, fetchOlderMessages]);
|
}, [id, isLoadingOlder, hasMore, fetchOlderMessages]);
|
||||||
const messages = id ? messagesByConv[id] || [] : [];
|
const messages = id ? messagesByConv[id] || [] : [];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -329,9 +330,8 @@ export function DMChat() {
|
|||||||
const lastReadId = id ? convStates[id] : undefined;
|
const lastReadId = id ? convStates[id] : undefined;
|
||||||
const showDivider = lastReadId && msg.id === lastReadId && i < messages.length - 1;
|
const showDivider = lastReadId && msg.id === lastReadId && i < messages.length - 1;
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={msg.id}>
|
||||||
<DMMessageItem
|
<DMMessageItem
|
||||||
key={msg.id}
|
|
||||||
msg={msg}
|
msg={msg}
|
||||||
onAddReaction={handleAddReaction}
|
onAddReaction={handleAddReaction}
|
||||||
activeReactionMessageId={activeReactionMessageId}
|
activeReactionMessageId={activeReactionMessageId}
|
||||||
@@ -348,7 +348,7 @@ export function DMChat() {
|
|||||||
<span className="flex-1 border-t border-gb-red"></span>
|
<span className="flex-1 border-t border-gb-red"></span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function LoginForm() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{!isRegister && (
|
{!isRegister && (
|
||||||
<div className="mt-3 text-center">
|
<div className="mt-4 text-center">
|
||||||
<Link
|
<Link
|
||||||
to="/forgot-password"
|
to="/forgot-password"
|
||||||
className="text-gb-yellow hover:text-gb-orange text-xs"
|
className="text-gb-yellow hover:text-gb-orange text-xs"
|
||||||
@@ -127,7 +127,7 @@ export function LoginForm() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isRegister && (
|
{!isRegister && (
|
||||||
<div className="mt-3">
|
<div className="mt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -157,13 +157,13 @@ export function LoginForm() {
|
|||||||
console.error("Passkey login failed:", err);
|
console.error("Passkey login failed:", err);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="terminal-button w-full border-gb-aqua text-gb-aqua"
|
className="terminal-button w-full border-gb-aqua text-gb-aqua text-xs"
|
||||||
>
|
>
|
||||||
[SIGN IN WITH PASSKEY]
|
[SIGN IN WITH PASSKEY]
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-4 text-center">
|
<div className="mt-5 text-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -610,8 +610,8 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(f
|
|||||||
<button type="button" disabled={disabled || uploading}
|
<button type="button" disabled={disabled || uploading}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
title="Upload file"
|
title="Upload file"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faPlus} className={`w-3.5 h-3.5 ${uploading ? "animate-pulse" : ""}`} />
|
<FontAwesomeIcon icon={faPlus} className={`w-4 h-4 ${uploading ? "animate-pulse" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className="text-gb-bg-t mx-1">│</span>
|
<span className="text-gb-bg-t mx-1">│</span>
|
||||||
@@ -619,28 +619,28 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(f
|
|||||||
{/* block formatting */}
|
{/* block formatting */}
|
||||||
<button type="button" disabled={disabled}
|
<button type="button" disabled={disabled}
|
||||||
onClick={() => execBlock("ul")} title="Unordered list"
|
onClick={() => execBlock("ul")} title="Unordered list"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faListUl} className="w-3 h-3" />
|
<FontAwesomeIcon icon={faListUl} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={disabled}
|
<button type="button" disabled={disabled}
|
||||||
onClick={() => execBlock("ol")} title="Ordered list"
|
onClick={() => execBlock("ol")} title="Ordered list"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faListOl} className="w-3 h-3" />
|
<FontAwesomeIcon icon={faListOl} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={disabled}
|
<button type="button" disabled={disabled}
|
||||||
onClick={() => execBlock("blockquote")} title="Blockquote"
|
onClick={() => execBlock("blockquote")} title="Blockquote"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faQuoteRight} className="w-3 h-3" />
|
<FontAwesomeIcon icon={faQuoteRight} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={disabled}
|
<button type="button" disabled={disabled}
|
||||||
onClick={execLink} title="Insert link"
|
onClick={execLink} title="Insert link"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faLink} className="w-3 h-3" />
|
<FontAwesomeIcon icon={faLink} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={disabled}
|
<button type="button" disabled={disabled}
|
||||||
onClick={() => execBlock("h2")} title="Heading"
|
onClick={() => execBlock("h2")} title="Heading"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faHeading} className="w-3 h-3" />
|
<FontAwesomeIcon icon={faHeading} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className="text-gb-bg-t mx-1">│</span>
|
<span className="text-gb-bg-t mx-1">│</span>
|
||||||
@@ -648,43 +648,43 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(f
|
|||||||
{/* inline formatting */}
|
{/* inline formatting */}
|
||||||
<button type="button" disabled={disabled}
|
<button type="button" disabled={disabled}
|
||||||
onClick={() => execInline("**", "**")} title="Bold"
|
onClick={() => execInline("**", "**")} title="Bold"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faBold} className="w-3 h-3" />
|
<FontAwesomeIcon icon={faBold} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={disabled}
|
<button type="button" disabled={disabled}
|
||||||
onClick={() => execInline("*", "*")} title="Italic"
|
onClick={() => execInline("*", "*")} title="Italic"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faItalic} className="w-3 h-3" />
|
<FontAwesomeIcon icon={faItalic} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={disabled}
|
<button type="button" disabled={disabled}
|
||||||
onClick={() => execInline("~~", "~~")} title="Strikethrough"
|
onClick={() => execInline("~~", "~~")} title="Strikethrough"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faStrikethrough} className="w-3 h-3" />
|
<FontAwesomeIcon icon={faStrikethrough} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={disabled}
|
<button type="button" disabled={disabled}
|
||||||
onClick={() => execInline("`", "`")} title="Code"
|
onClick={() => execInline("`", "`")} title="Code"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faCode} className="w-3 h-3" />
|
<FontAwesomeIcon icon={faCode} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={disabled}
|
<button type="button" disabled={disabled}
|
||||||
onClick={() => execInline("||", "||")} title="Spoiler"
|
onClick={() => execInline("||", "||")} title="Spoiler"
|
||||||
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
|
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
|
||||||
<FontAwesomeIcon icon={faEyeSlash} className="w-3 h-3" />
|
<FontAwesomeIcon icon={faEyeSlash} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className="text-gb-bg-t mx-1">│</span>
|
<span className="text-gb-bg-t mx-1">│</span>
|
||||||
|
|
||||||
{/* emoji / kaomoji / gif */}
|
{/* emoji / kaomoji / gif */}
|
||||||
<button type="button" disabled={disabled} onClick={toggleEmoji} title="Emoji"
|
<button type="button" disabled={disabled} onClick={toggleEmoji} title="Emoji"
|
||||||
className={`p-1 disabled:opacity-50 ${showEmoji ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
|
className={`p-1.5 disabled:opacity-50 ${showEmoji ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
|
||||||
<FontAwesomeIcon icon={faSmile} className="w-4 h-4" />
|
<FontAwesomeIcon icon={faSmile} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={disabled} onClick={toggleKaomoji} title="Kaomoji"
|
<button type="button" disabled={disabled} onClick={toggleKaomoji} title="Kaomoji"
|
||||||
className={`p-1 disabled:opacity-50 ${showKaomoji ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
|
className={`p-1.5 disabled:opacity-50 ${showKaomoji ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
|
||||||
<FontAwesomeIcon icon={faGrin} className="w-4 h-4" />
|
<FontAwesomeIcon icon={faGrin} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" disabled={disabled} onClick={toggleGif} title="GIF"
|
<button type="button" disabled={disabled} onClick={toggleGif} title="GIF"
|
||||||
className={`p-1 disabled:opacity-50 ${showGif ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
|
className={`p-1.5 disabled:opacity-50 ${showGif ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
|
||||||
<FontAwesomeIcon icon={faFilm} className="w-4 h-4" />
|
<FontAwesomeIcon icon={faFilm} className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useConversationStore } from "../stores/conversation.ts";
|
import { useConversationStore } from "../stores/conversation.ts";
|
||||||
import { useMemberStore } from "../stores/member.ts";
|
import { useMemberStore } from "../stores/member.ts";
|
||||||
import { useServerStore } from "../stores/server.ts";
|
import { useServerStore } from "../stores/server.ts";
|
||||||
@@ -11,6 +12,7 @@ interface NewConversationModalProps {
|
|||||||
export function NewConversationModal({ onClose }: NewConversationModalProps) {
|
export function NewConversationModal({ onClose }: NewConversationModalProps) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [selected, setSelected] = useState<string[]>([]);
|
const [selected, setSelected] = useState<string[]>([]);
|
||||||
|
const navigate = useNavigate();
|
||||||
const createConversation = useConversationStore((s) => s.createConversation);
|
const createConversation = useConversationStore((s) => s.createConversation);
|
||||||
const activeServerId = useServerStore((s) => s.activeServerId);
|
const activeServerId = useServerStore((s) => s.activeServerId);
|
||||||
const membersByServer = useMemberStore((s) => s.membersByServer);
|
const membersByServer = useMemberStore((s) => s.membersByServer);
|
||||||
@@ -48,7 +50,14 @@ export function NewConversationModal({ onClose }: NewConversationModalProps) {
|
|||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (selected.length === 0) return;
|
if (selected.length === 0) return;
|
||||||
await createConversation(selected);
|
try {
|
||||||
|
const conv = await createConversation(selected);
|
||||||
|
if (conv?.id) {
|
||||||
|
navigate(`/dm/${conv.id}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to create conversation:", err);
|
||||||
|
}
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ if (isTauri) {
|
|||||||
// WebView safe-area fallbacks: Android doesn't support CSS env(), and
|
// WebView safe-area fallbacks: Android doesn't support CSS env(), and
|
||||||
// Linux WebKitGTK chokes on env() inside var(). Set explicit values via JS.
|
// Linux WebKitGTK chokes on env() inside var(). Set explicit values via JS.
|
||||||
if (navigator.userAgent.includes('Android')) {
|
if (navigator.userAgent.includes('Android')) {
|
||||||
document.documentElement.style.setProperty('--safe-top', '1.5rem');
|
document.documentElement.style.setProperty('--safe-top', '2rem');
|
||||||
document.documentElement.style.setProperty('--safe-bottom', '0.75rem');
|
document.documentElement.style.setProperty('--safe-bottom', '0.75rem');
|
||||||
document.documentElement.style.setProperty('--safe-left', '0px');
|
document.documentElement.style.setProperty('--safe-left', '0px');
|
||||||
document.documentElement.style.setProperty('--safe-right', '0px');
|
document.documentElement.style.setProperty('--safe-right', '0px');
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ import { create } from "zustand";
|
|||||||
import { api } from "../lib/api.ts";
|
import { api } from "../lib/api.ts";
|
||||||
import { type Reaction } from "./message.ts";
|
import { type Reaction } from "./message.ts";
|
||||||
|
|
||||||
|
function parseDate(iso: string): number {
|
||||||
|
if (!iso) return 0;
|
||||||
|
const normalized = iso.includes("T") ? iso : iso.replace(" ", "T");
|
||||||
|
const t = new Date(normalized).getTime();
|
||||||
|
return isNaN(t) ? 0 : t;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConversationMember {
|
export interface ConversationMember {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -89,17 +96,26 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
|
|||||||
`/conversations/${conversationId}/messages${params}`,
|
`/conversations/${conversationId}/messages${params}`,
|
||||||
);
|
);
|
||||||
const list = Array.isArray(messages) ? messages : [];
|
const list = Array.isArray(messages) ? messages : [];
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
messagesByConversation: {
|
const existing = state.messagesByConversation[conversationId] || [];
|
||||||
...state.messagesByConversation,
|
const map = new Map<string, ConversationMessage>();
|
||||||
[conversationId]: list,
|
existing.forEach((m) => map.set(m.id, m));
|
||||||
},
|
list.forEach((m) => map.set(m.id, m));
|
||||||
hasMoreByConversation: {
|
const merged = Array.from(map.values()).sort(
|
||||||
...state.hasMoreByConversation,
|
(a, b) => parseDate(a.created_at) - parseDate(b.created_at)
|
||||||
[conversationId]: list.length >= 50,
|
);
|
||||||
},
|
return {
|
||||||
isLoading: false,
|
messagesByConversation: {
|
||||||
}));
|
...state.messagesByConversation,
|
||||||
|
[conversationId]: merged,
|
||||||
|
},
|
||||||
|
hasMoreByConversation: {
|
||||||
|
...state.hasMoreByConversation,
|
||||||
|
[conversationId]: list.length >= 50,
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ isLoading: false, error: error instanceof Error ? error.message : "Failed" });
|
set({ isLoading: false, error: error instanceof Error ? error.message : "Failed" });
|
||||||
}
|
}
|
||||||
@@ -118,17 +134,24 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
|
|||||||
`/conversations/${conversationId}/messages?before=${encodeURIComponent(oldestId)}`,
|
`/conversations/${conversationId}/messages?before=${encodeURIComponent(oldestId)}`,
|
||||||
);
|
);
|
||||||
const list = Array.isArray(older) ? older : [];
|
const list = Array.isArray(older) ? older : [];
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
messagesByConversation: {
|
const map = new Map<string, ConversationMessage>();
|
||||||
...state.messagesByConversation,
|
[...list, ...existing].forEach((m) => map.set(m.id, m));
|
||||||
[conversationId]: [...list, ...existing],
|
const merged = Array.from(map.values()).sort(
|
||||||
},
|
(a, b) => parseDate(a.created_at) - parseDate(b.created_at)
|
||||||
hasMoreByConversation: {
|
);
|
||||||
...state.hasMoreByConversation,
|
return {
|
||||||
[conversationId]: list.length >= 50,
|
messagesByConversation: {
|
||||||
},
|
...state.messagesByConversation,
|
||||||
isLoadingOlder: false,
|
[conversationId]: merged,
|
||||||
}));
|
},
|
||||||
|
hasMoreByConversation: {
|
||||||
|
...state.hasMoreByConversation,
|
||||||
|
[conversationId]: list.length >= 50,
|
||||||
|
},
|
||||||
|
isLoadingOlder: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
set({ isLoadingOlder: false });
|
set({ isLoadingOlder: false });
|
||||||
}
|
}
|
||||||
@@ -156,7 +179,7 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
|
|||||||
messagesByConversation: {
|
messagesByConversation: {
|
||||||
...state.messagesByConversation,
|
...state.messagesByConversation,
|
||||||
[message.conversation_id]: [...existing, message]
|
[message.conversation_id]: [...existing, message]
|
||||||
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()),
|
.sort((a, b) => parseDate(a.created_at) - parseDate(b.created_at)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user