paginated messages: load latest 100, scroll-up for older
Backend: - Add before_seq + limit params to list_messages - When limit set without after_seq, ORDER BY DESC then reverse (gets latest page) - Reject after_seq + before_seq together (400) - Cap limit at 1000, no default change (CLI compat) Frontend: - Initial load fetches ?limit=100, scrolls to bottom - Scroll near top triggers ?before_seq=lowestSeq&limit=100 - useLayoutEffect maintains scroll position after prepend - Gap repair loops after_seq fetches until caught up - Auto-scroll only when near bottom (doesn't yank while reading) - "loading older..." and "beginning of conversation" indicators Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,8 @@ pub struct AckRequest {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct MessageQuery {
|
pub struct MessageQuery {
|
||||||
pub after_seq: Option<i64>,
|
pub after_seq: Option<i64>,
|
||||||
|
pub before_seq: Option<i64>,
|
||||||
|
pub limit: Option<i64>,
|
||||||
pub r#type: Option<MessageType>,
|
pub r#type: Option<MessageType>,
|
||||||
pub user_id: Option<Uuid>,
|
pub user_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,11 @@ pub async fn list_messages(
|
|||||||
Path(channel_id): Path<String>,
|
Path(channel_id): Path<String>,
|
||||||
Query(query): Query<MessageQuery>,
|
Query(query): Query<MessageQuery>,
|
||||||
) -> Result<Json<Vec<Message>>> {
|
) -> Result<Json<Vec<Message>>> {
|
||||||
|
// Reject conflicting cursors
|
||||||
|
if query.after_seq.is_some() && query.before_seq.is_some() {
|
||||||
|
return Err(AppError::BadRequest("Cannot use both after_seq and before_seq".into()));
|
||||||
|
}
|
||||||
|
|
||||||
let mut sql = String::from(
|
let mut sql = String::from(
|
||||||
"SELECT m.*, u.id as u_id, u.username, u.display_name, u.role, u.created_at as u_created_at \
|
"SELECT m.*, u.id as u_id, u.username, u.display_name, u.role, u.created_at as u_created_at \
|
||||||
FROM messages m JOIN users u ON m.user_id = u.id \
|
FROM messages m JOIN users u ON m.user_id = u.id \
|
||||||
@@ -193,10 +198,16 @@ pub async fn list_messages(
|
|||||||
);
|
);
|
||||||
let mut binds: Vec<String> = vec![channel_id.clone()];
|
let mut binds: Vec<String> = vec![channel_id.clone()];
|
||||||
|
|
||||||
|
// Cursor filtering
|
||||||
if let Some(after_seq) = &query.after_seq {
|
if let Some(after_seq) = &query.after_seq {
|
||||||
sql.push_str(" AND m.seq > ?");
|
sql.push_str(" AND m.seq > ?");
|
||||||
binds.push(after_seq.to_string());
|
binds.push(after_seq.to_string());
|
||||||
}
|
}
|
||||||
|
if let Some(before_seq) = &query.before_seq {
|
||||||
|
sql.push_str(" AND m.seq < ?");
|
||||||
|
binds.push(before_seq.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(msg_type) = &query.r#type {
|
if let Some(msg_type) = &query.r#type {
|
||||||
sql.push_str(" AND m.type = ?");
|
sql.push_str(" AND m.type = ?");
|
||||||
binds.push(match msg_type {
|
binds.push(match msg_type {
|
||||||
@@ -213,7 +224,21 @@ pub async fn list_messages(
|
|||||||
binds.push(user_id.to_string());
|
binds.push(user_id.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When limit is set without after_seq, fetch the LATEST messages
|
||||||
|
// (ORDER BY DESC LIMIT N, then reverse to chronological order)
|
||||||
|
let use_desc = query.limit.is_some() && query.after_seq.is_none();
|
||||||
|
|
||||||
|
if use_desc {
|
||||||
|
sql.push_str(" ORDER BY m.seq DESC");
|
||||||
|
} else {
|
||||||
sql.push_str(" ORDER BY m.seq ASC");
|
sql.push_str(" ORDER BY m.seq ASC");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply limit (capped at 1000)
|
||||||
|
if let Some(limit) = &query.limit {
|
||||||
|
let capped = (*limit).min(1000).max(1);
|
||||||
|
sql.push_str(&format!(" LIMIT {}", capped));
|
||||||
|
}
|
||||||
|
|
||||||
let mut q = sqlx::query_as::<_, MessageWithUserRow>(&sql);
|
let mut q = sqlx::query_as::<_, MessageWithUserRow>(&sql);
|
||||||
for b in &binds {
|
for b in &binds {
|
||||||
@@ -221,7 +246,13 @@ pub async fn list_messages(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let rows = q.fetch_all(&state.db).await?;
|
let rows = q.fetch_all(&state.db).await?;
|
||||||
let messages: Vec<Message> = rows.iter().map(|r| r.to_api_message()).collect();
|
let mut messages: Vec<Message> = rows.iter().map(|r| r.to_api_message()).collect();
|
||||||
|
|
||||||
|
// Reverse DESC results back to chronological order
|
||||||
|
if use_desc {
|
||||||
|
messages.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(messages))
|
Ok(Json(messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import type { Channel } from "@/types/Channel";
|
import type { Channel } from "@/types/Channel";
|
||||||
import type { Message } from "@/types/Message";
|
import type { Message } from "@/types/Message";
|
||||||
import { getChannels, getMessages, getCurrentUsername, deleteMessage, restoreMessage } from "@/api";
|
import { getChannels, getMessages, getCurrentUsername, deleteMessage, restoreMessage } from "@/api";
|
||||||
@@ -43,95 +43,22 @@ export default function App() {
|
|||||||
const [activeChannelId, setActiveChannelId] = useState<string | null>(null);
|
const [activeChannelId, setActiveChannelId] = useState<string | null>(null);
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||||
|
const [hasMoreBefore, setHasMoreBefore] = useState(true);
|
||||||
const [selectedMessages, setSelectedMessages] = useState<{ id: string; username: string; content: string }[]>([]);
|
const [selectedMessages, setSelectedMessages] = useState<{ id: string; username: string; content: string }[]>([]);
|
||||||
const [sheetOpen, setSheetOpen] = useState(false);
|
const [sheetOpen, setSheetOpen] = useState(false);
|
||||||
const [showScrollDown, setShowScrollDown] = useState(false);
|
const [showScrollDown, setShowScrollDown] = useState(false);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const prevMsgCountRef = useRef(0);
|
const prevMsgCountRef = useRef(0);
|
||||||
const maxSeqRef = useRef(0);
|
const maxSeqRef = useRef(0);
|
||||||
|
const initialLoadRef = useRef(true);
|
||||||
|
const pendingPrependRef = useRef(false);
|
||||||
|
const prependScrollHeightRef = useRef(0);
|
||||||
const activeChannelRef = useRef(activeChannelId);
|
const activeChannelRef = useRef(activeChannelId);
|
||||||
|
|
||||||
activeChannelRef.current = activeChannelId;
|
activeChannelRef.current = activeChannelId;
|
||||||
|
|
||||||
const loadChannels = useCallback(async () => {
|
const PAGE_SIZE = 100;
|
||||||
const chs = await getChannels();
|
|
||||||
setChannels(chs);
|
|
||||||
setActiveChannelId((prev) => (prev ? prev : chs[0]?.id ?? null));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadMessages = useCallback(async (afterSeq?: number) => {
|
|
||||||
const channelId = activeChannelRef.current;
|
|
||||||
if (!channelId) return;
|
|
||||||
if (!afterSeq) setLoading(true);
|
|
||||||
try {
|
|
||||||
const params = afterSeq ? { after_seq: afterSeq } : undefined;
|
|
||||||
const msgs = await getMessages(channelId, params);
|
|
||||||
if (activeChannelRef.current === channelId) {
|
|
||||||
if (afterSeq) {
|
|
||||||
// Gap repair: merge new messages, dedup by id
|
|
||||||
setMessages((prev) => {
|
|
||||||
const existing = new Set(prev.map((m) => m.id));
|
|
||||||
const fresh = msgs.filter((m) => !existing.has(m.id));
|
|
||||||
return fresh.length ? [...prev, ...fresh] : prev;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setMessages(msgs);
|
|
||||||
}
|
|
||||||
// Track highest seq for gap repair
|
|
||||||
for (const m of msgs) {
|
|
||||||
const s = Number(m.seq);
|
|
||||||
if (s > maxSeqRef.current) maxSeqRef.current = s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Silently ignore fetch errors
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// WebSocket: append new messages in real-time
|
|
||||||
const handleWsMessage = useCallback((msg: Message) => {
|
|
||||||
setMessages((prev) => {
|
|
||||||
if (prev.some((m) => m.id === msg.id)) return prev;
|
|
||||||
return [...prev, msg];
|
|
||||||
});
|
|
||||||
const s = Number(msg.seq);
|
|
||||||
if (s > maxSeqRef.current) maxSeqRef.current = s;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// WebSocket: replace edited/restored messages
|
|
||||||
const handleWsEdit = useCallback((msg: Message) => {
|
|
||||||
setMessages((prev) => prev.map((m) => (m.id === msg.id ? msg : m)));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// WebSocket: mark deleted messages
|
|
||||||
const handleWsDelete = useCallback((id: string) => {
|
|
||||||
setMessages((prev) =>
|
|
||||||
prev.map((m) =>
|
|
||||||
m.id === id
|
|
||||||
? { ...m, content: "[deleted]", deleted_at: new Date().toISOString(), mentions: [] }
|
|
||||||
: m,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// On WS reconnect/lag, fetch only missed messages
|
|
||||||
const handleWsReconnect = useCallback(() => {
|
|
||||||
loadMessages(maxSeqRef.current || undefined);
|
|
||||||
}, [loadMessages]);
|
|
||||||
|
|
||||||
useChannelSocket(activeChannelId, handleWsMessage, handleWsEdit, handleWsDelete, handleWsReconnect);
|
|
||||||
|
|
||||||
useEffect(() => { loadChannels(); }, [loadChannels]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMessages([]);
|
|
||||||
setSelectedMessages([]);
|
|
||||||
prevMsgCountRef.current = 0;
|
|
||||||
maxSeqRef.current = 0;
|
|
||||||
loadMessages();
|
|
||||||
}, [activeChannelId, loadMessages]);
|
|
||||||
|
|
||||||
function getViewport() {
|
function getViewport() {
|
||||||
const el = scrollRef.current as unknown as HTMLElement | null;
|
const el = scrollRef.current as unknown as HTMLElement | null;
|
||||||
@@ -143,9 +70,175 @@ export default function App() {
|
|||||||
if (vp) vp.scrollTo({ top: vp.scrollHeight, behavior: smooth ? "smooth" : "instant" });
|
if (vp) vp.scrollTo({ top: vp.scrollHeight, behavior: smooth ? "smooth" : "instant" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-scroll only when user is near the bottom
|
function updateSeqRefs(msgs: Message[]) {
|
||||||
|
for (const m of msgs) {
|
||||||
|
const s = Number(m.seq);
|
||||||
|
if (s > maxSeqRef.current) maxSeqRef.current = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadChannels = useCallback(async () => {
|
||||||
|
const chs = await getChannels();
|
||||||
|
setChannels(chs);
|
||||||
|
setActiveChannelId((prev) => (prev ? prev : chs[0]?.id ?? null));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Initial load: fetch latest PAGE_SIZE messages
|
||||||
|
const loadInitialMessages = useCallback(async () => {
|
||||||
|
const channelId = activeChannelRef.current;
|
||||||
|
if (!channelId) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const msgs = await getMessages(channelId, { limit: PAGE_SIZE });
|
||||||
|
if (activeChannelRef.current === channelId) {
|
||||||
|
setMessages(msgs);
|
||||||
|
setHasMoreBefore(msgs.length >= PAGE_SIZE);
|
||||||
|
updateSeqRefs(msgs);
|
||||||
|
initialLoadRef.current = true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load older messages (scroll-up pagination)
|
||||||
|
const loadOlderMessages = useCallback(async () => {
|
||||||
|
const channelId = activeChannelRef.current;
|
||||||
|
if (!channelId || loadingOlder || !hasMoreBefore) return;
|
||||||
|
setLoadingOlder(true);
|
||||||
|
|
||||||
|
// Save scroll height before prepend
|
||||||
|
const vp = getViewport();
|
||||||
|
if (vp) prependScrollHeightRef.current = vp.scrollHeight;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the lowest seq from current messages
|
||||||
|
let lowestSeq = Infinity;
|
||||||
|
setMessages((prev) => {
|
||||||
|
for (const m of prev) {
|
||||||
|
const s = Number(m.seq);
|
||||||
|
if (s < lowestSeq) lowestSeq = s;
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lowestSeq === Infinity) { setLoadingOlder(false); return; }
|
||||||
|
|
||||||
|
const msgs = await getMessages(channelId, { before_seq: lowestSeq, limit: PAGE_SIZE });
|
||||||
|
if (activeChannelRef.current === channelId && msgs.length > 0) {
|
||||||
|
setHasMoreBefore(msgs.length >= PAGE_SIZE);
|
||||||
|
pendingPrependRef.current = true;
|
||||||
|
setMessages((prev) => {
|
||||||
|
const existing = new Set(prev.map((m) => m.id));
|
||||||
|
const fresh = msgs.filter((m) => !existing.has(m.id));
|
||||||
|
return fresh.length ? [...fresh, ...prev] : prev;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setHasMoreBefore(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setLoadingOlder(false);
|
||||||
|
}
|
||||||
|
}, [loadingOlder, hasMoreBefore]);
|
||||||
|
|
||||||
|
// Gap repair: loop after_seq fetches until caught up
|
||||||
|
const repairGap = useCallback(async () => {
|
||||||
|
const channelId = activeChannelRef.current;
|
||||||
|
if (!channelId || !maxSeqRef.current) return;
|
||||||
|
|
||||||
|
let cursor = maxSeqRef.current;
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const msgs = await getMessages(channelId, { after_seq: cursor, limit: PAGE_SIZE });
|
||||||
|
if (activeChannelRef.current !== channelId || msgs.length === 0) break;
|
||||||
|
setMessages((prev) => {
|
||||||
|
const existing = new Set(prev.map((m) => m.id));
|
||||||
|
const fresh = msgs.filter((m) => !existing.has(m.id));
|
||||||
|
return fresh.length ? [...prev, ...fresh] : prev;
|
||||||
|
});
|
||||||
|
updateSeqRefs(msgs);
|
||||||
|
cursor = maxSeqRef.current;
|
||||||
|
if (msgs.length < PAGE_SIZE) break; // caught up
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// WebSocket handlers
|
||||||
|
const handleWsMessage = useCallback((msg: Message) => {
|
||||||
|
setMessages((prev) => {
|
||||||
|
if (prev.some((m) => m.id === msg.id)) return prev;
|
||||||
|
return [...prev, msg];
|
||||||
|
});
|
||||||
|
const s = Number(msg.seq);
|
||||||
|
if (s > maxSeqRef.current) maxSeqRef.current = s;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleWsEdit = useCallback((msg: Message) => {
|
||||||
|
setMessages((prev) => prev.map((m) => (m.id === msg.id ? msg : m)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleWsDelete = useCallback((id: string) => {
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((m) =>
|
||||||
|
m.id === id
|
||||||
|
? { ...m, content: "[deleted]", deleted_at: new Date().toISOString(), mentions: [] }
|
||||||
|
: m,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// On reconnect/lag, repair the gap (don't refetch everything)
|
||||||
|
const handleWsReconnect = useCallback(() => {
|
||||||
|
repairGap();
|
||||||
|
}, [repairGap]);
|
||||||
|
|
||||||
|
useChannelSocket(activeChannelId, handleWsMessage, handleWsEdit, handleWsDelete, handleWsReconnect);
|
||||||
|
|
||||||
|
useEffect(() => { loadChannels(); }, [loadChannels]);
|
||||||
|
|
||||||
|
// Channel switch: reset and load latest
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages.length > prevMsgCountRef.current) {
|
setMessages([]);
|
||||||
|
setSelectedMessages([]);
|
||||||
|
prevMsgCountRef.current = 0;
|
||||||
|
maxSeqRef.current = 0;
|
||||||
|
setHasMoreBefore(true);
|
||||||
|
initialLoadRef.current = true;
|
||||||
|
loadInitialMessages();
|
||||||
|
}, [activeChannelId, loadInitialMessages]);
|
||||||
|
|
||||||
|
// Scroll to bottom on initial load
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialLoadRef.current && messages.length > 0) {
|
||||||
|
initialLoadRef.current = false;
|
||||||
|
// Use requestAnimationFrame to ensure DOM has rendered
|
||||||
|
requestAnimationFrame(() => scrollToBottom());
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Maintain scroll position after prepending older messages
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (pendingPrependRef.current) {
|
||||||
|
pendingPrependRef.current = false;
|
||||||
|
const vp = getViewport();
|
||||||
|
if (vp) {
|
||||||
|
const newHeight = vp.scrollHeight;
|
||||||
|
const delta = newHeight - prependScrollHeightRef.current;
|
||||||
|
vp.scrollTop += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Auto-scroll on new WS messages only when near bottom
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialLoadRef.current && messages.length > prevMsgCountRef.current && !pendingPrependRef.current) {
|
||||||
const vp = getViewport();
|
const vp = getViewport();
|
||||||
const nearBottom = !vp || (vp.scrollHeight - vp.scrollTop - vp.clientHeight < 150);
|
const nearBottom = !vp || (vp.scrollHeight - vp.scrollTop - vp.clientHeight < 150);
|
||||||
if (nearBottom) scrollToBottom();
|
if (nearBottom) scrollToBottom();
|
||||||
@@ -153,13 +246,17 @@ export default function App() {
|
|||||||
prevMsgCountRef.current = messages.length;
|
prevMsgCountRef.current = messages.length;
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// Track scroll position for scroll-down button
|
// Track scroll position for scroll-down button + trigger upward pagination
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const vp = getViewport();
|
const vp = getViewport();
|
||||||
if (!vp) return;
|
if (!vp) return;
|
||||||
function onScroll() {
|
function onScroll() {
|
||||||
const v = vp!;
|
const v = vp!;
|
||||||
setShowScrollDown(v.scrollHeight - v.scrollTop - v.clientHeight > 150);
|
setShowScrollDown(v.scrollHeight - v.scrollTop - v.clientHeight > 150);
|
||||||
|
// Trigger load-older when near top
|
||||||
|
if (v.scrollTop < 200 && !loadingOlder && hasMoreBefore) {
|
||||||
|
loadOlderMessages();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
vp.addEventListener("scroll", onScroll, { passive: true });
|
vp.addEventListener("scroll", onScroll, { passive: true });
|
||||||
return () => vp.removeEventListener("scroll", onScroll);
|
return () => vp.removeEventListener("scroll", onScroll);
|
||||||
@@ -243,7 +340,18 @@ export default function App() {
|
|||||||
no messages yet — start typing below
|
no messages yet — start typing below
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
messages.map((msg, i) => {
|
<>
|
||||||
|
{loadingOlder && (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<span className="text-[10px] font-mono text-muted-foreground animate-pulse">loading older...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasMoreBefore && messages.length > 0 && (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<span className="text-[10px] font-mono text-muted-foreground/40">beginning of conversation</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{messages.map((msg, i) => {
|
||||||
const prev = i > 0 ? messages[i - 1] : null;
|
const prev = i > 0 ? messages[i - 1] : null;
|
||||||
const next = i < messages.length - 1 ? messages[i + 1] : null;
|
const next = i < messages.length - 1 ? messages[i + 1] : null;
|
||||||
const sameSender = prev && prev.user.username === msg.user.username;
|
const sameSender = prev && prev.user.username === msg.user.username;
|
||||||
@@ -305,7 +413,8 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,12 @@ export async function createChannel(body: CreateChannel): Promise<Channel> {
|
|||||||
|
|
||||||
export async function getMessages(
|
export async function getMessages(
|
||||||
channelId: string,
|
channelId: string,
|
||||||
params?: { after_seq?: number; type?: string; user_id?: string },
|
params?: { after_seq?: number; before_seq?: number; limit?: number; type?: string; user_id?: string },
|
||||||
): Promise<Message[]> {
|
): Promise<Message[]> {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
if (params?.after_seq) query.set("after_seq", String(params.after_seq));
|
if (params?.after_seq) query.set("after_seq", String(params.after_seq));
|
||||||
|
if (params?.before_seq) query.set("before_seq", String(params.before_seq));
|
||||||
|
if (params?.limit) query.set("limit", String(params.limit));
|
||||||
if (params?.type) query.set("type", params.type);
|
if (params?.type) query.set("type", params.type);
|
||||||
if (params?.user_id) query.set("user_id", params.user_id);
|
if (params?.user_id) query.set("user_id", params.user_id);
|
||||||
const qs = query.toString();
|
const qs = query.toString();
|
||||||
|
|||||||
Reference in New Issue
Block a user