- Error boundary with Macedonian fallback screen and retry button wrapping all navigation - Pull-to-refresh on all list/detail screens (home, explore, posts, chat, profile, ad detail, post detail) - Accessibility: labels on all interactive elements, roles on Pressable/Button, selectable text for user data - Deep linking: mojmajstor:// scheme configured in app.json, ad/[id] and chat/[id] routes work via Expo Router auto-routing - Pagination: cursor-based paginated queries for ads, posts, messages, chats, reviews with Load More buttons on frontend - Profile: shows customer posts with PostCard, loading/error states, create-post navigation - Auth error messages in Macedonian (Невалидни акредитиви, итн.) All UI text in Macedonian. TypeScript clean, Convex functions deployed.
105 lines
3.1 KiB
TypeScript
105 lines
3.1 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
|
||
accessible
|
||
accessibilityLabel={`Разговор со ${chat.otherUser?.name ?? "Непознат"}: ${previewText}`}
|
||
accessibilityRole="button"
|
||
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 }; |