feat: chat reactions
This commit is contained in:
+51
@@ -119,6 +119,8 @@ export type ChatMessage = {
|
||||
reply_to?: ReplyPreview | null;
|
||||
/** Always an array — media, link cards and shared entities. */
|
||||
attachments: Attachment[];
|
||||
/** Emoji chips, grouped with counts. `mine` marks the viewer's own. */
|
||||
reactions: Reaction[];
|
||||
/**
|
||||
* Client-only. Set on a message shown optimistically before the server has
|
||||
* acknowledged it; never sent by the BFF.
|
||||
@@ -171,6 +173,21 @@ export type AttachmentKind =
|
||||
| "entity"
|
||||
| "quote";
|
||||
|
||||
/** One emoji on a message, grouped across everyone who chose it. */
|
||||
export type Reaction = {
|
||||
emoji: string;
|
||||
count: number;
|
||||
/** The viewer's own choice — highlight the chip, and tapping it removes. */
|
||||
mine: boolean;
|
||||
};
|
||||
|
||||
/** One person's reaction, for the "who reacted" list. */
|
||||
export type ReactionDetail = {
|
||||
user: ChatPeer;
|
||||
emoji: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type Attachment = {
|
||||
id: string;
|
||||
message_id: string;
|
||||
@@ -411,6 +428,40 @@ export const chat = {
|
||||
body: JSON.stringify({ seq }),
|
||||
}),
|
||||
|
||||
/** One hydrated message — reaction counts are per viewer, so a client told
|
||||
* "message X changed" has to ask rather than compute. */
|
||||
message: (conversationId: string, messageId: string) =>
|
||||
call<ChatMessage>(
|
||||
`/chat/conversations/${conversationId}/messages/${messageId}`,
|
||||
),
|
||||
|
||||
/** Tell the thread someone is composing. Ephemeral — nothing is stored. */
|
||||
typing: (conversationId: string) =>
|
||||
call<void>(`/chat/conversations/${conversationId}/typing`, {
|
||||
method: "POST",
|
||||
}),
|
||||
|
||||
/** Who reacted, and with what. Only fetched when the list is opened —
|
||||
* the thread renders from the grouped counts on each message. */
|
||||
reactionDetails: (conversationId: string, messageId: string) =>
|
||||
call<ReactionDetail[]>(
|
||||
`/chat/conversations/${conversationId}/messages/${messageId}/reactions`,
|
||||
),
|
||||
|
||||
/** Set the caller's reaction, replacing any previous one. */
|
||||
react: (conversationId: string, messageId: string, emoji: string) =>
|
||||
call<void>(
|
||||
`/chat/conversations/${conversationId}/messages/${messageId}/reaction`,
|
||||
{ method: "PUT", body: JSON.stringify({ emoji }) },
|
||||
),
|
||||
|
||||
/** Remove the caller's reaction. Idempotent. */
|
||||
unreact: (conversationId: string, messageId: string) =>
|
||||
call<void>(
|
||||
`/chat/conversations/${conversationId}/messages/${messageId}/reaction`,
|
||||
{ method: "DELETE" },
|
||||
),
|
||||
|
||||
markDelivered: (conversationId: string, seq: number) =>
|
||||
call<void>(`/chat/conversations/${conversationId}/delivered`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Whether the app's own chrome should step aside for the page.
|
||||
*
|
||||
* A chat thread is a full-height surface with its own header and a composer
|
||||
* pinned to the bottom: the tab bar competes with the composer for the same
|
||||
* edge, the hamburger duplicates a back button already on screen, and the
|
||||
* wordmark header stacks a second title bar above the thread's own.
|
||||
*
|
||||
* One hook so the header, the nav, the hamburger and the layout's bottom
|
||||
* padding can never disagree — if they did, hiding one would leave a gap
|
||||
* where it used to be. The desktop sidebar is deliberately unaffected: it is
|
||||
* the only way out of a thread on a wide screen.
|
||||
*/
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
/**
|
||||
* True on a thread itself (`/chats/<id>`) but NOT on the inbox or a chat's
|
||||
* profile page: those are ordinary scrolling pages where the tab bar belongs.
|
||||
*/
|
||||
export function useHidesAppChrome(): boolean {
|
||||
const pathname = usePathname();
|
||||
if (!pathname?.startsWith("/chats/")) return false;
|
||||
// "", "chats", "<id>" — anything deeper is a sub-page like /profile.
|
||||
return pathname.split("/").filter(Boolean).length === 2;
|
||||
}
|
||||
Reference in New Issue
Block a user