Fix get message being called for each convo

This commit is contained in:
MartinBraquet
2026-03-07 13:21:36 +01:00
parent 2d5690cea2
commit 1165927337
8 changed files with 114 additions and 35 deletions

View File

@@ -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<k>} = {
'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,

View File

@@ -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<number, PrivateChatMessage>,
)
}

View File

@@ -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<User>) => {
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
}

View File

@@ -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<number, PrivateChatMessage>,
summary: 'Get last message for each channel',
tag: 'Messages',
},
'get-channel-seen-time': {
method: 'GET',
authed: true,

View File

@@ -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<Row<'users'>>): Partial<User> {
return removeUndefinedProps(toDb(row))
return removeUndefinedProps(toTS(row))
}
// Reciprocal of convertUser, from typescript to DB
export function convertUserToDb(user: Partial<User>): Partial<Row<'users'>>
export function convertUserToDb(user: Partial<User> | null): Partial<Row<'users'>> | null {
export function convertUserToSQL(user: Partial<User>): Partial<Row<'users'>>
export function convertUserToSQL(user: Partial<User> | null): Partial<Row<'users'>> | 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<Row<'users'>>>(user, {
created_time: millisToTs as any,
})
}

View File

@@ -147,8 +147,9 @@ export const convertSQLtoTS = <R extends Selectable, T extends Record<string, an
else return {...newRows} as T
}
export const convertObjectToSQLRow = <T extends Record<string, any>, R extends Selectable>(
export const convertObjectToSQLRow = <R extends Selectable, T extends Record<string, any>>(
objData: Partial<T>,
converters: TypeConverter<any, any>,
) => {
const entries = Object.entries(objData)
@@ -156,9 +157,12 @@ export const convertObjectToSQLRow = <T extends Record<string, any>, 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)

View File

@@ -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<number, PrivateChatMessage> {
const key = `last-messages-${userId}`
const [lastMessages, setLastMessages] = usePersistentLocalState<
Record<number, PrivateChatMessage>
>({}, 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
}

View File

@@ -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 && <BannedBadge />}
</span>
<span className={'text-ink-400 dark:text-ink-500 text-xs'}>
{chat && <RelativeTimestamp time={chat.createdTime} />}
{lastMessage && <RelativeTimestamp time={lastMessage.createdTime} />}
</span>
</Row>
<Row className="items-center justify-between gap-1">
@@ -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)}
</>
)}
</span>