318 lines
9.5 KiB
TypeScript
318 lines
9.5 KiB
TypeScript
import { useState, useCallback } from "react";
|
||
import {
|
||
View,
|
||
Text,
|
||
FlatList,
|
||
TextInput,
|
||
Pressable,
|
||
KeyboardAvoidingView,
|
||
Platform,
|
||
RefreshControl,
|
||
type ListRenderItemInfo,
|
||
} from "react-native";
|
||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||
import { useLocalSearchParams, Stack, useRouter } from "expo-router";
|
||
import { Ionicons } from "@expo/vector-icons";
|
||
import { useQuery, useMutation } from "convex/react";
|
||
import { api } from "../../convex/_generated/api";
|
||
import { useAuth } from "../_layout";
|
||
import { useTheme } from "../../components/theme";
|
||
import { Button } from "../../components/ui/button";
|
||
import { Loading } from "../../components/ui/loading";
|
||
|
||
function formatMessageTime(timestamp: number): string {
|
||
return new Date(timestamp).toLocaleTimeString("mk-MK", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
});
|
||
}
|
||
|
||
export default function ChatDetailScreen() {
|
||
const { id } = useLocalSearchParams<{ id: string }>();
|
||
const theme = useTheme();
|
||
const router = useRouter();
|
||
const insets = useSafeAreaInsets();
|
||
const { token, userId, isAuthenticated } = useAuth();
|
||
|
||
const chat = useQuery(
|
||
api.chats.getById,
|
||
isAuthenticated && token && id ? { token, chatId: id as any } : "skip"
|
||
);
|
||
const messages = useQuery(
|
||
api.messages.listByChat,
|
||
isAuthenticated && token && id ? { token, chatId: id as any } : "skip"
|
||
);
|
||
|
||
const sendMessage = useMutation(api.messages.send);
|
||
const [text, setText] = useState("");
|
||
const [sending, setSending] = useState(false);
|
||
const [refreshing, setRefreshing] = useState(false);
|
||
|
||
const onRefresh = useCallback(() => {
|
||
setRefreshing(true);
|
||
setTimeout(() => setRefreshing(false), 800);
|
||
}, []);
|
||
|
||
async function handleSend() {
|
||
const content = text.trim();
|
||
if (!content || !token || !id) return;
|
||
setText("");
|
||
setSending(true);
|
||
try {
|
||
await sendMessage({ token, chatId: id as any, content });
|
||
} catch {
|
||
setText(content);
|
||
} finally {
|
||
setSending(false);
|
||
}
|
||
}
|
||
|
||
if (!isAuthenticated) {
|
||
return (
|
||
<>
|
||
<Stack.Screen
|
||
options={{
|
||
title: "Разговор",
|
||
headerShown: true,
|
||
headerBackButtonDisplayMode: "minimal",
|
||
}}
|
||
/>
|
||
<View
|
||
style={{
|
||
flex: 1,
|
||
justifyContent: "center",
|
||
alignItems: "center",
|
||
padding: 24,
|
||
backgroundColor: theme.colors.background,
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
...theme.typography.body,
|
||
color: theme.colors.text,
|
||
marginBottom: 16,
|
||
textAlign: "center",
|
||
}}
|
||
>
|
||
Најавете се за да ги видите пораките.
|
||
</Text>
|
||
<Button onPress={() => router.push("/(auth)/login")}>
|
||
Најави се
|
||
</Button>
|
||
</View>
|
||
</>
|
||
);
|
||
}
|
||
|
||
if (!chat || !messages) {
|
||
return (
|
||
<>
|
||
<Stack.Screen
|
||
options={{
|
||
title: "Разговор",
|
||
headerShown: true,
|
||
headerBackButtonDisplayMode: "minimal",
|
||
}}
|
||
/>
|
||
<View
|
||
style={{
|
||
flex: 1,
|
||
justifyContent: "center",
|
||
alignItems: "center",
|
||
backgroundColor: theme.colors.background,
|
||
}}
|
||
>
|
||
<Loading />
|
||
</View>
|
||
</>
|
||
);
|
||
}
|
||
|
||
const otherUserName = chat.otherUser?.name ?? "Непознат";
|
||
|
||
return (
|
||
<>
|
||
<Stack.Screen
|
||
options={{
|
||
title: otherUserName,
|
||
headerShown: true,
|
||
headerBackButtonDisplayMode: "minimal",
|
||
}}
|
||
/>
|
||
<KeyboardAvoidingView
|
||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||
style={{ flex: 1, backgroundColor: theme.colors.background }}
|
||
keyboardVerticalOffset={Platform.OS === "ios" ? 90 : 0}
|
||
>
|
||
{messages.length === 0 ? (
|
||
<View
|
||
style={{
|
||
flex: 1,
|
||
justifyContent: "center",
|
||
alignItems: "center",
|
||
padding: 24,
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
width: 88,
|
||
height: 88,
|
||
borderRadius: 44,
|
||
backgroundColor: theme.colors.primaryLight,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
marginBottom: theme.spacing.md,
|
||
...theme.shadows.sm,
|
||
}}
|
||
>
|
||
<Ionicons name="chatbubbles" size={38} color={theme.colors.primary} />
|
||
</View>
|
||
<Text
|
||
style={{
|
||
...theme.typography.h3,
|
||
color: theme.colors.text,
|
||
marginBottom: theme.spacing.sm,
|
||
}}
|
||
>
|
||
Започнете разговор
|
||
</Text>
|
||
<Text
|
||
style={{
|
||
...theme.typography.body,
|
||
color: theme.colors.textSecondary,
|
||
textAlign: "center",
|
||
}}
|
||
>
|
||
Испратете порака за да започнете разговор.
|
||
</Text>
|
||
</View>
|
||
) : (
|
||
<FlatList
|
||
data={messages}
|
||
keyExtractor={(item) => item._id}
|
||
inverted
|
||
refreshControl={
|
||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
|
||
}
|
||
contentContainerStyle={{
|
||
paddingVertical: 16,
|
||
paddingHorizontal: 16,
|
||
}}
|
||
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
|
||
renderItem={({
|
||
item,
|
||
}: ListRenderItemInfo<typeof messages[number]>) => {
|
||
const isSelf = item.senderId === userId;
|
||
return (
|
||
<View
|
||
style={{
|
||
alignItems: isSelf ? "flex-end" : "flex-start",
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
maxWidth: "78%",
|
||
backgroundColor: isSelf
|
||
? theme.colors.primary
|
||
: theme.colors.card,
|
||
borderRadius: theme.radius.lg,
|
||
paddingHorizontal: theme.spacing.md,
|
||
paddingVertical: 12,
|
||
borderWidth: isSelf ? 0 : 1,
|
||
borderColor: theme.colors.borderLight,
|
||
...(isSelf
|
||
? { borderBottomRightRadius: 4 }
|
||
: { borderBottomLeftRadius: 4 }),
|
||
...theme.shadows.xs,
|
||
}}
|
||
>
|
||
<Text
|
||
style={{
|
||
color: isSelf ? theme.colors.textInverse : theme.colors.text,
|
||
...theme.typography.body,
|
||
lineHeight: 22,
|
||
}}
|
||
>
|
||
{item.content}
|
||
</Text>
|
||
</View>
|
||
<Text
|
||
style={{
|
||
...theme.typography.label,
|
||
color: theme.colors.textTertiary,
|
||
marginTop: 4,
|
||
marginHorizontal: 4,
|
||
}}
|
||
>
|
||
{formatMessageTime(item.createdAt)}
|
||
</Text>
|
||
</View>
|
||
);
|
||
}}
|
||
style={{ flex: 1 }}
|
||
/>
|
||
)}
|
||
<View
|
||
style={{
|
||
flexDirection: "row",
|
||
alignItems: "flex-end",
|
||
paddingHorizontal: 16,
|
||
paddingTop: 12,
|
||
paddingBottom: Math.max(insets.bottom, 12),
|
||
borderTopWidth: 1,
|
||
borderTopColor: theme.colors.borderLight,
|
||
backgroundColor: theme.colors.card,
|
||
gap: theme.spacing.sm,
|
||
...theme.shadows.xs,
|
||
}}
|
||
>
|
||
<TextInput
|
||
accessible
|
||
accessibilityLabel="Порака"
|
||
accessibilityRole="none"
|
||
value={text}
|
||
onChangeText={setText}
|
||
placeholder="Напишете порака..."
|
||
placeholderTextColor={theme.colors.textTertiary}
|
||
style={{
|
||
flex: 1,
|
||
backgroundColor: theme.colors.surface,
|
||
borderWidth: 1.5,
|
||
borderColor: theme.colors.border,
|
||
borderRadius: theme.radius.md,
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 12,
|
||
...theme.typography.body,
|
||
color: theme.colors.text,
|
||
maxHeight: 100,
|
||
}}
|
||
multiline
|
||
maxLength={1000}
|
||
/>
|
||
<Pressable
|
||
accessible
|
||
accessibilityLabel="Испрати порака"
|
||
accessibilityRole="button"
|
||
onPress={handleSend}
|
||
disabled={!text.trim() || sending}
|
||
style={{
|
||
backgroundColor: text.trim()
|
||
? theme.colors.primary
|
||
: theme.colors.border,
|
||
width: 46,
|
||
height: 46,
|
||
borderRadius: 23,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
opacity: sending ? 0.5 : 1,
|
||
...theme.shadows.sm,
|
||
}}
|
||
>
|
||
<Ionicons name="send" size={20} color={theme.colors.textInverse} />
|
||
</Pressable>
|
||
</View>
|
||
</KeyboardAvoidingView>
|
||
</>
|
||
);
|
||
}
|