fix(mobile): Chat startet bei neuester Nachricht und Chats nach letzter Aktivität sortieren
Der Verlauf nutzt eine invertierte Liste und öffnet dadurch immer ganz unten. Raumlisten liefern lastActivityAt; die App sortiert alle Chats danach und zieht einen Chat nach dem Senden sofort nach oben. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -242,9 +242,11 @@ export class FederationService {
|
||||
for (const b of bindings) {
|
||||
if (b.status === "pending" && new Date(b.invitation.expiresAt).getTime() > Date.now()) result.invitations.push({ ...b.invitation, id: b.id, remote: true })
|
||||
if (b.status === "active") {
|
||||
const [unread] = await rows(this.service.db, sql`SELECT count(*)::int AS count FROM collaboration_remote_messages WHERE binding_id=${b.id} AND seq>${b.last_read_seq} AND (payload->>'own')='false'`)
|
||||
const [unread] = await rows(this.service.db, sql`SELECT count(*)::int AS count,
|
||||
(SELECT payload->>'createdAt' FROM collaboration_remote_messages WHERE binding_id=${b.id} ORDER BY seq DESC LIMIT 1) AS "lastActivityAt"
|
||||
FROM collaboration_remote_messages WHERE binding_id=${b.id} AND seq>${b.last_read_seq} AND (payload->>'own')='false'`)
|
||||
result.rooms.push({ id: b.id, name: b.snapshot?.name || b.invitation.name, topic: b.snapshot?.topic || b.invitation.topic,
|
||||
ownerName: b.invitation.ownerName, status: b.snapshot?.status || 'active', remote: true, unread: unread.count })
|
||||
ownerName: b.invitation.ownerName, status: b.snapshot?.status || 'active', remote: true, unread: unread.count, lastActivityAt: unread.lastActivityAt || null })
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -144,6 +144,8 @@ export class CollaborationService {
|
||||
t.name AS "ownerName", m.overview_allowed AS "overviewAllowed",
|
||||
(SELECT count(*)::int FROM collaboration_messages cm WHERE cm.room_id=r.id
|
||||
AND cm.seq>=m.history_from_seq AND cm.seq>m.last_read_seq AND cm.principal_id<>${principal.id}) AS unread
|
||||
,coalesce((SELECT max(cm.created_at) FROM collaboration_messages cm WHERE cm.room_id=r.id
|
||||
AND cm.seq>=m.history_from_seq), r.created_at) AS "lastActivityAt"
|
||||
FROM collaboration_members m JOIN collaboration_rooms r ON r.id=m.room_id JOIN tenants t ON t.id=r.tenant_id
|
||||
LEFT JOIN projects p ON p.id=r.project_id
|
||||
WHERE m.principal_id=${principal.id} AND m.status='active' AND (m.expires_at IS NULL OR m.expires_at>now())
|
||||
|
||||
@@ -158,6 +158,16 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
entityUuid: room.entityUuid,
|
||||
exists: true,
|
||||
})
|
||||
// Rooms without messages fall back to their creation date so new rooms still sort by recency.
|
||||
const withActivity = async <T extends { room: typeof communicationRooms.$inferSelect }>(rows: T[]) => {
|
||||
const ids = rows.map(({ room }) => room.id)
|
||||
const latest = ids.length ? await server.db.select({
|
||||
roomId: communicationMessages.roomId,
|
||||
at: sql<Date>`max(${communicationMessages.createdAt})`.mapWith(communicationMessages.createdAt),
|
||||
}).from(communicationMessages).where(inArray(communicationMessages.roomId, ids)).groupBy(communicationMessages.roomId) : []
|
||||
const byRoom = new Map(latest.map((row) => [row.roomId, row.at]))
|
||||
return (room: typeof communicationRooms.$inferSelect) => (byRoom.get(room.id) || room.createdAt).toISOString()
|
||||
}
|
||||
|
||||
const messagesForRoom = async (roomId: string, currentUserId: string, tenantId: number, afterId = 0, limit = 50) => {
|
||||
const conditions = [eq(communicationMessages.roomId, roomId)]
|
||||
@@ -295,7 +305,8 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
eq(communicationRooms.archived, false),
|
||||
eq(communicationRoomMembers.userId, req.user.user_id)
|
||||
)).orderBy(asc(communicationRooms.name))
|
||||
return { rooms: rows.map(({ room }) => roomToApi(room)) }
|
||||
const activity = await withActivity(rows)
|
||||
return { rooms: rows.map(({ room }) => ({ ...roomToApi(room), lastActivityAt: activity(room) })) }
|
||||
})
|
||||
|
||||
server.post("/communication/chat/rooms", async (req: any, reply) => {
|
||||
@@ -335,9 +346,10 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
projectChatAccess(server.db, req),
|
||||
])
|
||||
const roomsByProject = new Map(roomRows.map(({ room }) => [room.entityId, room]))
|
||||
const activity = await withActivity(roomRows)
|
||||
return { rooms: projectRows.map((project) => {
|
||||
const room = roomsByProject.get(project.id)
|
||||
return room ? { ...roomToApi(room), projectId: project.id, projectNumber: project.projectNumber } : {
|
||||
return room ? { ...roomToApi(room), lastActivityAt: activity(room), projectId: project.id, projectNumber: project.projectNumber } : {
|
||||
key: `project_${project.id}`,
|
||||
name: project.projectNumber ? `${project.projectNumber} · ${project.name}` : project.name,
|
||||
topic: `Projektkommunikation zu ${project.name}`,
|
||||
@@ -387,11 +399,13 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
||||
.innerJoin(communicationRoomMembers, eq(communicationRoomMembers.roomId, communicationRooms.id))
|
||||
.where(and(eq(communicationRooms.tenantId, tenantId), eq(communicationRooms.type, "direct"), eq(communicationRoomMembers.userId, req.user.user_id)))
|
||||
const roomsByKey = new Map(existingRows.map(({ room }) => [room.key, room]))
|
||||
const activity = await withActivity(existingRows)
|
||||
return { rooms: users.map((user) => {
|
||||
const key = directRoomKey(req.user.user_id, user.userId)
|
||||
const room = roomsByKey.get(key)
|
||||
return room ? {
|
||||
...roomToApi(room),
|
||||
lastActivityAt: activity(room),
|
||||
name: displayName(user),
|
||||
topic: `Direktnachricht mit ${displayName(user)}`,
|
||||
entityUuid: user.userId,
|
||||
|
||||
Reference in New Issue
Block a user