- Chat list: auth-gated, shows all conversations for current user with other participant's avatar/name, last message preview, and relative time in Macedonian (пред X мин/ч/д) - Chat conversation: FlatList with inverted scroll, message bubbles aligned left (other) and right (self), colored backgrounds, timestamps, KeyboardAvoidingView send bar - Chat mutations: getOrCreate finds or creates a 1:1 chat, send creates message and updates lastMessageAt - Message queries: listByChat with participant auth check - Wired ad detail 'Започни разговор' button to create/open chat - ChatListItem component with avatar, preview, time, link to detail All UI text in Macedonian. TypeScript clean, Convex functions deployed.
102 lines
2.9 KiB
TypeScript
102 lines
2.9 KiB
TypeScript
import { View, Text, Pressable } from "react-native";
|
||
import { Link } from "expo-router";
|
||
import { useTheme } from "./theme";
|
||
import { Avatar } from "./ui/avatar";
|
||
|
||
interface ChatListItemProps {
|
||
chat: {
|
||
_id: string;
|
||
otherUser: {
|
||
_id: string;
|
||
name: string;
|
||
avatarId: string | null;
|
||
} | null;
|
||
lastMessage: {
|
||
content: string;
|
||
createdAt: number;
|
||
senderId: string;
|
||
} | null;
|
||
lastMessageAt: number;
|
||
};
|
||
currentUserId: string;
|
||
}
|
||
|
||
function formatRelativeTime(timestamp: number): string {
|
||
const now = Date.now();
|
||
const diff = now - timestamp;
|
||
const minutes = Math.floor(diff / 60000);
|
||
const hours = Math.floor(diff / 3600000);
|
||
const days = Math.floor(diff / 86400000);
|
||
|
||
if (minutes < 1) return "Сега";
|
||
if (minutes < 60) return `пред ${minutes} мин`;
|
||
if (hours < 24) return `пред ${hours} ч`;
|
||
if (days < 7) return `пред ${days} д`;
|
||
return new Date(timestamp).toLocaleDateString("mk-MK");
|
||
}
|
||
|
||
export function ChatListItem({ chat, currentUserId }: ChatListItemProps) {
|
||
const theme = useTheme();
|
||
|
||
const isSelfMessage = chat.lastMessage?.senderId === currentUserId;
|
||
const previewText = chat.lastMessage
|
||
? (isSelfMessage ? "Вие: " : "") +
|
||
(chat.lastMessage.content.length > 40
|
||
? chat.lastMessage.content.slice(0, 40) + "\u2026"
|
||
: chat.lastMessage.content)
|
||
: "Нема пораки";
|
||
|
||
return (
|
||
<Link href={`/chat/${chat._id}` as any} asChild>
|
||
<Pressable
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
paddingVertical: theme.spacing.md,
|
||
paddingHorizontal: theme.spacing.lg,
|
||
gap: theme.spacing.md,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: theme.colors.border,
|
||
backgroundColor: theme.colors.card,
|
||
}}
|
||
>
|
||
<Avatar
|
||
uri={chat.otherUser?.avatarId ?? null}
|
||
name={chat.otherUser?.name ?? "Непознат"}
|
||
size={50}
|
||
/>
|
||
<View style={{ flex: 1, gap: 2 }}>
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<Text
|
||
style={{ fontSize: 16, fontWeight: "600", color: theme.colors.text, flex: 1 }}
|
||
numberOfLines={1}
|
||
>
|
||
{chat.otherUser?.name ?? "Непознат"}
|
||
</Text>
|
||
{chat.lastMessage && (
|
||
<Text
|
||
style={{ fontSize: 12, color: theme.colors.textTertiary, marginLeft: 8 }}
|
||
>
|
||
{formatRelativeTime(chat.lastMessage.createdAt)}
|
||
</Text>
|
||
)}
|
||
</View>
|
||
<Text
|
||
style={{ fontSize: 14, color: theme.colors.textSecondary }}
|
||
numberOfLines={1}
|
||
>
|
||
{previewText}
|
||
</Text>
|
||
</View>
|
||
</Pressable>
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
export { formatRelativeTime }; |