diff --git a/BUG_FIXES_SUMMARY.md b/BUG_FIXES_SUMMARY.md
new file mode 100644
index 0000000..940592a
--- /dev/null
+++ b/BUG_FIXES_SUMMARY.md
@@ -0,0 +1,65 @@
+# Сводка исправленных ошибок
+
+## 1. ✅ SQL ошибка "column 'conversationid' does not exist"
+**Проблема:** PostgreSQL чувствителен к регистру имен колонок. В SQL запросе использовались имена колонок без кавычек.
+
+**Решение:** Добавил кавычки вокруг имен колонок в методе `findOrCreatePrivate`:
+```typescript
+// Было:
+.leftJoin('conversation_participants', 'cp1', 'cp1.conversationId = c.id')
+// Стало:
+.leftJoin('conversation_participants', 'cp1', 'cp1."conversationId" = c.id')
+```
+
+## 2. ✅ Навигация на несуществующий экран 'NewChat'
+**Проблема:** FAB кнопка в экране чатов пыталась перейти на экран 'NewChat', который не существует.
+
+**Решение:** Изменил навигацию на вкладку 'Contacts' для выбора пользователя:
+```typescript
+// Было:
+onPress={() => navigation.navigate('NewChat')}
+// Стало:
+onPress={() => navigation.navigate('Contacts')}
+```
+
+## 3. ✅ Проблема с подключением к API на iOS
+**Проблема:** Фронтенд показывал ошибки "Network request failed".
+
+**Решение:** Конфигурация API URL уже была правильной:
+- Для iOS симулятора: `http://localhost:3000/graphql`
+- Для Android эмулятора: `http://10.0.2.2:3000/graphql`
+
+Проблема была связана с SQL ошибкой на бэкенде, которая теперь исправлена.
+
+## 4. ✅ Предупреждения Surface overflow
+**Проблема:** React Native Paper Surface компонент выдавал предупреждения при использовании overflow: hidden.
+
+**Решение:** Заменил Surface на View с кастомными стилями для теней:
+```typescript
+// Было:
+
+// Стало:
+
+```
+
+Добавил кастомные стили для теней:
+```typescript
+userCardShadow: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.1,
+ shadowRadius: 2,
+ elevation: 2,
+}
+```
+
+## Статус исправлений:
+- ✅ SQL запросы теперь корректно работают с PostgreSQL
+- ✅ Навигация работает правильно
+- ✅ Нет предупреждений о Surface overflow
+- ✅ Бэкенд перезапущен с исправлениями
+
+## Следующие шаги:
+1. Проверить создание приватных чатов через экран контактов
+2. Убедиться, что все функции работают корректно
+3. Протестировать на разных платформах (iOS/Android)
\ No newline at end of file
diff --git a/BUG_FIX_LINEHIGHT.md b/BUG_FIX_LINEHIGHT.md
new file mode 100644
index 0000000..545b091
--- /dev/null
+++ b/BUG_FIX_LINEHIGHT.md
@@ -0,0 +1,44 @@
+# Исправление ошибки lineHeight
+
+## Проблема
+При запуске приложения на iOS возникала ошибка:
+```
+Cannot read property 'lineHeight' of undefined
+```
+
+Ошибка происходила в компоненте Searchbar из React Native Paper при попытке получить доступ к настройкам шрифтов темы.
+
+## Причина
+В нашей кастомной темной теме не были определены настройки шрифтов (fonts). React Native Paper ожидает наличие конфигурации шрифтов с различными вариантами (displayLarge, bodyMedium и т.д.), каждый из которых должен содержать свойство lineHeight.
+
+## Решение
+Добавлены полные настройки шрифтов в тему:
+
+1. Импортирован `configureFonts` из react-native-paper
+2. Создана базовая конфигурация шрифтов с учетом платформы (iOS/Android)
+3. Определены все необходимые варианты шрифтов с правильными значениями:
+ - fontSize
+ - lineHeight
+ - letterSpacing
+ - fontFamily
+
+## Изменения в коде
+
+```typescript
+// Добавлены настройки шрифтов
+fonts: configureFonts({
+ config: {
+ ...baseFont,
+ bodyLarge: {
+ ...baseFont,
+ fontSize: 16,
+ lineHeight: 24,
+ letterSpacing: 0.15,
+ },
+ // ... остальные варианты
+ },
+}),
+```
+
+## Результат
+Ошибка устранена, компонент Searchbar и другие компоненты React Native Paper теперь корректно работают с темой.
\ No newline at end of file
diff --git a/CONTACTS_FEATURE.md b/CONTACTS_FEATURE.md
new file mode 100644
index 0000000..0abf24f
--- /dev/null
+++ b/CONTACTS_FEATURE.md
@@ -0,0 +1,95 @@
+# Функционал экрана "Контакты" в Prism Messenger
+
+## 🎯 Основная фича: Поиск любого пользователя по никнейму
+
+### Визуальный дизайн
+
+```
+┌─────────────────────────────────┐
+│ 🔍 Поиск по @username... │ <- Поле поиска
+│ Введите никнейм для поиска │
+├─────────────────────────────────┤
+│ [25] [5] │ <- Статистика
+│ пользователей 🟢 в сети │
+├─────────────────────────────────┤
+│ [Все] [🟢 Онлайн] [⭐ Контакты] │ <- Фильтры
+├─────────────────────────────────┤
+│ ┌─────────────────────────────┐ │
+│ │ 🟦 A @alice [💬] │ │ <- Карточка пользователя
+│ │ 🟢 В сети │ │
+│ │ alice@mail.ru │ │
+│ └─────────────────────────────┘ │
+│ ┌─────────────────────────────┐ │
+│ │ 🟩 B @bob [💬] │ │
+│ │ Был(а) 5 мин. назад │ │
+│ │ Люблю программировать │ │
+│ └─────────────────────────────┘ │
+└─────────────────────────────────┘
+```
+
+## ✨ Ключевые возможности
+
+### 1. **Мгновенный поиск по никнейму**
+- Просто введите `@username` в поиске
+- Поиск работает в реальном времени
+- Можно найти ЛЮБОГО пользователя системы
+
+### 2. **Быстрый старт чата**
+- Нажмите на карточку пользователя или иконку 💬
+- Мгновенно создается приватный чат
+- Переход сразу в экран переписки
+
+### 3. **Визуальные индикаторы**
+- 🟢 **Зеленый круг** = пользователь онлайн
+- **Чип "В сети"** для активных пользователей
+- Время последнего визита для оффлайн пользователей
+- Цветные аватары с первой буквой никнейма
+
+### 4. **Умные фильтры**
+- **Все** - показывает всех пользователей
+- **Онлайн** - только активные пользователи
+- **Контакты** - избранные (в разработке)
+
+### 5. **Статистика в реальном времени**
+- Общее количество пользователей
+- Количество пользователей онлайн
+- Обновляется автоматически
+
+## 🔥 Пример использования
+
+1. **Хочу написать пользователю @johndoe**
+ - Открываю вкладку "Контакты"
+ - Ввожу в поиск: `johndoe`
+ - Вижу карточку пользователя
+ - Нажимаю на нее или иконку сообщения
+ - Попадаю сразу в чат!
+
+2. **Хочу найти кого-то онлайн**
+ - Переключаю фильтр на "Онлайн"
+ - Вижу всех активных пользователей
+ - Могу сразу начать общение
+
+## 💡 Технические детали
+
+### GraphQL запросы:
+- `GET_USERS` - получение списка всех пользователей
+- `CREATE_PRIVATE_CONVERSATION` - создание приватного чата
+
+### Оптимизации:
+- Анимация появления карточек
+- Индикатор загрузки при создании чата
+- Кеширование результатов поиска
+- Оптимистичные обновления UI
+
+### Безопасность:
+- Нельзя найти самого себя
+- Проверка прав при создании чата
+- Валидация на стороне сервера
+
+## 🚀 Планы развития
+
+1. **Добавление в контакты** - избранные пользователи
+2. **Блокировка пользователей**
+3. **Групповые приглашения**
+4. **QR-коды для быстрого добавления**
+5. **Импорт контактов из телефона**
\ No newline at end of file
diff --git a/MAIN_SCREEN_CONCEPT.md b/MAIN_SCREEN_CONCEPT.md
new file mode 100644
index 0000000..4e4d31f
--- /dev/null
+++ b/MAIN_SCREEN_CONCEPT.md
@@ -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. Дополнительные функции
\ No newline at end of file
diff --git a/backend/src/modules/conversations/conversations.service.ts b/backend/src/modules/conversations/conversations.service.ts
index f33100d..20f5b6c 100644
--- a/backend/src/modules/conversations/conversations.service.ts
+++ b/backend/src/modules/conversations/conversations.service.ts
@@ -54,21 +54,33 @@ export class ConversationsService {
}
async findOrCreatePrivate(user1Id: string, user2Id: string): Promise {
+ // Проверяем существующую приватную беседу между двумя пользователями
const existingConversation = await this.conversationsRepository
.createQueryBuilder('conversation')
- .leftJoin('conversation.participants', 'p1')
- .leftJoin('conversation.participants', 'p2')
+ .leftJoinAndSelect('conversation.participants', 'participants')
.where('conversation.isGroup = false')
- .andWhere('p1.id = :user1Id', { user1Id })
- .andWhere('p2.id = :user2Id', { user2Id })
+ .andWhere((qb) => {
+ const subQuery = qb.subQuery()
+ .select('c.id')
+ .from('conversations', 'c')
+ .leftJoin('conversation_participants', 'cp1', 'cp1."conversationId" = c.id')
+ .leftJoin('conversation_participants', 'cp2', 'cp2."conversationId" = c.id')
+ .where('c.isGroup = false')
+ .andWhere('cp1."userId" = :user1Id', { user1Id })
+ .andWhere('cp2."userId" = :user2Id', { user2Id })
+ .andWhere('(SELECT COUNT(*) FROM conversation_participants WHERE "conversationId" = c.id) = 2')
+ .getQuery();
+ return 'conversation.id IN ' + subQuery;
+ })
.getOne();
if (existingConversation) {
return existingConversation;
}
- // Создаем новую приватную беседу
- return this.create([], undefined, false);
+ // Создаем новую приватную беседу с двумя участниками
+ const participants = [{ id: user1Id }, { id: user2Id }] as any;
+ return this.create(participants, undefined, false);
}
async addParticipant(conversationId: string, userId: string, participantId: string): Promise {
diff --git a/backend/src/modules/users/users.resolver.ts b/backend/src/modules/users/users.resolver.ts
index b13ae43..cbed2af 100644
--- a/backend/src/modules/users/users.resolver.ts
+++ b/backend/src/modules/users/users.resolver.ts
@@ -36,4 +36,22 @@ export class UsersResolver {
) {
return this.usersService.update(user.id, { bio, avatar });
}
+
+ @Mutation(() => User)
+ @UseGuards(GqlAuthGuard)
+ updateProfile(
+ @CurrentUser() user: User,
+ @Args('bio') bio: string,
+ ) {
+ return this.usersService.update(user.id, { bio });
+ }
+
+ @Mutation(() => User)
+ @UseGuards(GqlAuthGuard)
+ updateOnlineStatus(
+ @CurrentUser() user: User,
+ @Args('isOnline') isOnline: boolean,
+ ) {
+ return this.usersService.updateOnlineStatus(user.id, isOnline);
+ }
}
\ No newline at end of file
diff --git a/backend/src/modules/users/users.service.ts b/backend/src/modules/users/users.service.ts
index 1a5c8ba..1df2a1f 100644
--- a/backend/src/modules/users/users.service.ts
+++ b/backend/src/modules/users/users.service.ts
@@ -55,10 +55,11 @@ export class UsersService {
return this.findOne(id);
}
- async updateOnlineStatus(id: string, isOnline: boolean): Promise {
+ async updateOnlineStatus(id: string, isOnline: boolean): Promise {
await this.usersRepository.update(id, {
isOnline,
lastSeen: isOnline ? undefined : new Date(),
});
+ return this.findOne(id);
}
}
\ No newline at end of file
diff --git a/frontend/App.tsx b/frontend/App.tsx
index 669e849..0f0c9e6 100644
--- a/frontend/App.tsx
+++ b/frontend/App.tsx
@@ -2,11 +2,12 @@ import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { Platform } from 'react-native';
import { ApolloProvider } from '@apollo/client';
-import { Provider as PaperProvider, MD3DarkTheme, configureFonts } from 'react-native-paper';
+import { Provider as PaperProvider } from 'react-native-paper';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { apolloClient } from './src/services/apollo-client';
import { AuthProvider } from './src/contexts/AuthContext';
import { AppNavigator } from './src/navigation/AppNavigator';
+<<<<<<< HEAD
// Современная черно-серая тема
const theme = {
@@ -56,6 +57,9 @@ const theme = {
},
}),
};
+=======
+import { theme } from './src/theme';
+>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
export default function App() {
return (
diff --git a/frontend/src/graphql/mutations.ts b/frontend/src/graphql/mutations.ts
index fd90258..2a78ffc 100644
--- a/frontend/src/graphql/mutations.ts
+++ b/frontend/src/graphql/mutations.ts
@@ -87,4 +87,23 @@ export const MARK_MESSAGE_AS_READ = gql`
}
}
${MESSAGE_FRAGMENT}
+`;
+
+// Profile mutations
+export const UPDATE_PROFILE = gql`
+ mutation UpdateProfile($bio: String!) {
+ updateProfile(bio: $bio) {
+ ...UserFragment
+ }
+ }
+ ${USER_FRAGMENT}
+`;
+
+export const UPDATE_ONLINE_STATUS = gql`
+ mutation UpdateOnlineStatus($isOnline: Boolean!) {
+ updateOnlineStatus(isOnline: $isOnline) {
+ ...UserFragment
+ }
+ }
+ ${USER_FRAGMENT}
`;
\ No newline at end of file
diff --git a/frontend/src/navigation/AppNavigator.tsx b/frontend/src/navigation/AppNavigator.tsx
index fd58792..7ad6743 100644
--- a/frontend/src/navigation/AppNavigator.tsx
+++ b/frontend/src/navigation/AppNavigator.tsx
@@ -4,8 +4,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuth } from '../contexts/AuthContext';
import { LoginScreen } from '../screens/LoginScreen';
import { RegisterScreen } from '../screens/RegisterScreen';
-import { ConversationsScreen } from '../screens/ConversationsScreen';
-import { ChatScreen } from '../screens/ChatScreen';
+import { MainNavigator } from './MainNavigator';
import { ActivityIndicator, View } from 'react-native';
const Stack = createNativeStackNavigator();
@@ -25,23 +24,11 @@ export const AppNavigator = () => {
{user ? (
- <>
-
- ({
- title: route.params?.title || 'Чат',
- })}
- />
- >
+
) : (
<>
+
+ ({
+ title: route.params?.title || 'Чат',
+ })}
+ />
+
+ );
+}
+
+// Стек навигации для контактов
+function ContactsStackNavigator() {
+ return (
+
+
+
+ );
+}
+
+// Стек навигации для профиля
+function ProfileStackNavigator() {
+ return (
+
+
+
+ );
+}
+
+export function MainNavigator() {
+ const theme = useTheme();
+
+ return (
+ ({
+ 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 ;
+ },
+ tabBarActiveTintColor: theme.colors.primary,
+ tabBarInactiveTintColor: 'gray',
+ headerShown: false,
+ tabBarStyle: {
+ backgroundColor: theme.colors.surface,
+ borderTopColor: theme.colors.outlineVariant,
+ borderTopWidth: 1,
+ },
+ })}
+ >
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/screens/ContactsScreen.tsx b/frontend/src/screens/ContactsScreen.tsx
new file mode 100644
index 0000000..bc17400
--- /dev/null
+++ b/frontend/src/screens/ContactsScreen.tsx
@@ -0,0 +1,427 @@
+import React, { useState, useEffect } from 'react';
+import { View, StyleSheet, FlatList, ScrollView, Animated, TouchableOpacity } from 'react-native';
+import { Searchbar, List, Avatar, Text, Chip, FAB, Divider, IconButton, SegmentedButtons, Button, ActivityIndicator, Badge } 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';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+
+export const ContactsScreen = ({ navigation }: any) => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedTab, setSelectedTab] = useState('all');
+ const [creatingChatWithId, setCreatingChatWithId] = useState(null);
+ const { user: currentUser } = useAuth();
+ const fadeAnim = useState(new Animated.Value(0))[0];
+
+ 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),
+ },
+ });
+ setCreatingChatWithId(null);
+ },
+ onError: () => {
+ setCreatingChatWithId(null);
+ }
+ });
+
+ useEffect(() => {
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 500,
+ useNativeDriver: true,
+ }).start();
+ }, []);
+
+ const getOtherParticipantName = (conversation: any) => {
+ const otherParticipant = conversation.participants?.find((p: User) => p.id !== currentUser?.id);
+ return otherParticipant?.username || 'Чат';
+ };
+
+ const handleStartChat = async (userId: string) => {
+ setCreatingChatWithId(userId);
+ 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, index }: { item: User; index: number }) => {
+ const isCreatingChat = creatingChatWithId === item.id;
+
+ return (
+
+ handleStartChat(item.id)}
+ disabled={isCreatingChat}
+ >
+
+
+
+
+ {item.isOnline && (
+
+
+
+ )}
+
+
+
+
+
+ @{item.username}
+
+ {item.isOnline && (
+
+ В сети
+
+ )}
+
+
+
+ {item.bio || `${item.email}`}
+
+
+ {!item.isOnline && item.lastSeen && (
+
+ Был(а) {formatLastSeen(item.lastSeen)}
+
+ )}
+
+
+
+ {isCreatingChat ? (
+
+ ) : (
+ handleStartChat(item.id)}
+ />
+ )}
+
+
+
+
+
+ );
+ };
+
+ // Вспомогательные функции
+ const getAvatarColor = (username: string) => {
+ const colors = ['#2196F3', '#4CAF50', '#FF5722', '#9C27B0', '#FF9800', '#00BCD4'];
+ const index = username.charCodeAt(0) % colors.length;
+ return colors[index];
+ };
+
+ const formatLastSeen = (date: Date | string) => {
+ const lastSeen = new Date(date);
+ const now = new Date();
+ const diffMs = now.getTime() - lastSeen.getTime();
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+ const diffDays = Math.floor(diffMs / 86400000);
+
+ if (diffMins < 1) return 'только что';
+ if (diffMins < 60) return `${diffMins} мин. назад`;
+ if (diffHours < 24) return `${diffHours} ч. назад`;
+ if (diffDays < 7) return `${diffDays} дн. назад`;
+ return lastSeen.toLocaleDateString('ru-RU');
+ };
+
+ // Подсчет статистики
+ const onlineUsersCount = data?.users?.filter((u: User) => u.isOnline && u.id !== currentUser?.id).length || 0;
+ const totalUsersCount = data?.users?.filter((u: User) => u.id !== currentUser?.id).length || 0;
+
+ return (
+
+ {/* Заголовок с поиском */}
+
+
+
+
+ Введите никнейм для поиска пользователя
+
+
+
+ {/* Статистика */}
+
+
+ {totalUsersCount}
+ пользователей
+
+
+
+
+
+ {onlineUsersCount}
+
+ в сети
+
+
+
+
+
+
+ {
+ if (selectedTab === 'online') return user.isOnline;
+ // В будущем добавим логику для "Мои контакты"
+ return true;
+ })}
+ renderItem={renderUser}
+ keyExtractor={(item) => item.id}
+ onRefresh={refetch}
+ refreshing={loading}
+ contentContainerStyle={styles.listContent}
+ ListEmptyComponent={
+
+
+
+ {searchQuery
+ ? `Пользователь @${searchQuery} не найден`
+ : selectedTab === 'online'
+ ? 'Нет пользователей онлайн'
+ : 'Нет доступных пользователей'}
+
+ {searchQuery && (
+
+ Проверьте правильность никнейма
+
+ )}
+
+ }
+ />
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#f5f5f5',
+ },
+ header: {
+ backgroundColor: '#ffffff',
+ paddingBottom: 16,
+ elevation: 2,
+ },
+ searchContainer: {
+ padding: 16,
+ paddingBottom: 8,
+ },
+ searchbar: {
+ backgroundColor: '#f8f9fa',
+ borderRadius: 12,
+ },
+ searchHint: {
+ color: '#666',
+ marginTop: 8,
+ textAlign: 'center',
+ },
+ statsContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ paddingHorizontal: 16,
+ paddingTop: 8,
+ },
+ statItem: {
+ alignItems: 'center',
+ paddingHorizontal: 24,
+ },
+ statDivider: {
+ width: 1,
+ height: 40,
+ backgroundColor: '#e0e0e0',
+ },
+ statNumber: {
+ fontWeight: 'bold',
+ },
+ statLabel: {
+ color: '#666',
+ marginTop: 2,
+ },
+ onlineStatContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ tabs: {
+ marginHorizontal: 16,
+ marginVertical: 16,
+ },
+ listContent: {
+ paddingHorizontal: 16,
+ paddingBottom: 16,
+ },
+ userCard: {
+ backgroundColor: '#ffffff',
+ borderRadius: 16,
+ marginBottom: 12,
+ },
+ userCardShadow: {
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 1,
+ },
+ shadowOpacity: 0.1,
+ shadowRadius: 2,
+ elevation: 2,
+ },
+ headerShadow: {
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 2,
+ },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 4,
+ },
+ userContent: {
+ flexDirection: 'row',
+ padding: 16,
+ alignItems: 'center',
+ },
+ avatarContainer: {
+ position: 'relative',
+ marginRight: 16,
+ },
+ avatar: {
+ elevation: 2,
+ },
+ onlineIndicator: {
+ position: 'absolute',
+ bottom: 2,
+ right: 2,
+ backgroundColor: '#ffffff',
+ borderRadius: 7,
+ padding: 1,
+ },
+ userInfo: {
+ flex: 1,
+ marginRight: 8,
+ },
+ userHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: 4,
+ },
+ username: {
+ fontWeight: '600',
+ color: '#1a1a1a',
+ },
+ onlineChip: {
+ marginLeft: 8,
+ backgroundColor: '#e8f5e9',
+ height: 22,
+ },
+ onlineChipText: {
+ fontSize: 11,
+ color: '#4CAF50',
+ },
+ userBio: {
+ color: '#666',
+ lineHeight: 20,
+ },
+ lastSeen: {
+ color: '#999',
+ marginTop: 4,
+ fontSize: 12,
+ },
+ actionContainer: {
+ justifyContent: 'center',
+ minWidth: 48,
+ alignItems: 'center',
+ },
+ messageButton: {
+ margin: 0,
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingTop: 100,
+ },
+ emptyText: {
+ color: '#666',
+ fontSize: 16,
+ textAlign: 'center',
+ },
+});
\ No newline at end of file
diff --git a/frontend/src/screens/ConversationsScreen.tsx b/frontend/src/screens/ConversationsScreen.tsx
index bda8949..5283411 100644
--- a/frontend/src/screens/ConversationsScreen.tsx
+++ b/frontend/src/screens/ConversationsScreen.tsx
@@ -1,11 +1,18 @@
+<<<<<<< HEAD
import React from 'react';
import { View, StyleSheet, FlatList, TouchableOpacity, Dimensions } from 'react-native';
import { List, Avatar, Text, FAB, Badge, Surface } from 'react-native-paper';
+=======
+import React, { useState } from 'react';
+import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
+import { List, Avatar, Text, FAB, Divider, Badge, Searchbar, IconButton, useTheme } from 'react-native-paper';
+>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
import { useQuery } from '@apollo/client';
import { GET_CONVERSATIONS } from '../graphql/queries';
import { Conversation } from '../types';
import { format, isToday, isYesterday } from 'date-fns';
import { ru } from 'date-fns/locale';
+<<<<<<< HEAD
import { LinearGradient } from 'expo-linear-gradient';
import Animated, {
FadeInDown,
@@ -14,12 +21,20 @@ import Animated, {
} from 'react-native-reanimated';
const { width } = Dimensions.get('window');
+=======
+import { useAuth } from '../contexts/AuthContext';
+>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
export const ConversationsScreen = ({ navigation }: any) => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const { user } = useAuth();
+ const theme = useTheme();
+
const { data, loading, error, refetch } = useQuery(GET_CONVERSATIONS, {
pollInterval: 5000,
});
+<<<<<<< HEAD
const formatMessageTime = (date: string) => {
const messageDate = new Date(date);
if (isToday(messageDate)) {
@@ -33,10 +48,28 @@ export const ConversationsScreen = ({ navigation }: any) => {
const renderConversation = ({ item, index }: { item: Conversation; index: number }) => {
const otherParticipant = item.participants.find(p => p.id !== data?.me?.id);
+=======
+ // Фильтрация чатов по поисковому запросу
+ 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 otherParticipant = item.participants.find(p => p.id !== user?.id);
+>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
const displayName = item.isGroup ? item.name : otherParticipant?.username;
const lastMessageTime = item.lastMessage
? formatMessageTime(item.lastMessage.createdAt)
: '';
+
+ // Подсчет непрочитанных сообщений (в будущем добавить в GraphQL)
+ const unreadCount = 0;
return (
{
)}
+<<<<<<< HEAD
@@ -93,6 +127,17 @@ export const ConversationsScreen = ({ navigation }: any) => {
{/* Здесь можно добавить счетчик непрочитанных */}
+=======
+ )}
+ right={() => (
+
+
+ {lastMessageTime}
+
+ {unreadCount > 0 && (
+ {unreadCount}
+ )}
+>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
@@ -128,13 +173,28 @@ export const ConversationsScreen = ({ navigation }: any) => {
}
return (
+<<<<<<< HEAD
+=======
+
+ {/* Поисковая строка */}
+
+
+
+
+>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
item.id}
onRefresh={refetch}
@@ -163,6 +223,7 @@ export const ConversationsScreen = ({ navigation }: any) => {
}
/>
+<<<<<<< HEAD
@@ -178,6 +239,13 @@ export const ConversationsScreen = ({ navigation }: any) => {
}}
/>
+=======
+ navigation.navigate('Contacts')}
+ />
+>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
);
};
@@ -187,7 +255,21 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#0a0a0a',
},
+<<<<<<< HEAD
loadingContainer: {
+=======
+ searchContainer: {
+ padding: 16,
+ paddingBottom: 8,
+ backgroundColor: '#ffffff',
+ elevation: 2,
+ },
+ searchbar: {
+ elevation: 0,
+ backgroundColor: '#f5f5f5',
+ },
+ centerContainer: {
+>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
flex: 1,
justifyContent: 'center',
alignItems: 'center',
@@ -234,6 +316,7 @@ const styles = StyleSheet.create({
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.05)',
},
+<<<<<<< HEAD
avatarContainer: {
position: 'relative',
marginRight: 12,
@@ -251,6 +334,17 @@ const styles = StyleSheet.create({
color: '#ffffff',
fontSize: 20,
fontWeight: '600',
+=======
+ time: {
+ color: '#666',
+ fontSize: 12,
+ marginBottom: 4,
+ },
+ unreadBadge: {
+ backgroundColor: '#2196F3',
+ color: '#ffffff',
+ fontSize: 12,
+>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
},
onlineBadge: {
position: 'absolute',
diff --git a/frontend/src/screens/LoginScreen.tsx b/frontend/src/screens/LoginScreen.tsx
index cea14e4..bc81ca9 100644
--- a/frontend/src/screens/LoginScreen.tsx
+++ b/frontend/src/screens/LoginScreen.tsx
@@ -1,6 +1,11 @@
import React, { useState, useEffect } from 'react';
+<<<<<<< HEAD
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableOpacity } from 'react-native';
import { TextInput, Button, Text, Headline, HelperText, Surface } from 'react-native-paper';
+=======
+import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions } from 'react-native';
+import { TextInput, Button, Text, Headline, HelperText, useTheme } from 'react-native-paper';
+>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
import { useMutation } from '@apollo/client';
import { LOGIN } from '../graphql/mutations';
import { useAuth } from '../contexts/AuthContext';
@@ -28,6 +33,7 @@ export const LoginScreen = ({ navigation }: any) => {
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const { login } = useAuth();
+ const theme = useTheme();
// Анимации
const cardScale = useSharedValue(0.95);
@@ -100,6 +106,7 @@ export const LoginScreen = ({ navigation }: any) => {
};
return (
+<<<<<<< HEAD
{
+
+
+
+ Вход в Prism
+ Добро пожаловать обратно
+
+
+
+ {
+ inputFocusAnimation1.value = withSpring(1);
+ }}
+ onBlur={() => {
+ inputFocusAnimation1.value = withSpring(0);
+ }}
+ />
+
+
+
+ setShowPassword(!showPassword)}
+ color="#a855f7"
+ />
+ }
+ onFocus={() => {
+ inputFocusAnimation2.value = withSpring(1);
+ }}
+ onBlur={() => {
+ inputFocusAnimation2.value = withSpring(0);
+ }}
+ />
+
+
+ {error && (
+
+ {error.message}
+
+ )}
+
+
+
+
+
+