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)]
|
||||
pub struct MessageQuery {
|
||||
pub after_seq: Option<i64>,
|
||||
pub before_seq: Option<i64>,
|
||||
pub limit: Option<i64>,
|
||||
pub r#type: Option<MessageType>,
|
||||
pub user_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
@@ -186,6 +186,11 @@ pub async fn list_messages(
|
||||
Path(channel_id): Path<String>,
|
||||
Query(query): Query<MessageQuery>,
|
||||
) -> 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(
|
||||
"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 \
|
||||
@@ -193,10 +198,16 @@ pub async fn list_messages(
|
||||
);
|
||||
let mut binds: Vec<String> = vec![channel_id.clone()];
|
||||
|
||||
// Cursor filtering
|
||||
if let Some(after_seq) = &query.after_seq {
|
||||
sql.push_str(" AND m.seq > ?");
|
||||
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 {
|
||||
sql.push_str(" AND m.type = ?");
|
||||
binds.push(match msg_type {
|
||||
@@ -213,7 +224,21 @@ pub async fn list_messages(
|
||||
binds.push(user_id.to_string());
|
||||
}
|
||||
|
||||
sql.push_str(" ORDER BY m.seq ASC");
|
||||
// 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");
|
||||
}
|
||||
|
||||
// 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);
|
||||
for b in &binds {
|
||||
@@ -221,7 +246,13 @@ pub async fn list_messages(
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user