Implement main screen structure with tab navigation, contacts and profile screens
This commit is contained in:
117
MAIN_SCREEN_CONCEPT.md
Normal file
117
MAIN_SCREEN_CONCEPT.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# Концепция главного экрана Prism Messenger
|
||||||
|
|
||||||
|
## Структура приложения после авторизации
|
||||||
|
|
||||||
|
### 1. Bottom Tab Navigation (Нижняя навигация)
|
||||||
|
|
||||||
|
Основные разделы приложения:
|
||||||
|
|
||||||
|
1. **Чаты** (Chats) - главный раздел
|
||||||
|
2. **Контакты** (Contacts) - список пользователей
|
||||||
|
3. **Профиль** (Profile) - настройки и профиль
|
||||||
|
|
||||||
|
### 2. Экран "Чаты" (главный)
|
||||||
|
|
||||||
|
#### Header (Заголовок):
|
||||||
|
- **Логотип/Название** приложения
|
||||||
|
- **Поиск** (иконка лупы)
|
||||||
|
- **Новый чат** (иконка +)
|
||||||
|
|
||||||
|
#### Основной контент:
|
||||||
|
- **Список активных чатов** с превью последних сообщений
|
||||||
|
- **Индикаторы непрочитанных** сообщений
|
||||||
|
- **Время последнего сообщения**
|
||||||
|
- **Онлайн статус** собеседника
|
||||||
|
- **Swipe actions** для быстрых действий (архив, удаление)
|
||||||
|
|
||||||
|
#### Floating Action Button:
|
||||||
|
- Быстрое создание нового чата
|
||||||
|
|
||||||
|
### 3. Экран "Контакты"
|
||||||
|
|
||||||
|
#### Разделы:
|
||||||
|
- **Поиск пользователей** по username
|
||||||
|
- **Мои контакты** (добавленные пользователи)
|
||||||
|
- **Все пользователи** системы
|
||||||
|
- **Приглашения** (входящие/исходящие)
|
||||||
|
|
||||||
|
#### Функции:
|
||||||
|
- Добавить в контакты
|
||||||
|
- Начать чат
|
||||||
|
- Посмотреть профиль
|
||||||
|
- Заблокировать пользователя
|
||||||
|
|
||||||
|
### 4. Экран "Профиль"
|
||||||
|
|
||||||
|
#### Разделы:
|
||||||
|
- **Мой профиль**:
|
||||||
|
- Аватар (с возможностью изменения)
|
||||||
|
- Username
|
||||||
|
- Email
|
||||||
|
- Био/Статус
|
||||||
|
- Статус "В сети"
|
||||||
|
|
||||||
|
- **Настройки**:
|
||||||
|
- Уведомления
|
||||||
|
- Приватность
|
||||||
|
- Темная тема
|
||||||
|
- Язык приложения
|
||||||
|
|
||||||
|
- **Действия**:
|
||||||
|
- Выйти из аккаунта
|
||||||
|
- Удалить аккаунт
|
||||||
|
|
||||||
|
### 5. Дополнительные функции
|
||||||
|
|
||||||
|
#### Глобальный поиск:
|
||||||
|
- По сообщениям
|
||||||
|
- По пользователям
|
||||||
|
- По названиям чатов
|
||||||
|
|
||||||
|
#### Уведомления:
|
||||||
|
- Push-уведомления о новых сообщениях
|
||||||
|
- Индикаторы на иконках табов
|
||||||
|
|
||||||
|
#### Статусы пользователей:
|
||||||
|
- Онлайн/Оффлайн
|
||||||
|
- Последний визит
|
||||||
|
- "Печатает..."
|
||||||
|
|
||||||
|
## Технические детали реализации
|
||||||
|
|
||||||
|
### Навигация:
|
||||||
|
```typescript
|
||||||
|
// React Navigation структура
|
||||||
|
- Tab.Navigator
|
||||||
|
- Stack.Navigator (Чаты)
|
||||||
|
- ConversationsScreen
|
||||||
|
- ChatScreen
|
||||||
|
- NewChatScreen
|
||||||
|
- Stack.Navigator (Контакты)
|
||||||
|
- ContactsScreen
|
||||||
|
- UserProfileScreen
|
||||||
|
- Stack.Navigator (Профиль)
|
||||||
|
- ProfileScreen
|
||||||
|
- SettingsScreen
|
||||||
|
```
|
||||||
|
|
||||||
|
### GraphQL запросы для главного экрана:
|
||||||
|
1. `getConversations` - список чатов с последними сообщениями
|
||||||
|
2. `getUnreadCount` - количество непрочитанных
|
||||||
|
3. `getOnlineUsers` - список онлайн пользователей
|
||||||
|
4. `searchUsers` - поиск пользователей
|
||||||
|
|
||||||
|
### Компоненты UI:
|
||||||
|
- `TabBar` - кастомная нижняя навигация
|
||||||
|
- `ChatListItem` - элемент списка чатов
|
||||||
|
- `SearchBar` - универсальный поиск
|
||||||
|
- `UserAvatar` - аватар с индикатором онлайн
|
||||||
|
- `UnreadBadge` - бейдж непрочитанных
|
||||||
|
|
||||||
|
## Приоритеты разработки:
|
||||||
|
1. Базовая Tab навигация
|
||||||
|
2. Улучшенный список чатов
|
||||||
|
3. Экран контактов
|
||||||
|
4. Профиль и настройки
|
||||||
|
5. Поиск и фильтрация
|
||||||
|
6. Дополнительные функции
|
@ -4,8 +4,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { LoginScreen } from '../screens/LoginScreen';
|
import { LoginScreen } from '../screens/LoginScreen';
|
||||||
import { RegisterScreen } from '../screens/RegisterScreen';
|
import { RegisterScreen } from '../screens/RegisterScreen';
|
||||||
import { ConversationsScreen } from '../screens/ConversationsScreen';
|
import { MainNavigator } from './MainNavigator';
|
||||||
import { ChatScreen } from '../screens/ChatScreen';
|
|
||||||
import { ActivityIndicator, View } from 'react-native';
|
import { ActivityIndicator, View } from 'react-native';
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator();
|
const Stack = createNativeStackNavigator();
|
||||||
@ -25,23 +24,11 @@ export const AppNavigator = () => {
|
|||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<Stack.Navigator>
|
<Stack.Navigator>
|
||||||
{user ? (
|
{user ? (
|
||||||
<>
|
<Stack.Screen
|
||||||
<Stack.Screen
|
name="Main"
|
||||||
name="Conversations"
|
component={MainNavigator}
|
||||||
component={ConversationsScreen}
|
options={{ headerShown: false }}
|
||||||
options={{
|
/>
|
||||||
title: 'Чаты',
|
|
||||||
headerLargeTitle: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Stack.Screen
|
|
||||||
name="Chat"
|
|
||||||
component={ChatScreen}
|
|
||||||
options={({ route }) => ({
|
|
||||||
title: route.params?.title || 'Чат',
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
|
130
frontend/src/navigation/MainNavigator.tsx
Normal file
130
frontend/src/navigation/MainNavigator.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||||
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
|
import { useTheme } from 'react-native-paper';
|
||||||
|
|
||||||
|
// Импортируем существующие экраны
|
||||||
|
import { ConversationsScreen } from '../screens/ConversationsScreen';
|
||||||
|
import { ChatScreen } from '../screens/ChatScreen';
|
||||||
|
|
||||||
|
// Заглушки для новых экранов (создадим позже)
|
||||||
|
import { ContactsScreen } from '../screens/ContactsScreen';
|
||||||
|
import { ProfileScreen } from '../screens/ProfileScreen';
|
||||||
|
|
||||||
|
const Tab = createBottomTabNavigator();
|
||||||
|
const ChatsStack = createNativeStackNavigator();
|
||||||
|
const ContactsStack = createNativeStackNavigator();
|
||||||
|
const ProfileStack = createNativeStackNavigator();
|
||||||
|
|
||||||
|
// Стек навигации для чатов
|
||||||
|
function ChatsStackNavigator() {
|
||||||
|
return (
|
||||||
|
<ChatsStack.Navigator>
|
||||||
|
<ChatsStack.Screen
|
||||||
|
name="ConversationsList"
|
||||||
|
component={ConversationsScreen}
|
||||||
|
options={{
|
||||||
|
title: 'Чаты',
|
||||||
|
headerLargeTitle: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ChatsStack.Screen
|
||||||
|
name="Chat"
|
||||||
|
component={ChatScreen}
|
||||||
|
options={({ route }) => ({
|
||||||
|
title: route.params?.title || 'Чат',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</ChatsStack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Стек навигации для контактов
|
||||||
|
function ContactsStackNavigator() {
|
||||||
|
return (
|
||||||
|
<ContactsStack.Navigator>
|
||||||
|
<ContactsStack.Screen
|
||||||
|
name="ContactsList"
|
||||||
|
component={ContactsScreen}
|
||||||
|
options={{
|
||||||
|
title: 'Контакты',
|
||||||
|
headerLargeTitle: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ContactsStack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Стек навигации для профиля
|
||||||
|
function ProfileStackNavigator() {
|
||||||
|
return (
|
||||||
|
<ProfileStack.Navigator>
|
||||||
|
<ProfileStack.Screen
|
||||||
|
name="ProfileMain"
|
||||||
|
component={ProfileScreen}
|
||||||
|
options={{
|
||||||
|
title: 'Профиль',
|
||||||
|
headerLargeTitle: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ProfileStack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MainNavigator() {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab.Navigator
|
||||||
|
screenOptions={({ route }) => ({
|
||||||
|
tabBarIcon: ({ focused, color, size }) => {
|
||||||
|
let iconName: string;
|
||||||
|
|
||||||
|
if (route.name === 'Chats') {
|
||||||
|
iconName = focused ? 'message' : 'message-outline';
|
||||||
|
} else if (route.name === 'Contacts') {
|
||||||
|
iconName = focused ? 'account-group' : 'account-group-outline';
|
||||||
|
} else if (route.name === 'Profile') {
|
||||||
|
iconName = focused ? 'account-circle' : 'account-circle-outline';
|
||||||
|
} else {
|
||||||
|
iconName = 'help';
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MaterialCommunityIcons name={iconName} size={size} color={color} />;
|
||||||
|
},
|
||||||
|
tabBarActiveTintColor: theme.colors.primary,
|
||||||
|
tabBarInactiveTintColor: 'gray',
|
||||||
|
headerShown: false,
|
||||||
|
tabBarStyle: {
|
||||||
|
backgroundColor: theme.colors.surface,
|
||||||
|
borderTopColor: theme.colors.outlineVariant,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Chats"
|
||||||
|
component={ChatsStackNavigator}
|
||||||
|
options={{
|
||||||
|
tabBarLabel: 'Чаты',
|
||||||
|
tabBarBadge: undefined, // Здесь можно показывать количество непрочитанных
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Contacts"
|
||||||
|
component={ContactsStackNavigator}
|
||||||
|
options={{
|
||||||
|
tabBarLabel: 'Контакты',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Profile"
|
||||||
|
component={ProfileStackNavigator}
|
||||||
|
options={{
|
||||||
|
tabBarLabel: 'Профиль',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tab.Navigator>
|
||||||
|
);
|
||||||
|
}
|
186
frontend/src/screens/ContactsScreen.tsx
Normal file
186
frontend/src/screens/ContactsScreen.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, StyleSheet, FlatList, ScrollView } from 'react-native';
|
||||||
|
import { Searchbar, List, Avatar, Text, Chip, FAB, Divider, IconButton, SegmentedButtons } from 'react-native-paper';
|
||||||
|
import { useQuery, useMutation } from '@apollo/client';
|
||||||
|
import { GET_USERS } from '../graphql/queries';
|
||||||
|
import { CREATE_PRIVATE_CONVERSATION } from '../graphql/mutations';
|
||||||
|
import { User } from '../types';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
|
export const ContactsScreen = ({ navigation }: any) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [selectedTab, setSelectedTab] = useState('all');
|
||||||
|
const { user: currentUser } = useAuth();
|
||||||
|
|
||||||
|
const { data, loading, refetch } = useQuery(GET_USERS);
|
||||||
|
|
||||||
|
const [createConversation] = useMutation(CREATE_PRIVATE_CONVERSATION, {
|
||||||
|
onCompleted: (data) => {
|
||||||
|
navigation.navigate('Chats', {
|
||||||
|
screen: 'Chat',
|
||||||
|
params: {
|
||||||
|
conversationId: data.createPrivateConversation.id,
|
||||||
|
title: getOtherParticipantName(data.createPrivateConversation),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getOtherParticipantName = (conversation: any) => {
|
||||||
|
const otherParticipant = conversation.participants?.find((p: User) => p.id !== currentUser?.id);
|
||||||
|
return otherParticipant?.username || 'Чат';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartChat = async (userId: string) => {
|
||||||
|
try {
|
||||||
|
await createConversation({
|
||||||
|
variables: { recipientId: userId },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating conversation:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredUsers = data?.users?.filter((user: User) => {
|
||||||
|
if (user.id === currentUser?.id) return false;
|
||||||
|
|
||||||
|
if (searchQuery) {
|
||||||
|
return user.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
user.email.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
const renderUser = ({ item }: { item: User }) => (
|
||||||
|
<>
|
||||||
|
<List.Item
|
||||||
|
title={item.username}
|
||||||
|
description={item.bio || item.email}
|
||||||
|
left={() => (
|
||||||
|
<View style={styles.avatarContainer}>
|
||||||
|
<Avatar.Text
|
||||||
|
size={48}
|
||||||
|
label={item.username.charAt(0).toUpperCase()}
|
||||||
|
/>
|
||||||
|
{item.isOnline && (
|
||||||
|
<View style={styles.onlineIndicator} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
right={() => (
|
||||||
|
<View style={styles.actions}>
|
||||||
|
<IconButton
|
||||||
|
icon="message-outline"
|
||||||
|
size={24}
|
||||||
|
onPress={() => handleStartChat(item.id)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
style={styles.listItem}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.searchContainer}>
|
||||||
|
<Searchbar
|
||||||
|
placeholder="Поиск пользователей..."
|
||||||
|
onChangeText={setSearchQuery}
|
||||||
|
value={searchQuery}
|
||||||
|
style={styles.searchbar}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<SegmentedButtons
|
||||||
|
value={selectedTab}
|
||||||
|
onValueChange={setSelectedTab}
|
||||||
|
buttons={[
|
||||||
|
{ value: 'all', label: 'Все пользователи' },
|
||||||
|
{ value: 'contacts', label: 'Мои контакты' },
|
||||||
|
{ value: 'online', label: 'Онлайн' },
|
||||||
|
]}
|
||||||
|
style={styles.tabs}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
data={filteredUsers.filter((user: User) => {
|
||||||
|
if (selectedTab === 'online') return user.isOnline;
|
||||||
|
// В будущем добавим логику для "Мои контакты"
|
||||||
|
return true;
|
||||||
|
})}
|
||||||
|
renderItem={renderUser}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
onRefresh={refetch}
|
||||||
|
refreshing={loading}
|
||||||
|
contentContainerStyle={styles.listContent}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<View style={styles.emptyContainer}>
|
||||||
|
<Text variant="bodyLarge" style={styles.emptyText}>
|
||||||
|
{searchQuery
|
||||||
|
? 'Пользователи не найдены'
|
||||||
|
: selectedTab === 'online'
|
||||||
|
? 'Нет пользователей онлайн'
|
||||||
|
: 'Нет пользователей'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
},
|
||||||
|
searchContainer: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 8,
|
||||||
|
},
|
||||||
|
searchbar: {
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
marginHorizontal: 16,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
listContent: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
listItem: {
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
avatarContainer: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
onlineIndicator: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
borderRadius: 7,
|
||||||
|
backgroundColor: '#4CAF50',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
emptyContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: 100,
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
});
|
@ -1,23 +1,41 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
|
import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
|
||||||
import { List, Avatar, Text, FAB, Divider, Badge } from 'react-native-paper';
|
import { List, Avatar, Text, FAB, Divider, Badge, Searchbar, IconButton } from 'react-native-paper';
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import { GET_CONVERSATIONS } from '../graphql/queries';
|
import { GET_CONVERSATIONS } from '../graphql/queries';
|
||||||
import { Conversation } from '../types';
|
import { Conversation } from '../types';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { ru } from 'date-fns/locale';
|
import { ru } from 'date-fns/locale';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
export const ConversationsScreen = ({ navigation }: any) => {
|
export const ConversationsScreen = ({ navigation }: any) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const { data, loading, error, refetch } = useQuery(GET_CONVERSATIONS, {
|
const { data, loading, error, refetch } = useQuery(GET_CONVERSATIONS, {
|
||||||
pollInterval: 5000, // Обновляем каждые 5 секунд
|
pollInterval: 5000, // Обновляем каждые 5 секунд
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Фильтрация чатов по поисковому запросу
|
||||||
|
const filteredConversations = data?.conversations?.filter((conv: Conversation) => {
|
||||||
|
if (!searchQuery) return true;
|
||||||
|
|
||||||
|
const otherParticipant = conv.participants.find(p => p.id !== user?.id);
|
||||||
|
const displayName = conv.isGroup ? conv.name : otherParticipant?.username;
|
||||||
|
|
||||||
|
return displayName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
conv.lastMessage?.content.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
}) || [];
|
||||||
|
|
||||||
const renderConversation = ({ item }: { item: Conversation }) => {
|
const renderConversation = ({ item }: { item: Conversation }) => {
|
||||||
const otherParticipant = item.participants.find(p => p.id !== data?.me?.id);
|
const otherParticipant = item.participants.find(p => p.id !== user?.id);
|
||||||
const displayName = item.isGroup ? item.name : otherParticipant?.username;
|
const displayName = item.isGroup ? item.name : otherParticipant?.username;
|
||||||
const lastMessageTime = item.lastMessage
|
const lastMessageTime = item.lastMessage
|
||||||
? format(new Date(item.lastMessage.createdAt), 'HH:mm', { locale: ru })
|
? format(new Date(item.lastMessage.createdAt), 'HH:mm', { locale: ru })
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
// Подсчет непрочитанных сообщений (в будущем добавить в GraphQL)
|
||||||
|
const unreadCount = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@ -42,7 +60,9 @@ export const ConversationsScreen = ({ navigation }: any) => {
|
|||||||
<Text variant="bodySmall" style={styles.time}>
|
<Text variant="bodySmall" style={styles.time}>
|
||||||
{lastMessageTime}
|
{lastMessageTime}
|
||||||
</Text>
|
</Text>
|
||||||
{/* Здесь можно добавить счетчик непрочитанных сообщений */}
|
{unreadCount > 0 && (
|
||||||
|
<Badge style={styles.unreadBadge}>{unreadCount}</Badge>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
style={styles.listItem}
|
style={styles.listItem}
|
||||||
@ -70,8 +90,19 @@ export const ConversationsScreen = ({ navigation }: any) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
{/* Поисковая строка */}
|
||||||
|
<View style={styles.searchContainer}>
|
||||||
|
<Searchbar
|
||||||
|
placeholder="Поиск чатов..."
|
||||||
|
onChangeText={setSearchQuery}
|
||||||
|
value={searchQuery}
|
||||||
|
style={styles.searchbar}
|
||||||
|
icon="magnify"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={data?.conversations || []}
|
data={filteredConversations}
|
||||||
renderItem={renderConversation}
|
renderItem={renderConversation}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
onRefresh={refetch}
|
onRefresh={refetch}
|
||||||
@ -102,6 +133,16 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
},
|
},
|
||||||
|
searchContainer: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 8,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
searchbar: {
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
centerContainer: {
|
centerContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -119,6 +160,13 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
time: {
|
time: {
|
||||||
color: '#666',
|
color: '#666',
|
||||||
|
fontSize: 12,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
unreadBadge: {
|
||||||
|
backgroundColor: '#2196F3',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
onlineBadge: {
|
onlineBadge: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
241
frontend/src/screens/ProfileScreen.tsx
Normal file
241
frontend/src/screens/ProfileScreen.tsx
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, StyleSheet, ScrollView, Alert } from 'react-native';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Text,
|
||||||
|
List,
|
||||||
|
Switch,
|
||||||
|
Divider,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Surface,
|
||||||
|
useTheme,
|
||||||
|
TextInput,
|
||||||
|
Dialog,
|
||||||
|
Portal,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
|
import { UPDATE_USER } from '../graphql/mutations';
|
||||||
|
|
||||||
|
export const ProfileScreen = ({ navigation }: any) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
const [editDialogVisible, setEditDialogVisible] = useState(false);
|
||||||
|
const [bio, setBio] = useState(user?.bio || '');
|
||||||
|
|
||||||
|
const [updateUser] = useMutation(UPDATE_USER, {
|
||||||
|
onCompleted: (data) => {
|
||||||
|
// В реальном приложении нужно обновить контекст пользователя
|
||||||
|
setEditDialogVisible(false);
|
||||||
|
Alert.alert('Успешно', 'Профиль обновлен');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
Alert.alert(
|
||||||
|
'Выход',
|
||||||
|
'Вы уверены, что хотите выйти?',
|
||||||
|
[
|
||||||
|
{ text: 'Отмена', style: 'cancel' },
|
||||||
|
{
|
||||||
|
text: 'Выйти',
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: async () => {
|
||||||
|
await logout();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateProfile = async () => {
|
||||||
|
try {
|
||||||
|
await updateUser({
|
||||||
|
variables: { bio },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Ошибка', 'Не удалось обновить профиль');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView style={styles.container}>
|
||||||
|
{/* Профиль пользователя */}
|
||||||
|
<Surface style={styles.profileSection} elevation={0}>
|
||||||
|
<View style={styles.profileHeader}>
|
||||||
|
<Avatar.Text
|
||||||
|
size={80}
|
||||||
|
label={user?.username?.charAt(0).toUpperCase() || '?'}
|
||||||
|
style={styles.avatar}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="pencil"
|
||||||
|
size={20}
|
||||||
|
onPress={() => setEditDialogVisible(true)}
|
||||||
|
style={styles.editButton}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text variant="headlineSmall" style={styles.username}>
|
||||||
|
{user?.username}
|
||||||
|
</Text>
|
||||||
|
<Text variant="bodyMedium" style={styles.email}>
|
||||||
|
{user?.email}
|
||||||
|
</Text>
|
||||||
|
{user?.bio && (
|
||||||
|
<Text variant="bodyMedium" style={styles.bio}>
|
||||||
|
{user.bio}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Surface>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Настройки */}
|
||||||
|
<List.Section>
|
||||||
|
<List.Subheader>Настройки</List.Subheader>
|
||||||
|
|
||||||
|
<List.Item
|
||||||
|
title="Уведомления"
|
||||||
|
description="Получать push-уведомления"
|
||||||
|
left={(props) => <List.Icon {...props} icon="bell" />}
|
||||||
|
right={() => (
|
||||||
|
<Switch
|
||||||
|
value={notificationsEnabled}
|
||||||
|
onValueChange={setNotificationsEnabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<List.Item
|
||||||
|
title="Темная тема"
|
||||||
|
description="Переключить тему приложения"
|
||||||
|
left={(props) => <List.Icon {...props} icon="theme-light-dark" />}
|
||||||
|
right={() => (
|
||||||
|
<Switch
|
||||||
|
value={darkMode}
|
||||||
|
onValueChange={setDarkMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<List.Item
|
||||||
|
title="Язык"
|
||||||
|
description="Русский"
|
||||||
|
left={(props) => <List.Icon {...props} icon="translate" />}
|
||||||
|
right={(props) => <List.Icon {...props} icon="chevron-right" />}
|
||||||
|
onPress={() => {}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<List.Item
|
||||||
|
title="Приватность"
|
||||||
|
description="Настройки приватности"
|
||||||
|
left={(props) => <List.Icon {...props} icon="lock" />}
|
||||||
|
right={(props) => <List.Icon {...props} icon="chevron-right" />}
|
||||||
|
onPress={() => {}}
|
||||||
|
/>
|
||||||
|
</List.Section>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* О приложении */}
|
||||||
|
<List.Section>
|
||||||
|
<List.Subheader>О приложении</List.Subheader>
|
||||||
|
|
||||||
|
<List.Item
|
||||||
|
title="Версия"
|
||||||
|
description="1.0.0"
|
||||||
|
left={(props) => <List.Icon {...props} icon="information" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<List.Item
|
||||||
|
title="Помощь"
|
||||||
|
left={(props) => <List.Icon {...props} icon="help-circle" />}
|
||||||
|
right={(props) => <List.Icon {...props} icon="chevron-right" />}
|
||||||
|
onPress={() => {}}
|
||||||
|
/>
|
||||||
|
</List.Section>
|
||||||
|
|
||||||
|
{/* Действия */}
|
||||||
|
<View style={styles.actions}>
|
||||||
|
<Button
|
||||||
|
mode="outlined"
|
||||||
|
onPress={handleLogout}
|
||||||
|
style={styles.logoutButton}
|
||||||
|
textColor={theme.colors.error}
|
||||||
|
>
|
||||||
|
Выйти из аккаунта
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Диалог редактирования профиля */}
|
||||||
|
<Portal>
|
||||||
|
<Dialog visible={editDialogVisible} onDismiss={() => setEditDialogVisible(false)}>
|
||||||
|
<Dialog.Title>Редактировать профиль</Dialog.Title>
|
||||||
|
<Dialog.Content>
|
||||||
|
<TextInput
|
||||||
|
label="О себе"
|
||||||
|
value={bio}
|
||||||
|
onChangeText={setBio}
|
||||||
|
multiline
|
||||||
|
numberOfLines={3}
|
||||||
|
mode="outlined"
|
||||||
|
/>
|
||||||
|
</Dialog.Content>
|
||||||
|
<Dialog.Actions>
|
||||||
|
<Button onPress={() => setEditDialogVisible(false)}>Отмена</Button>
|
||||||
|
<Button onPress={handleUpdateProfile}>Сохранить</Button>
|
||||||
|
</Dialog.Actions>
|
||||||
|
</Dialog>
|
||||||
|
</Portal>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
|
profileSection: {
|
||||||
|
padding: 24,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
profileHeader: {
|
||||||
|
position: 'relative',
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
backgroundColor: '#2196F3',
|
||||||
|
},
|
||||||
|
editButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -8,
|
||||||
|
right: -8,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
color: '#666',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
bio: {
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
padding: 16,
|
||||||
|
marginTop: 16,
|
||||||
|
},
|
||||||
|
logoutButton: {
|
||||||
|
borderColor: '#f44336',
|
||||||
|
},
|
||||||
|
});
|
Reference in New Issue
Block a user