From a658eaf65ccefcf082c515f3868a6eb68b663143 Mon Sep 17 00:00:00 2001 From: echo Date: Fri, 3 Apr 2026 05:29:39 +0200 Subject: [PATCH] chat needs redesign --- apps/admin/data/fitai.db | Bin 360448 -> 360448 bytes apps/mobile/src/app/(tabs)/chat.tsx | 474 ++++++++++++++++++++-------- 2 files changed, 346 insertions(+), 128 deletions(-) diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 123a239087a651267400deba5ee03ef082c708a6..5f9356d15163381a4f50f77bdc192fe54755d10d 100644 GIT binary patch delta 573 zcmZo@5Nl`FGJUR2csAuMR)7PA1;WI$@0ve2e(1`9k<~`PhIKJmH-l6342-Y&@BF`Ufy0ng`6-UJ=LIp$GuRu&urT delta 254 zcmZo@5Nl`F`@1A*H)HCzEzWcTU7Ebx; zH_x#MumHKX(`EZv&QIS3<#ueFz`~*+#m)JKfxn#ZEnhV6d0tnZ?K~RX{oL$awVZD@ zD+-+B-2P@Kiv*h#H{Tit{zv?W`Iqpw@~82;^K0;P^1b3a&9`Q=qCp?u^t^K{eA|I0 zdofCJ^V%}-JMh)>+40))ALMW3XW%=)`;T`YZyivnJn!^9cUX+J=ig=NcVYs%P=S@j v0BCv$1OF}l&HO$5sr+{Q;(Q | null>(null); + const activeMessages = useMemo( () => (activeThreadId ? (messagesByThreadId[activeThreadId] ?? []) : []), [activeThreadId, messagesByThreadId], ); + const typingUsers = useMemo( () => (activeThreadId ? (typingByThreadId[activeThreadId] ?? []) : []), [activeThreadId, typingByThreadId], ); + const activeThread = useMemo( + () => threads.find((thread) => thread.id === activeThreadId) ?? null, + [activeThreadId, threads], + ); + + const orderedThreads = useMemo( + () => + [...threads].sort((a, b) => { + const aTime = a.lastMessageAt ?? a.createdAt; + const bTime = b.lastMessageAt ?? b.createdAt; + return new Date(bTime).getTime() - new Date(aTime).getTime(); + }), + [threads], + ); + const handleSend = async () => { if (!activeThreadId) { return; @@ -73,8 +95,7 @@ export default function ChatScreen() { return; } - const isTyping = value.trim().length > 0; - setTyping(activeThreadId, isTyping); + setTyping(activeThreadId, value.trim().length > 0); if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); @@ -109,54 +130,97 @@ export default function ChatScreen() { return ( - - - {socketConnected ? "Realtime connected" : "Realtime disconnected"} - - - {totalUnreadCount} unread total - + + - - + + + + Conversations + {loadingThreads ? ( - + + + ) : ( item.id} + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.threadListContent} renderItem={({ item }) => { const selected = item.id === activeThreadId; return ( - setActiveThread(item.id)} - style={[ - styles.threadItem, - { - backgroundColor: selected - ? colors.surface - : "transparent", - borderColor: colors.border, - }, - ]} - > - setActiveThread(item.id)}> + - {item.type === "gym" ? "Gym Room" : "Trainer Chat"} - - - {item.lastMessageBody ?? "No messages yet"} - - {item.unreadCount > 0 && ( - - {item.unreadCount} unread + + + + + {item.type === "gym" ? "Gym" : "Direct"} + + + {item.unreadCount > 0 && ( + + + {item.unreadCount > 9 ? "9+" : item.unreadCount} + + + )} + + + + {item.type === "gym" ? "Gym Room" : "Trainer Chat"} - )} + + + {item.lastMessageBody ?? "No messages yet"} + + ); }} @@ -164,22 +228,63 @@ export default function ChatScreen() { )} - + {!activeThreadId ? ( - - - Select a thread to start chatting + + + + Pick a chat - + + Choose a conversation on the left to start messaging. + + ) : ( <> + + + {activeThread?.type === "gym" ? "Gym Room" : "Trainer Chat"} + + + {socketConnected + ? "Live updates enabled" + : "Offline sync active"} + + + {loadingMessages ? ( - + + + ) : ( item.id} - contentContainerStyle={styles.messagesList} + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.messagesContent} refreshControl={ - {isLoadingOlderMessages(activeThreadId) ? ( - - ) : ( - - Load older messages - - )} + + + {isLoadingOlderMessages(activeThreadId) ? ( + + ) : ( + + Load earlier messages + + )} + ) : null } @@ -219,12 +328,10 @@ export default function ChatScreen() { const threadReads = activeThreadId ? (readByThreadId[activeThreadId] ?? {}) : {}; - const readers = Object.entries(threadReads) - .filter( - ([readerUserId, messageId]) => - readerUserId !== userId && messageId === item.id, - ) - .map(([readerUserId]) => readerUserId); + const seen = Object.entries(threadReads).some( + ([readerUserId, messageId]) => + readerUserId !== userId && messageId === item.id, + ); return ( {item.body} - {own && readers.length > 0 && ( + - Seen + {formatTime(item.createdAt)} - )} + {own && seen && ( + + Seen + + )} + ); }} @@ -265,7 +390,17 @@ export default function ChatScreen() { {typingUsers.length > 0 && ( - + + {typingUsers.length === 1 ? "Someone is typing..." : `${typingUsers.length} people are typing...`} @@ -273,17 +408,22 @@ export default function ChatScreen() { )} - + @@ -291,95 +431,173 @@ export default function ChatScreen() { onPress={handleSend} style={[ styles.sendButton, - { backgroundColor: colors.primary }, + { + backgroundColor: draft.trim() + ? colors.primary + : colors.border, + }, ]} > - - Send - + )} - + ); } +function formatTime(isoDate: string): string { + const date = new Date(isoDate); + if (Number.isNaN(date.getTime())) { + return ""; + } + + return date.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); +} + const styles = StyleSheet.create({ container: { flex: 1, }, - connection: { - borderBottomWidth: 1, + headerWrap: { paddingHorizontal: 16, - paddingVertical: 8, + paddingTop: 16, + paddingBottom: 8, }, - layout: { + content: { flex: 1, flexDirection: "row", + paddingHorizontal: 12, + paddingBottom: 8, + gap: 10, }, - sidebar: { - width: 160, - borderRightWidth: 1, - padding: 8, + threadRail: { + width: 148, }, - threadItem: { - borderWidth: 1, - borderRadius: 12, - padding: 10, + railLabel: { marginBottom: 8, }, - chatPane: { - flex: 1, - borderLeftWidth: 0, - }, - emptyState: { + loaderWrap: { flex: 1, alignItems: "center", justifyContent: "center", }, - messagesList: { - padding: 12, + threadListContent: { + gap: 8, + paddingBottom: 20, + }, + threadCard: { + borderRadius: 16, + }, + threadCardTopRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 8, + }, + threadTypeChip: { + flexDirection: "row", + alignItems: "center", + gap: 4, + }, + threadTypeText: { + fontSize: 11, + fontWeight: "700", + }, + threadUnreadBadge: { + minWidth: 18, + height: 18, + borderRadius: 9, + alignItems: "center", + justifyContent: "center", + paddingHorizontal: 4, + }, + threadUnreadText: { + color: "white", + fontSize: 10, + fontWeight: "700", + }, + threadTitle: { + fontSize: 13, + fontWeight: "700", + }, + threadPreview: { + fontSize: 11, + marginTop: 4, + lineHeight: 15, + }, + chatArea: { + flex: 1, + }, + emptyCard: { + flex: 1, + alignItems: "center", + justifyContent: "center", + marginBottom: 8, + }, + chatHeadCard: { + borderRadius: 14, + marginBottom: 8, + }, + messagesContent: { + paddingHorizontal: 6, + paddingBottom: 12, gap: 8, }, - loadOlder: { - borderWidth: 1, - borderRadius: 10, - paddingHorizontal: 10, - paddingVertical: 8, + loadOlderCard: { alignSelf: "center", + borderRadius: 12, marginBottom: 10, }, messageBubble: { - borderRadius: 12, borderWidth: 1, - paddingHorizontal: 10, - paddingVertical: 8, - maxWidth: "80%", + borderRadius: 16, + paddingHorizontal: 12, + paddingVertical: 9, + maxWidth: "84%", }, - composer: { - borderTopWidth: 1, - padding: 10, + metaRow: { + marginTop: 4, flexDirection: "row", - gap: 8, alignItems: "center", }, typingRow: { - paddingHorizontal: 14, + paddingHorizontal: 6, paddingBottom: 6, + flexDirection: "row", + alignItems: "center", + gap: 4, + }, + composer: { + borderTopWidth: 1, + paddingTop: 8, + paddingBottom: 10, + flexDirection: "row", + alignItems: "flex-end", + gap: 8, }, input: { flex: 1, borderWidth: 1, - borderRadius: 10, - paddingHorizontal: 10, - paddingVertical: 8, + borderRadius: 14, + paddingHorizontal: 12, + paddingVertical: 9, + minHeight: 42, + maxHeight: 110, + textAlignVertical: "top", + fontSize: 14, }, sendButton: { - borderRadius: 10, - paddingHorizontal: 14, - paddingVertical: 10, + width: 40, + height: 40, + borderRadius: 20, + alignItems: "center", + justifyContent: "center", }, });