From 1165927337eba950ed0ddb7aaa1b0448b92215ad Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Sat, 7 Mar 2026 13:21:36 +0100 Subject: [PATCH] Fix get message being called for each convo --- backend/api/src/app.ts | 2 ++ backend/api/src/get-last-messages.ts | 33 +++++++++++++++++++++ backend/shared/src/supabase/users.ts | 4 +-- common/src/api/schema.ts | 11 +++++++ common/src/supabase/users.ts | 41 ++++++++++++-------------- common/src/supabase/utils.ts | 8 +++-- web/hooks/use-last-private-messages.ts | 30 +++++++++++++++++++ web/pages/messages/index.tsx | 20 +++++++------ 8 files changed, 114 insertions(+), 35 deletions(-) create mode 100644 backend/api/src/get-last-messages.ts create mode 100644 web/hooks/use-last-private-messages.ts diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index 50a11187..75da5393 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -6,6 +6,7 @@ import {createVote} from 'api/create-vote' import {deleteMessage} from 'api/delete-message' import {editMessage} from 'api/edit-message' import {getHiddenProfiles} from 'api/get-hidden-profiles' +import {getLastMessages} from 'api/get-last-messages' import {getMessagesCountEndpoint} from 'api/get-messages-count' import {getOptions} from 'api/get-options' import { @@ -597,6 +598,7 @@ const handlers: {[k in APIPath]: APIHandler} = { 'get-channel-memberships': getChannelMemberships, 'get-channel-messages': getChannelMessagesEndpoint, 'get-channel-seen-time': getLastSeenChannelTime, + 'get-last-messages': getLastMessages, 'get-compatibility-questions': getCompatibilityQuestions, 'get-likes-and-ships': getLikesAndShips, 'get-messages-count': getMessagesCountEndpoint, diff --git a/backend/api/src/get-last-messages.ts b/backend/api/src/get-last-messages.ts new file mode 100644 index 00000000..6f9e383f --- /dev/null +++ b/backend/api/src/get-last-messages.ts @@ -0,0 +1,33 @@ +import {PrivateChatMessage} from 'common/chat-message' +import {createSupabaseDirectClient} from 'shared/supabase/init' +import {convertPrivateChatMessage} from 'shared/supabase/messages' + +import {APIHandler} from './helpers/endpoint' + +export const getLastMessages: APIHandler<'get-last-messages'> = async (props, auth) => { + const pg = createSupabaseDirectClient() + const {channelIds} = props + + const messages = await pg.map( + `select distinct on (channel_id) channel_id, id, user_id, content, created_time, visibility, ciphertext, iv, tag + from private_user_messages + where visibility != 'system_status' + and channel_id in ( + select channel_id from private_user_message_channel_members + where user_id = $1 and not status = 'left' + ) + ${channelIds ? 'and channel_id = any ($2)' : ''} + order by channel_id, created_time desc + `, + [auth.uid, channelIds], + convertPrivateChatMessage, + ) + + return messages.reduce( + (acc, msg) => { + acc[Number(msg.channelId)] = msg + return acc + }, + {} as Record, + ) +} diff --git a/backend/shared/src/supabase/users.ts b/backend/shared/src/supabase/users.ts index f4d7b186..9932b906 100644 --- a/backend/shared/src/supabase/users.ts +++ b/backend/shared/src/supabase/users.ts @@ -1,5 +1,5 @@ import {ProfileRow} from 'common/profiles/profile' -import {convertUserToDb} from 'common/supabase/users' +import {convertUserToSQL} from 'common/supabase/users' import {User} from 'common/user' import {removeUndefinedProps} from 'common/util/object' import {createSupabaseDirectClient, SupabaseDirectClient} from 'shared/supabase/init' @@ -20,7 +20,7 @@ export const updateUser = async (id: string, updated: Partial) => { if (!updated) return const fullUpdate = {id, ...updated} const pg = createSupabaseDirectClient() - const result = await update(pg, 'users', 'id', convertUserToDb(fullUpdate)) + const result = await update(pg, 'users', 'id', convertUserToSQL(fullUpdate)) broadcastUpdatedUser(fullUpdate) return result } diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 6d921dad..4410a889 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -871,6 +871,17 @@ export const API = (_apiTypeCheck = { summary: 'Retrieve messages for a private channel', tag: 'Messages', }, + 'get-last-messages': { + method: 'GET', + authed: true, + rateLimited: false, + props: z.object({ + channelIds: z.array(z.coerce.number()).optional(), + }), + returns: {} as Record, + summary: 'Get last message for each channel', + tag: 'Messages', + }, 'get-channel-seen-time': { method: 'GET', authed: true, diff --git a/common/src/supabase/users.ts b/common/src/supabase/users.ts index 2830b9a1..581c9bc8 100644 --- a/common/src/supabase/users.ts +++ b/common/src/supabase/users.ts @@ -1,48 +1,45 @@ import {PrivateUser, User} from 'common/user' import {removeUndefinedProps} from 'common/util/object' -import {millisToTs, Row, run, SupabaseClient, tsToMillis} from './utils' +import { + convertObjectToSQLRow, + convertSQLtoTS, + millisToTs, + Row, + run, + SupabaseClient, + tsToMillis, +} from './utils' export async function getUserForStaticProps(db: SupabaseClient, username: string) { const {data} = await run(db.from('users').select().ilike('username', username)) return convertUser(data[0] ?? null) } -function toDb(row: any): any { - return { - ...(row.data as any), - id: row.id, - username: row.username, - name: row.name, - avatarUrl: row.avatar_url, - isBannedFromPosting: row.is_banned_from_posting, - createdTime: row.created_time ? tsToMillis(row.created_time) : undefined, - } +function toTS(row: any): any { + return convertSQLtoTS<'users', User>(row, { + created_time: tsToMillis as any, + }) } // From DB to typescript export function convertUser(row: Row<'users'>): User export function convertUser(row: Row<'users'> | null): User | null { if (!row) return null - return toDb(row) + return toTS(row) } export function convertPartialUser(row: Partial>): Partial { - return removeUndefinedProps(toDb(row)) + return removeUndefinedProps(toTS(row)) } // Reciprocal of convertUser, from typescript to DB -export function convertUserToDb(user: Partial): Partial> -export function convertUserToDb(user: Partial | null): Partial> | null { +export function convertUserToSQL(user: Partial): Partial> +export function convertUserToSQL(user: Partial | null): Partial> | null { if (!user) return null - return removeUndefinedProps({ - id: user.id, - username: user.username, - name: user.name, - avatar_url: user.avatarUrl, - is_banned_from_posting: user.isBannedFromPosting, - created_time: millisToTs(user.createdTime), + return convertObjectToSQLRow<'users', Partial>>(user, { + created_time: millisToTs as any, }) } diff --git a/common/src/supabase/utils.ts b/common/src/supabase/utils.ts index d5035730..8d499646 100644 --- a/common/src/supabase/utils.ts +++ b/common/src/supabase/utils.ts @@ -147,8 +147,9 @@ export const convertSQLtoTS = , R extends Selectable>( +export const convertObjectToSQLRow = >( objData: Partial, + converters: TypeConverter, ) => { const entries = Object.entries(objData) @@ -156,9 +157,12 @@ export const convertObjectToSQLRow = , R extends S .map((entry) => { const [key, val] = entry as [string, T[keyof T]] + const convert = converters[key] + if (convert === false) return null const decamelizeKey = key.replace(/([A-Z])/g, '_$1').toLowerCase() + const jsVal = convert != null ? convert(val) : val - return [decamelizeKey, val] + return [decamelizeKey, jsVal] }) .filter((x) => x != null) diff --git a/web/hooks/use-last-private-messages.ts b/web/hooks/use-last-private-messages.ts new file mode 100644 index 00000000..2abe8fff --- /dev/null +++ b/web/hooks/use-last-private-messages.ts @@ -0,0 +1,30 @@ +import {PrivateChatMessage} from 'common/chat-message' +import {useEffect} from 'react' +import {usePersistentLocalState} from 'web/hooks/use-persistent-local-state' +import {api} from 'web/lib/api' + +export function useLastPrivateMessages( + userId: string | undefined, + channelIds?: number[] | undefined, +): Record { + const key = `last-messages-${userId}` + const [lastMessages, setLastMessages] = usePersistentLocalState< + Record + >({}, key) + + useEffect(() => { + if (!userId) { + setLastMessages({}) + return + } + + const fetchLastMessages = async () => { + const messages = await api('get-last-messages', {channelIds}) + setLastMessages(messages) + } + + fetchLastMessages() + }, [userId, channelIds?.join(',')]) + + return lastMessages +} diff --git a/web/pages/messages/index.tsx b/web/pages/messages/index.tsx index cc9b93b9..8039e54c 100644 --- a/web/pages/messages/index.tsx +++ b/web/pages/messages/index.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx' +import {PrivateChatMessage} from 'common/chat-message' import {PrivateMessageChannel} from 'common/supabase/private-messages' import {User} from 'common/user' import {parseJsonContentToText} from 'common/util/parse' @@ -15,8 +16,8 @@ import {Avatar} from 'web/components/widgets/avatar' import {Title} from 'web/components/widgets/title' import {BannedBadge} from 'web/components/widgets/user-link' import {useFirebaseUser} from 'web/hooks/use-firebase-user' +import {useLastPrivateMessages} from 'web/hooks/use-last-private-messages' import { - usePrivateMessages, useSortedPrivateMessageMemberships, useUnseenPrivateMessageChannels, } from 'web/hooks/use-private-messages' @@ -54,6 +55,7 @@ export function MessagesContent(props: {currentUser: User}) { const t = useT() const {channels, memberIdsByChannelId} = useSortedPrivateMessageMemberships(currentUser.id) const {lastSeenChatTimeByChannelId} = useUnseenPrivateMessageChannels(currentUser.id, true) + const lastMessages = useLastPrivateMessages(currentUser.id) return ( <> @@ -76,6 +78,7 @@ export function MessagesContent(props: {currentUser: User}) { currentUser={currentUser} channel={channel} lastSeenTime={lastSeenChatTimeByChannelId[channel.channel_id]} + lastMessage={lastMessages[channel.channel_id]} /> ) })} @@ -89,13 +92,12 @@ export const MessageChannelRow = (props: { currentUser: User channel: PrivateMessageChannel lastSeenTime: string + lastMessage?: PrivateChatMessage }) => { - const {otherUserIds, lastSeenTime, currentUser, channel} = props + const {otherUserIds, lastSeenTime, currentUser, channel, lastMessage} = props const channelId = channel.channel_id const otherUsers = useUsersInStore(otherUserIds, `${channelId}`, 100) - const {messages} = usePrivateMessages(channelId, 1, currentUser.id) - const unseen = (messages?.[0]?.createdTimeTs ?? '0') > lastSeenTime - const chat = messages?.[0] + const unseen = (lastMessage?.createdTimeTs ?? '0') > lastSeenTime const numOthers = otherUsers?.length ?? 0 const t = useT() @@ -146,7 +148,7 @@ export const MessageChannelRow = (props: { {isBanned && } - {chat && } + {lastMessage && } @@ -156,10 +158,10 @@ export const MessageChannelRow = (props: { unseen ? '' : 'text-ink-500 dark:text-ink-600', )} > - {chat && ( + {lastMessage && ( <> - {chat.userId == currentUser.id && t('messages.you_prefix', 'You: ')} - {parseJsonContentToText(chat.content)} + {lastMessage.userId == currentUser.id && t('messages.you_prefix', 'You: ')} + {parseJsonContentToText(lastMessage.content)} )}