Tg with Notifications, bags fixxed
This commit is contained in:
134
GAME_IMPROVEMENTS_README.md
Normal file
134
GAME_IMPROVEMENTS_README.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Улучшения игры - Полный список
|
||||
|
||||
## 🆕 Новые функции
|
||||
|
||||
### 1. Уведомления о сообщениях в Telegram
|
||||
- **Описание**: Показываются уведомления о новых сообщениях, когда Telegram не открыт
|
||||
- **Как работает**:
|
||||
- WebSocket событие `newMessage` обрабатывается на клиенте
|
||||
- Уведомление появляется в правом верхнем углу экрана
|
||||
- Автоматически исчезает через 5 секунд
|
||||
- Анимированное появление и исчезновение
|
||||
- **Файлы**: `src/Game.js` - добавлена функция `showMessageNotification()`
|
||||
|
||||
### 2. Уведомления о перезагрузке сервера
|
||||
- **Описание**: Игроки получают предупреждение о перезагрузке сервера
|
||||
- **Как работает**:
|
||||
- Сервер отправляет событие `serverRestart` всем клиентам
|
||||
- Показывается красное уведомление с обратным отсчетом
|
||||
- Сервер корректно завершает работу через 5 секунд
|
||||
- **Файлы**:
|
||||
- `server.js` - добавлена функция `gracefulShutdown()`
|
||||
- `src/Game.js` - добавлена функция `showServerRestartNotification()`
|
||||
|
||||
## 🎮 Улучшения управления
|
||||
|
||||
### 3. Исправление застревания в текстурах
|
||||
- **Проблема**: Игрок застревал в текстурах и не мог выбраться
|
||||
- **Решение**:
|
||||
- Уменьшены размеры коллизионной коробки игрока (с 0.25 до 0.2)
|
||||
- Добавлено скольжение вдоль стен при столкновении
|
||||
- Увеличен зазор между игроком и объектами (с 0.01 до 0.05)
|
||||
- Улучшена система определения направления скольжения
|
||||
- **Файлы**: `src/Game.js` - улучшена функция `updateFirstPersonMovement()`
|
||||
|
||||
### 4. Улучшенное управление камерой
|
||||
- **Новые возможности**:
|
||||
- `Ctrl + колесо` = вертикальный поворот камеры (как было)
|
||||
- `Shift + Ctrl + колесо` = горизонтальный поворот камеры (±90 градусов)
|
||||
- **Ограничения**: Горизонтальный поворот ограничен для предотвращения дезориентации
|
||||
- **Файлы**: `src/Game.js` - улучшена функция `onMouseWheel()`
|
||||
|
||||
### 5. Отключение браузерного масштабирования
|
||||
- **Проблема**: Браузер масштабировал страницу при Ctrl + колесо
|
||||
- **Решение**: Добавлены обработчики событий для предотвращения масштабирования
|
||||
- **Файлы**: `src/Game.js` - добавлены обработчики `wheel` и `keydown`
|
||||
|
||||
## 🚀 Оптимизация производительности
|
||||
|
||||
### 6. Улучшенная система загрузки ресурсов
|
||||
- **Проблема**: Панель загрузки блокировала управление игрой
|
||||
- **Решение**:
|
||||
- Панель показывается только при начальной загрузке или загрузке >5 ресурсов
|
||||
- Малые загрузки происходят в фоне без блокировки
|
||||
- Добавлен флаг `isInitialLoad` для контроля отображения
|
||||
- **Файлы**: `src/Game.js` - улучшен `LoadingManager`
|
||||
|
||||
## 🔧 Технические улучшения
|
||||
|
||||
### 7. Graceful Shutdown сервера
|
||||
- **Описание**: Сервер корректно завершает работу при получении сигналов
|
||||
- **Сигналы**: `SIGTERM`, `SIGINT` (Ctrl+C)
|
||||
- **Процесс**:
|
||||
1. Уведомление всех клиентов
|
||||
2. Ожидание 5 секунд
|
||||
3. Закрытие HTTP сервера
|
||||
4. Принудительное завершение через 10 секунд
|
||||
|
||||
### 8. Улучшенная система коллизий
|
||||
- **Алгоритм скольжения**:
|
||||
- Определение ближайшего препятствия
|
||||
- Вычисление направления скольжения
|
||||
- Проверка возможности движения в направлении скольжения
|
||||
- Применение скольжения с уменьшенной дистанцией
|
||||
|
||||
## 📱 Пользовательский интерфейс
|
||||
|
||||
### 9. Подсказки по управлению
|
||||
- **Описание**: Автоматически показывается подсказка об управлении камерой
|
||||
- **Время**: Появляется через 3 секунды после загрузки игры
|
||||
- **Длительность**: Висит 10 секунд, затем плавно исчезает
|
||||
- **Стиль**: Темная полупрозрачная панель с анимацией
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
### 10. Тестовые файлы
|
||||
- `test_telegram_status.html` - тест API статуса пользователей
|
||||
- `TELEGRAM_STATUS_README.md` - документация по системе статусов
|
||||
- `TESTING_INSTRUCTIONS.md` - инструкции по тестированию
|
||||
- `GAME_IMPROVEMENTS_README.md` - этот файл
|
||||
|
||||
## 🚀 Как запустить
|
||||
|
||||
1. **Запуск сервера**:
|
||||
```bash
|
||||
node server.js
|
||||
```
|
||||
|
||||
2. **Тестирование уведомлений**:
|
||||
- Откройте игру в браузере
|
||||
- Войдите в систему
|
||||
- Откройте Telegram и отправьте сообщение
|
||||
- Проверьте уведомления
|
||||
|
||||
3. **Тестирование перезагрузки**:
|
||||
- Нажмите Ctrl+C в терминале сервера
|
||||
- Проверьте уведомление о перезагрузке
|
||||
|
||||
4. **Тестирование управления**:
|
||||
- `Ctrl + колесо` = вертикальный поворот
|
||||
- `Shift + Ctrl + колесо` = горизонтальный поворот
|
||||
|
||||
## 🔍 Отладка
|
||||
|
||||
- **Сервер**: Логи в консоли терминала
|
||||
- **Клиент**: Логи в консоли браузера (F12)
|
||||
- **WebSocket**: Проверка соединения в Network tab
|
||||
|
||||
## 📋 Чек-лист тестирования
|
||||
|
||||
- [ ] Уведомления о сообщениях работают
|
||||
- [ ] Уведомления о перезагрузке сервера работают
|
||||
- [ ] Игрок не застревает в текстурах
|
||||
- [ ] Управление камерой работает корректно
|
||||
- [ ] Браузерное масштабирование отключено
|
||||
- [ ] Панель загрузки не блокирует игру
|
||||
- [ ] Подсказки по управлению отображаются
|
||||
|
||||
## 🎯 Следующие шаги
|
||||
|
||||
1. **Кэширование**: Добавить кэширование статуса пользователей
|
||||
2. **Группировка**: Группировать пользователей по статусу в Telegram
|
||||
3. **Уведомления**: Push-уведомления для важных событий
|
||||
4. **Статистика**: Время онлайн, активность пользователей
|
||||
5. **Оптимизация**: Дальнейшее улучшение производительности
|
||||
164
TELEGRAM_IMPROVEMENTS_README.md
Normal file
164
TELEGRAM_IMPROVEMENTS_README.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Улучшения Telegram в игре
|
||||
|
||||
## Обзор
|
||||
|
||||
Этот документ описывает улучшения, внесенные в приложение Telegram (Shipgram) в игре, включая систему уведомлений, индикаторы непрочитанных сообщений и исправление проблемы "неизвестного отправителя".
|
||||
|
||||
## Основные улучшения
|
||||
|
||||
### 1. Уведомления о новых сообщениях
|
||||
|
||||
**Проблема**: При получении новых сообщений в Telegram не было уведомлений, если приложение не было открыто.
|
||||
|
||||
**Решение**: Реализована система уведомлений, которая показывает красивые всплывающие уведомления в правом верхнем углу экрана.
|
||||
|
||||
**Функциональность**:
|
||||
- Уведомления появляются только когда Telegram не открыт
|
||||
- Отображают имя отправителя и текст сообщения
|
||||
- Автоматически исчезают через 5 секунд
|
||||
- Красивый дизайн с градиентом и анимацией
|
||||
|
||||
**Код**: `showMessageNotification(senderId, messageText)` в `src/Game.js`
|
||||
|
||||
### 2. Исправление проблемы "неизвестного отправителя"
|
||||
|
||||
**Проблема**: В уведомлениях отображалось "Неизвестный" вместо имени отправителя.
|
||||
|
||||
**Решение**: Реализована система поиска информации об отправителе:
|
||||
1. Сначала ищет в списке контактов
|
||||
2. Если не найден, загружает информацию с сервера через новый API endpoint
|
||||
|
||||
**Новые API endpoints**:
|
||||
- `GET /api/users/:userId` - получение информации о пользователе по ID
|
||||
- `GET /api/messages-read/:contactId` - получение количества непрочитанных сообщений
|
||||
|
||||
**Код**: Обновленная функция `showMessageNotification` в `src/Game.js`
|
||||
|
||||
### 3. Индикаторы непрочитанных сообщений
|
||||
|
||||
**Проблема**: Не было визуального индикатора непрочитанных сообщений в списке контактов.
|
||||
|
||||
**Решение**: Добавлены счетчики непрочитанных сообщений:
|
||||
- Красные бейджи с количеством непрочитанных сообщений
|
||||
- Имена контактов выделяются жирным шрифтом при наличии непрочитанных сообщений
|
||||
- Автоматическое обновление счетчиков
|
||||
|
||||
**Функциональность**:
|
||||
- `updateUnreadCount(senderId)` - обновляет счетчик для конкретного отправителя
|
||||
- Автоматическое обновление при получении новых сообщений
|
||||
- Периодическое обновление каждые 30 секунд
|
||||
|
||||
**Код**: Функция `updateUnreadCount` и обновленный UI в `src/Game.js`
|
||||
|
||||
## Технические детали
|
||||
|
||||
### Серверная часть (`server.js`)
|
||||
|
||||
#### Новые API endpoints
|
||||
|
||||
```javascript
|
||||
// Получение информации о пользователе по ID
|
||||
app.get('/api/users/:userId', authenticate, async (req, res) => {
|
||||
// Возвращает: id, firstName, lastName, avatarURL, isOnline, lastSeen
|
||||
});
|
||||
|
||||
// Получение количества непрочитанных сообщений
|
||||
app.get('/api/messages-read/:contactId', authenticate, async (req, res) => {
|
||||
// Возвращает: { unreadCount: number }
|
||||
});
|
||||
```
|
||||
|
||||
#### Исправления для onlineUsers и lastSeenTimes
|
||||
|
||||
- Заменены `onlineUsers.hasOwnProperty()` на `onlineUsers.has()`
|
||||
- Заменены `lastSeenTimes[userId]` на `lastSeenTimes.get(userId)`
|
||||
- Исправлен `console.log` для отображения ключей Map
|
||||
|
||||
### Клиентская часть (`src/Game.js`)
|
||||
|
||||
#### Новые функции
|
||||
|
||||
```javascript
|
||||
// Показ уведомлений о сообщениях
|
||||
const showMessageNotification = async (senderId, messageText) => {
|
||||
// Логика поиска имени отправителя и показа уведомления
|
||||
};
|
||||
|
||||
// Обновление счетчика непрочитанных сообщений
|
||||
const updateUnreadCount = async (senderId) => {
|
||||
// Загрузка и обновление счетчика
|
||||
};
|
||||
```
|
||||
|
||||
#### Обновленный UI
|
||||
|
||||
- Счетчики непрочитанных сообщений (красные бейджи)
|
||||
- Выделение имен контактов жирным шрифтом
|
||||
- Улучшенная структура контактов
|
||||
|
||||
### API функции (`src/api/auth.js`)
|
||||
|
||||
```javascript
|
||||
// Загрузка информации о пользователе по ID
|
||||
export const loadUserInfo = async (userId, token) => {
|
||||
// Запрос к /api/users/:userId
|
||||
};
|
||||
|
||||
// Обновленная функция получения статуса пользователей
|
||||
export const getUsersStatus = async (token) => {
|
||||
// Улучшенная обработка ошибок
|
||||
};
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Тестовый файл
|
||||
|
||||
Создан `test_telegram_improvements.html` для тестирования новых API endpoints:
|
||||
|
||||
1. **Тест статуса пользователей** - `/api/users/status`
|
||||
2. **Тест информации о пользователе** - `/api/users/:userId`
|
||||
3. **Тест количества непрочитанных сообщений** - `/api/messages-read/:contactId`
|
||||
|
||||
### Инструкции по тестированию
|
||||
|
||||
1. Откройте `test_telegram_improvements.html` в браузере
|
||||
2. Введите JWT токен пользователя
|
||||
3. Протестируйте каждый endpoint
|
||||
4. Проверьте корректность возвращаемых данных
|
||||
|
||||
## Интеграция с существующим кодом
|
||||
|
||||
### WebSocket события
|
||||
|
||||
- `newMessage` - автоматически вызывает `updateUnreadCount` и `showMessageNotification`
|
||||
- `userStatusChanged` - обновляет статус пользователей в реальном времени
|
||||
|
||||
### Интервалы обновления
|
||||
|
||||
- `statusInterval` - каждые 30 секунд обновляет статусы и счетчики непрочитанных сообщений
|
||||
|
||||
### Состояние компонента
|
||||
|
||||
- `telegramContacts` - расширен для хранения `unreadCount`
|
||||
- Автоматическое обновление при изменении данных
|
||||
|
||||
## Преимущества
|
||||
|
||||
1. **Лучший UX**: Пользователи видят уведомления о новых сообщениях
|
||||
2. **Информативность**: Четко видно, от кого пришло сообщение
|
||||
3. **Отслеживание**: Легко понять, какие чаты требуют внимания
|
||||
4. **Производительность**: Эффективное обновление данных через интервалы
|
||||
5. **Надежность**: Обработка ошибок и fallback для неизвестных отправителей
|
||||
|
||||
## Возможные улучшения в будущем
|
||||
|
||||
1. **Звуковые уведомления** - добавление звуковых сигналов
|
||||
2. **Настройки уведомлений** - возможность отключения для определенных контактов
|
||||
3. **Push-уведомления** - интеграция с браузерными push-уведомлениями
|
||||
4. **Группировка уведомлений** - объединение уведомлений от одного отправителя
|
||||
5. **История уведомлений** - сохранение истории показанных уведомлений
|
||||
|
||||
## Заключение
|
||||
|
||||
Реализованные улучшения значительно повышают удобство использования Telegram в игре, решая основные проблемы с уведомлениями и отображением информации об отправителях. Система стала более информативной и удобной для пользователей.
|
||||
78
TELEGRAM_STATUS_README.md
Normal file
78
TELEGRAM_STATUS_README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Система статуса пользователей для Telegram (Shipgram)
|
||||
|
||||
## Описание
|
||||
|
||||
Реализована система отображения статуса "online/offline" для пользователей в Telegram приложении (Shipgram) игрового телефона. Статус показывает, действительно ли игрок находится онлайн в игре.
|
||||
|
||||
## Что реализовано
|
||||
|
||||
### 1. Серверная часть (server.js)
|
||||
|
||||
- **Отслеживание онлайн пользователей**: Переменная `onlineUsers` хранит ID пользователей, которые в данный момент подключены к WebSocket
|
||||
- **Отслеживание времени последнего онлайн**: Переменная `lastSeenTimes` хранит время последнего подключения/отключения каждого пользователя
|
||||
- **WebSocket события**:
|
||||
- При подключении: `userStatusChanged` с `isOnline: true`
|
||||
- При отключении: `userStatusChanged` с `isOnline: false`
|
||||
- **API endpoint**: `/api/users/status` возвращает список пользователей с их статусом
|
||||
|
||||
### 2. Клиентская часть (Game.js)
|
||||
|
||||
- **Обновленный API вызов**: `loadTelegramContacts()` теперь использует `/api/users/status` вместо `/api/users`
|
||||
- **WebSocket обработчик**: Слушает события `userStatusChanged` и обновляет статус в реальном времени
|
||||
- **Периодическое обновление**: Каждые 30 секунд обновляет статус пользователей
|
||||
- **Визуальные индикаторы**:
|
||||
- Зеленая точка для онлайн пользователей
|
||||
- Цветной текст статуса (зеленый для онлайн, серый для офлайн)
|
||||
- Время последнего онлайн для офлайн пользователей
|
||||
|
||||
### 3. API функции (auth.js)
|
||||
|
||||
- Добавлена функция `getUsersStatus()` для получения статуса пользователей
|
||||
|
||||
## Как это работает
|
||||
|
||||
1. **Подключение пользователя**:
|
||||
- Пользователь входит в игру
|
||||
- WebSocket middleware добавляет его в `onlineUsers`
|
||||
- Обновляется `lastSeenTimes`
|
||||
- Отправляется событие `userStatusChanged` всем клиентам
|
||||
|
||||
2. **Отключение пользователя**:
|
||||
- Пользователь выходит из игры или теряет соединение
|
||||
- Обновляется `lastSeenTimes`
|
||||
- Отправляется событие `userStatusChanged` всем клиентам
|
||||
- Пользователь удаляется из `onlineUsers`
|
||||
|
||||
3. **Отображение в Telegram**:
|
||||
- При открытии Telegram загружается список пользователей с их статусом
|
||||
- Статус обновляется в реальном времени через WebSocket
|
||||
- Периодически обновляется через API для синхронизации
|
||||
|
||||
## Файлы, которые были изменены
|
||||
|
||||
- `server.js` - добавлена логика отслеживания статуса и WebSocket события
|
||||
- `src/Game.js` - обновлено Telegram приложение для отображения статуса
|
||||
- `src/api/auth.js` - добавлена функция для получения статуса пользователей
|
||||
- `test_telegram_status.html` - тестовый файл для проверки API
|
||||
|
||||
## Тестирование
|
||||
|
||||
1. Запустите сервер
|
||||
2. Откройте `test_telegram_status.html` в браузере
|
||||
3. Введите JWT токен пользователя
|
||||
4. Нажмите "Тестировать API"
|
||||
5. Проверьте, что статус пользователей отображается корректно
|
||||
|
||||
## Особенности
|
||||
|
||||
- **Реальное время**: Статус обновляется мгновенно при подключении/отключении
|
||||
- **Надежность**: Периодическое обновление через API обеспечивает синхронизацию
|
||||
- **Производительность**: WebSocket события отправляются только при изменении статуса
|
||||
- **Визуальная обратная связь**: Зеленые точки и цветной текст для лучшего UX
|
||||
|
||||
## Возможные улучшения
|
||||
|
||||
1. **Кэширование**: Добавить кэширование статуса пользователей
|
||||
2. **Группировка**: Группировать пользователей по статусу
|
||||
3. **Уведомления**: Уведомления о том, что пользователь стал онлайн
|
||||
4. **Статистика**: Время, проведенное онлайн, активность и т.д.
|
||||
60
TESTING_INSTRUCTIONS.md
Normal file
60
TESTING_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Инструкция по тестированию системы статуса пользователей
|
||||
|
||||
## Быстрый тест
|
||||
|
||||
1. **Запустите сервер**:
|
||||
```bash
|
||||
node server.js
|
||||
```
|
||||
|
||||
2. **Откройте игру в браузере** и войдите в систему
|
||||
|
||||
3. **Откройте Telegram в игровом телефоне** и проверьте:
|
||||
- Отображается ли статус "Онлайн" для текущего пользователя
|
||||
- Есть ли зеленая точка рядом с аватаром
|
||||
- Показывает ли статус "Офлайн" для других пользователей
|
||||
|
||||
## Детальное тестирование
|
||||
|
||||
### Тест 1: Проверка API
|
||||
1. Откройте `test_telegram_status.html` в браузере
|
||||
2. Введите JWT токен из localStorage браузера
|
||||
3. Нажмите "Тестировать API"
|
||||
4. Проверьте, что возвращается список пользователей с полями:
|
||||
- `isOnline`: boolean
|
||||
- `lastSeen`: timestamp или null
|
||||
|
||||
### Тест 2: Проверка WebSocket событий
|
||||
1. Откройте консоль браузера
|
||||
2. Войдите в игру
|
||||
3. Проверьте логи:
|
||||
```
|
||||
Статус пользователя изменился: {userId: X, isOnline: true}
|
||||
```
|
||||
|
||||
### Тест 3: Проверка реального времени
|
||||
1. Откройте игру в двух вкладках браузера
|
||||
2. Войдите под разными пользователями
|
||||
3. В одной вкладке откройте Telegram
|
||||
4. В другой вкладке закройте игру
|
||||
5. Проверьте, что статус изменился на "Офлайн" в реальном времени
|
||||
|
||||
## Ожидаемые результаты
|
||||
|
||||
- ✅ Статус "Онлайн" отображается только для пользователей, которые действительно в игре
|
||||
- ✅ Зеленая точка появляется рядом с аватаром онлайн пользователей
|
||||
- ✅ Статус обновляется в реальном времени при подключении/отключении
|
||||
- ✅ Время последнего онлайн отображается для офлайн пользователей
|
||||
- ✅ В консоли сервера видны логи подключения/отключения пользователей
|
||||
|
||||
## Возможные проблемы
|
||||
|
||||
1. **Статус не обновляется**: Проверьте WebSocket соединение
|
||||
2. **API возвращает ошибку**: Проверьте JWT токен и права доступа
|
||||
3. **Статус не синхронизируется**: Проверьте логи сервера на наличие ошибок
|
||||
|
||||
## Отладка
|
||||
|
||||
- **Сервер**: Смотрите логи в консоли сервера
|
||||
- **Клиент**: Смотрите логи в консоли браузера
|
||||
- **WebSocket**: Проверьте соединение в Network tab браузера
|
||||
60
daily_availability.py
Normal file
60
daily_availability.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
# daily_availability.py
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
import httpx
|
||||
from apscheduler.schedulers.blocking import BlockingScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from config import *
|
||||
|
||||
# ====== CONFIGURATION ======
|
||||
API_BASE = "http://127.0.0.1:8000"
|
||||
TIMEOUT = httpx.Timeout(connect=5.0, read=30.0, write=5.0, pool=5.0)
|
||||
|
||||
# ====== LOGGING SETUP ======
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s | %(levelname)s | %(message)s',
|
||||
handlers=[logging.FileHandler("daily_availability.log", encoding="utf-8")]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ====== JOB FUNCTION ======
|
||||
def check_availability():
|
||||
today = date.today().isoformat()
|
||||
url = f"{API_BASE}/availability_group"
|
||||
headers = {
|
||||
"X-API-KEY": API_KEY,
|
||||
"Content-Type": "application/json" # хотя httpx сам проставит, но можно явно
|
||||
}
|
||||
payload = {"dates": [today]}
|
||||
|
||||
logger.info(f"Запрос создания доступности групп на {today}")
|
||||
try:
|
||||
with httpx.Client(timeout=TIMEOUT) as client:
|
||||
response = client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
logger.info(f"Успешно создана доступность: {data}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
allow = e.response.headers.get("Allow")
|
||||
logger.error(f"Метод не разрешён (status={e.response.status_code}), Allow={allow}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при запросе доступности: {e}")
|
||||
|
||||
|
||||
# ====== SCHEDULER SETUP ======
|
||||
if __name__ == "__main__":
|
||||
scheduler = BlockingScheduler(timezone="Asia/Yerevan")
|
||||
# Запускаем задачу каждый день в 00:01
|
||||
scheduler.add_job(
|
||||
check_availability,
|
||||
trigger=CronTrigger(hour=0, minute=1),
|
||||
name="daily_availability_check"
|
||||
)
|
||||
logger.info("Scheduler запущен. Ожидание выполнения задачи в 00:01...")
|
||||
try:
|
||||
scheduler.start()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
logger.info("Scheduler остановлен пользователем")
|
||||
@@ -1 +1 @@
|
||||
{"time":"2025-04-23T00:30:00.608Z","lastReal":1756243565634}
|
||||
{"time":"2025-06-16T00:46:41.600Z","lastReal":1756826890758}
|
||||
152
server.js
152
server.js
@@ -33,7 +33,7 @@ catch (e) {
|
||||
console.error('Ошибка при импорте db1 - virtual_World:', e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
/*
|
||||
try {
|
||||
new_quest_Base = require('./db2');
|
||||
console.log('db2 - new_quest_Base - успешно импортирован');
|
||||
@@ -42,7 +42,7 @@ catch (e) {
|
||||
console.error('Ошибка при импорте db2 - new_quest_Base: ', e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
*/
|
||||
try {
|
||||
db = require('./db');
|
||||
console.log('db успешно импортирован');
|
||||
@@ -99,7 +99,8 @@ const io = require('socket.io')(http, {
|
||||
}
|
||||
});
|
||||
|
||||
let onlineUsers = {};
|
||||
let onlineUsers = new Map();
|
||||
let lastSeenTimes = new Map(); // Добавляем отслеживание времени последнего онлайн
|
||||
|
||||
const organizationsRouter = require('./server/organizations')(io, onlineUsers);
|
||||
app.use('/api/organizations', organizationsRouter);
|
||||
@@ -120,7 +121,19 @@ io.use((socket, next) => {
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||
socket.userId = payload.id;
|
||||
onlineUsers[socket.userId] = socket.id; // Добавить пользователя в онлайн
|
||||
onlineUsers.set(socket.userId, socket.id); // Добавить пользователя в онлайн
|
||||
|
||||
// Обновляем время последнего онлайн
|
||||
lastSeenTimes.set(socket.userId, new Date());
|
||||
console.log(`Пользователь ${socket.userId} стал онлайн, время: ${lastSeenTimes.get(socket.userId)}`);
|
||||
|
||||
// Уведомляем всех клиентов о том, что пользователь стал онлайн
|
||||
socket.broadcast.emit('userStatusChanged', {
|
||||
userId: socket.userId,
|
||||
isOnline: true
|
||||
});
|
||||
console.log(`Отправлено событие userStatusChanged для пользователя ${socket.userId} (онлайн)`);
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
next(new Error('Invalid token'));
|
||||
@@ -315,7 +328,7 @@ io.on('connection', socket => {
|
||||
);
|
||||
|
||||
const newMessage = result.rows[0];
|
||||
const receiverSocketId = onlineUsers[recvId];
|
||||
const receiverSocketId = onlineUsers.get(recvId);
|
||||
|
||||
// Отправка получателю
|
||||
if (receiverSocketId) {
|
||||
@@ -404,7 +417,18 @@ io.on('connection', socket => {
|
||||
|
||||
// --- Отключение ---
|
||||
socket.on('disconnect', async () => {
|
||||
delete onlineUsers[socket.userId];
|
||||
// Обновляем время последнего онлайн
|
||||
lastSeenTimes.set(socket.userId, new Date());
|
||||
console.log(`Пользователь ${socket.userId} стал офлайн, время: ${lastSeenTimes.get(socket.userId)}`);
|
||||
|
||||
// Уведомляем всех клиентов о том, что пользователь стал офлайн
|
||||
socket.broadcast.emit('userStatusChanged', {
|
||||
userId: socket.userId,
|
||||
isOnline: false
|
||||
});
|
||||
console.log(`Отправлено событие userStatusChanged для пользователя ${socket.userId} (офлайн)`);
|
||||
|
||||
onlineUsers.delete(socket.userId);
|
||||
const cityId = socket.cityId;
|
||||
const player = playersByCity[cityId]?.[socket.id];
|
||||
if (player) {
|
||||
@@ -439,6 +463,82 @@ app.get('/api/users', authenticate, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// API endpoint для получения статуса пользователей для Telegram
|
||||
app.get('/api/users/status', authenticate, async (req, res) => {
|
||||
try {
|
||||
console.log(`Запрос статуса пользователей от пользователя ${req.user.id}`);
|
||||
|
||||
const { rows } = await db.query(`
|
||||
SELECT id, first_name AS "firstName", last_name AS "lastName", avatar_url AS "avatarURL"
|
||||
FROM users
|
||||
WHERE id != $1
|
||||
`, [req.user.id]);
|
||||
|
||||
// Добавляем статус online и время последнего онлайн для каждого пользователя
|
||||
const usersWithStatus = rows.map(user => ({
|
||||
...user,
|
||||
isOnline: onlineUsers.has(user.id),
|
||||
lastSeen: lastSeenTimes.get(user.id) || null
|
||||
}));
|
||||
|
||||
console.log(`Возвращено ${usersWithStatus.length} пользователей с статусом`);
|
||||
console.log('Онлайн пользователи:', Array.from(onlineUsers.keys()));
|
||||
|
||||
res.json(usersWithStatus);
|
||||
} catch (e) {
|
||||
console.error('Ошибка получения статуса пользователей', e);
|
||||
res.status(500).json({ error: 'Ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// API endpoint to get user information by ID
|
||||
app.get('/api/users/:userId', authenticate, async (req, res) => {
|
||||
const userId = parseInt(req.params.userId, 10);
|
||||
try {
|
||||
const { rows } = await db.query(`
|
||||
SELECT id, first_name AS "firstName", last_name AS "lastName", avatar_url AS "avatarURL"
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`, [userId]);
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
const user = rows[0];
|
||||
const isOnline = onlineUsers.has(user.id);
|
||||
const lastSeen = lastSeenTimes.get(user.id) || new Date();
|
||||
res.json({
|
||||
id: user.id,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
avatarURL: user.avatarURL,
|
||||
isOnline: isOnline,
|
||||
lastSeen: lastSeen
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Ошибка получения информации о пользователе по ID', e);
|
||||
res.status(500).json({ error: 'Ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// API endpoint to get unread message count for a specific contact
|
||||
app.get('/api/messages-read/:contactId', authenticate, async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const contactId = parseInt(req.params.contactId, 10);
|
||||
|
||||
try {
|
||||
const { rows } = await db.query(`
|
||||
SELECT COUNT(*) as unread_count
|
||||
FROM messages
|
||||
WHERE sender_id = $1 AND recipient_id = $2 AND is_read = false
|
||||
`, [contactId, userId]);
|
||||
|
||||
res.json({ unreadCount: parseInt(rows[0].unread_count) });
|
||||
} catch (e) {
|
||||
console.error('Ошибка получения количества непрочитанных сообщений:', e);
|
||||
res.status(500).json({ error: 'Ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// Новый маршрут для получения сообщений с конкретным контактом
|
||||
app.get('/api/messages/:contactId', authenticate, async (req, res) => {
|
||||
const userId = req.user.id;
|
||||
@@ -595,7 +695,7 @@ app.post('/api/messages/send', authenticate, async (req, res) => {
|
||||
);
|
||||
const newMessage = result.rows[0];
|
||||
|
||||
const receiverSocketId = onlineUsers[recvId];
|
||||
const receiverSocketId = onlineUsers.get(recvId);
|
||||
if (receiverSocketId) {
|
||||
io.to(receiverSocketId).emit('newMessage', {
|
||||
id: newMessage.id,
|
||||
@@ -1531,10 +1631,46 @@ app.use((req, res) => {
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 4000;
|
||||
http.listen(PORT, () => {
|
||||
const server = http.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
});
|
||||
|
||||
// Обработка сигналов для graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM received, shutting down gracefully...');
|
||||
gracefulShutdown();
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('SIGINT received, shutting down gracefully...');
|
||||
gracefulShutdown();
|
||||
});
|
||||
|
||||
function gracefulShutdown() {
|
||||
console.log('Уведомляем всех клиентов о перезагрузке сервера...');
|
||||
|
||||
// Отправляем уведомление всем подключенным клиентам
|
||||
io.emit('serverRestart', {
|
||||
message: 'Сервер будет перезагружен через 5 секунд. Пожалуйста, сохраните прогресс.',
|
||||
restartIn: 5000
|
||||
});
|
||||
|
||||
// Даем время клиентам получить уведомление
|
||||
setTimeout(() => {
|
||||
console.log('Закрываем сервер...');
|
||||
server.close(() => {
|
||||
console.log('HTTP server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Принудительно закрываем через 10 секунд
|
||||
setTimeout(() => {
|
||||
console.error('Could not close connections in time, forcefully shutting down');
|
||||
process.exit(1);
|
||||
}, 10000);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Логирование всех маршрутов и middleware
|
||||
['get', 'post', 'put', 'delete', 'use'].forEach(method => {
|
||||
const orig = app[method];
|
||||
|
||||
@@ -169,7 +169,7 @@ module.exports = function(io, onlineUsers) {
|
||||
let thirst = parseFloat(rows[0].thirst ?? 100);
|
||||
|
||||
if (balance < price) {
|
||||
const sock = onlineUsers[req.user.id];
|
||||
const sock = onlineUsers.get(req.user.id);
|
||||
if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вам недостаточно средств для покупки ${itemDef.name}` });
|
||||
return res.status(400).json({ error: 'insufficient funds' });
|
||||
}
|
||||
@@ -196,7 +196,7 @@ module.exports = function(io, onlineUsers) {
|
||||
satiety = Math.min(100, satiety + parseFloat(itemDef.hunger_gain));
|
||||
thirst = Math.min(100, thirst + parseFloat(itemDef.thirst_gain));
|
||||
|
||||
const sock = onlineUsers[req.user.id];
|
||||
const sock = onlineUsers.get(req.user.id);
|
||||
if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вы купили ${itemDef.name}` });
|
||||
|
||||
res.json({ success: true, balance, satiety, thirst });
|
||||
|
||||
543
src/Game.js
543
src/Game.js
@@ -15,6 +15,8 @@ import Inventory from './components/Inventory';
|
||||
import OrgControlPanel from './components/OrgControlPanel';
|
||||
import DoubleTapWrapper from './pages/DoubleTapWrapper';
|
||||
import WaveformPlayer from './pages/WaveformPlayer';
|
||||
import { getUsersStatus, loadUserInfo } from './api/auth.js';
|
||||
|
||||
function Game({ avatarUrl, gender }) {
|
||||
|
||||
// 1) реф для хранилища сцены
|
||||
@@ -31,6 +33,7 @@ function Game({ avatarUrl, gender }) {
|
||||
const cleanupTimerRef = useRef(null);
|
||||
// Глобальный менеджер прогресса загрузки (используем в GLTFLoader)
|
||||
const loadingManagerRef = useRef(null);
|
||||
const overlayTimeoutRef = useRef(null);
|
||||
// Кликабельные объекты внутри интерьера
|
||||
const interiorInteractablesRef = useRef([]);
|
||||
const npcMeshesRef = useRef([]);
|
||||
@@ -1341,14 +1344,19 @@ function Game({ avatarUrl, gender }) {
|
||||
const token = localStorage.getItem('token');
|
||||
try {
|
||||
setTgError(null);
|
||||
const res = await fetch('/api/users', {
|
||||
const res = await fetch('/api/users/status', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
credentials: 'include',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setTelegramContacts(data);
|
||||
// Добавляем счетчик непрочитанных сообщений для каждого пользователя
|
||||
const dataWithUnread = data.map(user => ({
|
||||
...user,
|
||||
unreadCount: 0
|
||||
}));
|
||||
setTelegramContacts(dataWithUnread);
|
||||
} else {
|
||||
const txt = await res.text().catch(() => '');
|
||||
console.error('Ошибка загрузки контактов Telegram', res.status, txt);
|
||||
@@ -1367,6 +1375,223 @@ function Game({ avatarUrl, gender }) {
|
||||
//const [readmes, setReadmes] = useState('false');
|
||||
const [userProfile, setUserProfile] = useState(null);
|
||||
|
||||
// Функция показа уведомлений о сообщениях
|
||||
const showMessageNotification = async (senderId, messageText) => {
|
||||
try {
|
||||
// Сначала пытаемся найти отправителя в контактах
|
||||
let senderName = 'Неизвестный';
|
||||
const contact = telegramContacts.find(c => c.id === senderId);
|
||||
|
||||
if (contact) {
|
||||
senderName = contact.firstName || contact.lastName || 'Неизвестный';
|
||||
} else {
|
||||
// Если не найден в контактах, загружаем информацию о пользователе
|
||||
try {
|
||||
const userInfo = await loadUserInfo(senderId, localStorage.getItem('token'));
|
||||
senderName = userInfo.firstName || userInfo.lastName || 'Неизвестный';
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки информации о пользователе:', error);
|
||||
senderName = 'Неизвестный';
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем уведомление
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||||
z-index: 10000;
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 14px;
|
||||
max-width: 300px;
|
||||
transform: translateX(400px);
|
||||
transition: transform 0.3s ease-out;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<div style="font-weight: bold; margin-bottom: 5px; color: #ffd700;">${senderName}</div>
|
||||
<div style="opacity: 0.9;">${messageText.length > 50 ? messageText.substring(0, 50) + '...' : messageText}</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Анимация появления
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
// Автоматическое скрытие через 5 секунд
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(400px)';
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 300);
|
||||
}, 5000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка показа уведомления:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для обновления счетчика непрочитанных сообщений
|
||||
const updateUnreadCount = async (senderId) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
// Получаем количество непрочитанных сообщений
|
||||
const response = await fetch(`/api/messages-read/${senderId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const unreadCount = data.unreadCount || 0;
|
||||
|
||||
// Обновляем счетчик в контактах
|
||||
setTelegramContacts(prev =>
|
||||
prev.map(contact =>
|
||||
contact.id === senderId
|
||||
? { ...contact, unreadCount: unreadCount }
|
||||
: contact
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка обновления счетчика непрочитанных сообщений:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Функция показа подсказки об управлении камерой
|
||||
function showCameraControlsHint() {
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 10px;
|
||||
font-family: system-ui, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
z-index: 9999;
|
||||
max-width: 300px;
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
`;
|
||||
|
||||
hint.innerHTML = `
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">🎮 Управление камерой:</div>
|
||||
<div style="margin-bottom: 5px;">• <strong>Ctrl + колесо</strong> = вертикальный поворот</div>
|
||||
<div style="margin-bottom: 5px;">• <strong>Shift + Ctrl + колесо</strong> = горизонтальный поворот</div>
|
||||
<div style="font-size: 12px; opacity: 0.8;">Подсказка исчезнет через 10 секунд</div>
|
||||
`;
|
||||
|
||||
// Добавляем CSS анимацию
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.body.appendChild(hint);
|
||||
|
||||
// Автоматически скрываем через 10 секунд
|
||||
setTimeout(() => {
|
||||
hint.style.animation = 'fadeOut 0.5s ease-out';
|
||||
hint.style.opacity = '0';
|
||||
setTimeout(() => hint.remove(), 500);
|
||||
}, 10000);
|
||||
|
||||
// Добавляем CSS для fadeOut
|
||||
if (!document.querySelector('#hint-styles')) {
|
||||
const fadeOutStyle = document.createElement('style');
|
||||
fadeOutStyle.id = 'hint-styles';
|
||||
fadeOutStyle.textContent = `
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(20px); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(fadeOutStyle);
|
||||
}
|
||||
}
|
||||
|
||||
// Функция показа уведомления о перезагрузке сервера
|
||||
function showServerRestartNotification(message, restartIn) {
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
z-index: 10001;
|
||||
max-width: 400px;
|
||||
font-family: system-ui, Arial, sans-serif;
|
||||
text-align: center;
|
||||
animation: serverRestartPulse 2s infinite;
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<div style="font-size: 18px; font-weight: 600; margin-bottom: 10px;">⚠️ Перезагрузка сервера</div>
|
||||
<div style="font-size: 14px; margin-bottom: 15px;">${message}</div>
|
||||
<div style="font-size: 12px; opacity: 0.8;">Перезагрузка через: <span id="restart-countdown">${Math.ceil(restartIn/1000)}</span> сек</div>
|
||||
`;
|
||||
|
||||
// Добавляем CSS анимацию
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes serverRestartPulse {
|
||||
0%, 100% { transform: translate(-50%, -50%) scale(1); }
|
||||
50% { transform: translate(-50%, -50%) scale(1.05); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Обновляем счетчик
|
||||
const countdownEl = notification.querySelector('#restart-countdown');
|
||||
const startTime = Date.now();
|
||||
const countdownInterval = setInterval(() => {
|
||||
const remaining = Math.max(0, restartIn - (Date.now() - startTime));
|
||||
if (countdownEl) {
|
||||
countdownEl.textContent = Math.ceil(remaining/1000);
|
||||
}
|
||||
if (remaining <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
notification.remove();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Автоматически скрываем через время перезагрузки
|
||||
setTimeout(() => {
|
||||
clearInterval(countdownInterval);
|
||||
notification.remove();
|
||||
}, restartIn);
|
||||
}
|
||||
|
||||
// Функция загрузки сообщений
|
||||
async function loadMessages(contactId) {
|
||||
if (!contactId) return;
|
||||
@@ -1910,11 +2135,18 @@ function Game({ avatarUrl, gender }) {
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Красивый загрузочный оверлей + LoadingManager
|
||||
// Улучшенный загрузочный оверлей + LoadingManager
|
||||
// ─────────────────────────────────────────────
|
||||
let overlayEl = null, barEl = null, textEl = null;
|
||||
let isInitialLoad = true; // Флаг для определения начальной загрузки
|
||||
|
||||
function createLoadingOverlay() {
|
||||
if (overlayEl) return;
|
||||
// Дополнительная проверка - не показываем overlay для очень маленьких загрузок
|
||||
if (!isInitialLoad && loadingManagerRef.current && loadingManagerRef.current.itemStart) {
|
||||
const currentTotal = loadingManagerRef.current.itemStart.length || 0;
|
||||
if (currentTotal <= 3) return; // Не показываем для загрузки 3 или меньше ресурсов
|
||||
}
|
||||
overlayEl = document.createElement('div');
|
||||
Object.assign(overlayEl.style, {
|
||||
position: 'fixed', inset: '0', zIndex: 2000,
|
||||
@@ -1951,6 +2183,7 @@ function Game({ avatarUrl, gender }) {
|
||||
overlayEl.appendChild(pct);
|
||||
document.body.appendChild(overlayEl);
|
||||
}
|
||||
|
||||
function updateLoadingOverlay(percent, text) {
|
||||
if (!overlayEl) return;
|
||||
const p = Math.max(0, Math.min(100, Math.round(percent || 0)));
|
||||
@@ -1959,8 +2192,16 @@ function Game({ avatarUrl, gender }) {
|
||||
if (pct) pct.textContent = p + '%';
|
||||
if (text && textEl) textEl.textContent = text;
|
||||
}
|
||||
|
||||
function removeLoadingOverlay() {
|
||||
if (!overlayEl) return;
|
||||
|
||||
// Очищаем все таймеры overlay
|
||||
if (overlayTimeoutRef.current) {
|
||||
clearTimeout(overlayTimeoutRef.current);
|
||||
overlayTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
overlayEl.style.transition = 'opacity .2s ease';
|
||||
overlayEl.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
@@ -1968,19 +2209,62 @@ function Game({ avatarUrl, gender }) {
|
||||
overlayEl = barEl = textEl = null;
|
||||
}, 220);
|
||||
}
|
||||
|
||||
// Общий менеджер загрузки (для GLTF/Texture и т.п.)
|
||||
const loadingManager = new THREE.LoadingManager();
|
||||
loadingManagerRef.current = loadingManager;
|
||||
|
||||
loadingManager.onStart = (_url, loaded, total) => {
|
||||
createLoadingOverlay();
|
||||
updateLoadingOverlay(total ? (loaded / total) * 100 : 5, 'Загрузка ресурсов...');
|
||||
console.log(`LoadingManager.onStart: isInitialLoad=${isInitialLoad}, total=${total}, url=${_url}`);
|
||||
// Показываем оверлей только при начальной загрузке или при загрузке большого количества ресурсов
|
||||
if (isInitialLoad || total > 10) {
|
||||
console.log('Показываем overlay для загрузки');
|
||||
createLoadingOverlay();
|
||||
updateLoadingOverlay(total ? (loaded / total) * 100 : 5, 'Загрузка ресурсов...');
|
||||
} else {
|
||||
console.log('Не показываем overlay - небольшая загрузка');
|
||||
}
|
||||
};
|
||||
|
||||
loadingManager.onProgress = (_url, loaded, total) => {
|
||||
updateLoadingOverlay(total ? (loaded / total) * 100 : 50);
|
||||
if (overlayEl && (isInitialLoad || total > 10)) {
|
||||
updateLoadingOverlay(total ? (loaded / total) * 100 : 50);
|
||||
}
|
||||
};
|
||||
|
||||
loadingManager.onLoad = () => {
|
||||
updateLoadingOverlay(100, 'Инициализация сцены...');
|
||||
setTimeout(removeLoadingOverlay, 150);
|
||||
console.log(`LoadingManager.onLoad: isInitialLoad=${isInitialLoad}, overlayEl=${!!overlayEl}`);
|
||||
if (overlayEl) {
|
||||
// Показываем "Инициализация сцены" только для начальной загрузки
|
||||
if (isInitialLoad) {
|
||||
console.log('Показываем "Инициализация сцены" для начальной загрузки');
|
||||
updateLoadingOverlay(100, 'Инициализация сцены...');
|
||||
setTimeout(removeLoadingOverlay, 150);
|
||||
} else {
|
||||
// Для небольших загрузок просто скрываем overlay
|
||||
console.log('Скрываем overlay для небольшой загрузки');
|
||||
removeLoadingOverlay();
|
||||
}
|
||||
}
|
||||
isInitialLoad = false; // После первой загрузки сбрасываем флаг
|
||||
|
||||
// Дополнительная защита - принудительно скрываем overlay через 3 секунды
|
||||
if (overlayEl) {
|
||||
overlayTimeoutRef.current = setTimeout(() => {
|
||||
if (overlayEl) {
|
||||
console.log('Принудительно скрываем overlay по таймауту');
|
||||
removeLoadingOverlay();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Глобальная защита - принудительно скрываем overlay через 5 секунд после начала игры
|
||||
overlayTimeoutRef.current = setTimeout(() => {
|
||||
if (overlayEl && !isInitialLoad) {
|
||||
console.log('Глобальная защита: принудительно скрываем overlay');
|
||||
removeLoadingOverlay();
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
|
||||
@@ -1989,7 +2273,7 @@ function Game({ avatarUrl, gender }) {
|
||||
const baseOffset = new THREE.Vector3(-200, 150, -200);
|
||||
const planarDist = Math.hypot(baseOffset.x, baseOffset.z);
|
||||
const radius = Math.hypot(planarDist, baseOffset.y);
|
||||
const baseAzimuth = Math.atan2(baseOffset.z, baseOffset.x);
|
||||
let baseAzimuth = Math.atan2(baseOffset.z, baseOffset.x);
|
||||
const basePolar = Math.atan2(baseOffset.y, planarDist);
|
||||
|
||||
let cameraPitchOffset = 0;
|
||||
@@ -2068,6 +2352,19 @@ function Game({ avatarUrl, gender }) {
|
||||
const p = JSON.parse(sessionStorage.getItem('user_profile') || '{}');
|
||||
if (p?.id) socket.emit('economy:getBalance', { userId: p.id });
|
||||
}, 3000);
|
||||
|
||||
// Периодическое обновление статуса пользователей для Telegram
|
||||
const statusInterval = setInterval(() => {
|
||||
if (activeApp === "Telegram" && telegramContacts.length > 0) {
|
||||
loadTelegramContacts();
|
||||
// Обновляем счетчики непрочитанных сообщений для всех контактов
|
||||
telegramContacts.forEach(contact => {
|
||||
if (contact.id !== profile.id) {
|
||||
updateUnreadCount(contact.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 30000); // Обновляем каждые 30 секунд
|
||||
socket.on('economy:balanceChanged', ({ userId, newBalance }) => {
|
||||
if (userId === profile.id) {
|
||||
setBalance(newBalance);
|
||||
@@ -2078,6 +2375,38 @@ function Game({ avatarUrl, gender }) {
|
||||
socket.emit('economy:getInventory', { userId: profile.id });
|
||||
socket.on('economy:inventory', setInventory);
|
||||
socket.on('gameTime:update', ({ time }) => setGameTime(time));
|
||||
|
||||
// Обработчик изменения статуса пользователей для Telegram
|
||||
socket.on('userStatusChanged', ({ userId, isOnline }) => {
|
||||
console.log('Статус пользователя изменился:', { userId, isOnline });
|
||||
setTelegramContacts(prev => prev.map(user =>
|
||||
user.id === userId ? { ...user, isOnline } : user
|
||||
));
|
||||
});
|
||||
|
||||
// Обработчик новых сообщений для уведомлений
|
||||
socket.on('newMessage', ({ id, text, senderId, timestamp, isRead }) => {
|
||||
console.log('Новое сообщение:', { id, text, senderId, timestamp, isRead });
|
||||
|
||||
// Показываем уведомление только если Telegram не открыт
|
||||
if (activeApp !== "Telegram") {
|
||||
showMessageNotification(senderId, text);
|
||||
}
|
||||
|
||||
// Обновляем счетчик непрочитанных сообщений
|
||||
updateUnreadCount(senderId);
|
||||
|
||||
// Обновляем список сообщений если открыт чат с этим пользователем
|
||||
if (activeChat && activeChat.id === senderId) {
|
||||
loadMessages(senderId);
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик перезагрузки сервера
|
||||
socket.on('serverRestart', ({ message, restartIn }) => {
|
||||
console.log('Сервер будет перезагружен:', { message, restartIn });
|
||||
showServerRestartNotification(message, restartIn);
|
||||
});
|
||||
// Лоадеры, учитывающиеся в прогрессе через loadingManagerRef
|
||||
const gltfLoader = new GLTFLoader(loadingManagerRef.current || undefined);
|
||||
const animLoader = new GLTFLoader(loadingManagerRef.current || undefined);
|
||||
@@ -2618,11 +2947,23 @@ function Game({ avatarUrl, gender }) {
|
||||
const delta = -e.deltaY * 0.001;
|
||||
|
||||
if (e.ctrlKey) {
|
||||
// При нажатом Ctrl управляем и вертикальным, и горизонтальным углом камеры
|
||||
if (e.shiftKey) {
|
||||
// Shift + Ctrl + колесо = горизонтальный поворот (влево-вправо)
|
||||
const horizontalDelta = delta * 2; // Увеличиваем чувствительность
|
||||
baseAzimuth = THREE.MathUtils.clamp(
|
||||
baseAzimuth + horizontalDelta,
|
||||
-Math.PI / 2, // -90 градусов
|
||||
Math.PI / 2 // +90 градусов
|
||||
);
|
||||
} else {
|
||||
// Ctrl + колесо = вертикальный поворот (вверх-вниз)
|
||||
cameraPitchOffset = THREE.MathUtils.clamp(
|
||||
cameraPitchOffset + delta,
|
||||
-maxPitch,
|
||||
maxPitch
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (cameraRef.current === orthoCamRef.current) {
|
||||
zoom = THREE.MathUtils.clamp(zoom * (1 + delta), minZoom, maxZoom);
|
||||
@@ -3531,6 +3872,26 @@ function Game({ avatarUrl, gender }) {
|
||||
setShowInventory(v => !v);
|
||||
}
|
||||
|
||||
// Ctrl + Arrow keys for camera control
|
||||
if (event.ctrlKey) {
|
||||
const key = event.key.toLowerCase();
|
||||
if (key === 'arrowleft') {
|
||||
const horizontalDelta = -0.1; // Поворот влево
|
||||
baseAzimuth = THREE.MathUtils.clamp(
|
||||
baseAzimuth + horizontalDelta,
|
||||
-Math.PI / 2, // -90 градусов
|
||||
Math.PI / 2 // +90 градусов
|
||||
);
|
||||
} else if (key === 'arrowright') {
|
||||
const horizontalDelta = 0.1; // Поворот вправо
|
||||
baseAzimuth = THREE.MathUtils.clamp(
|
||||
baseAzimuth + horizontalDelta,
|
||||
-Math.PI / 2, // -90 градусов
|
||||
Math.PI / 2 // +90 градусов
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Сбрасываем назначение только если не в интерьере
|
||||
if (!isInInteriorRef.current) {
|
||||
destination = null;
|
||||
@@ -3846,6 +4207,7 @@ function Game({ avatarUrl, gender }) {
|
||||
// Поворот влево-вправо (A/D или стрелки)
|
||||
if (move.left) player.rotation.y += rotSpeed * delta;
|
||||
if (move.right) player.rotation.y -= rotSpeed * delta;
|
||||
|
||||
// Камера следует за вращением тела
|
||||
const headHeight = 1.6;
|
||||
const camBase = new THREE.Vector3(player.position.x, player.position.y + headHeight, player.position.z);
|
||||
@@ -3854,45 +4216,96 @@ function Game({ avatarUrl, gender }) {
|
||||
const lookForward = new THREE.Vector3(0, 0, -1).applyEuler(new THREE.Euler(0, player.rotation.y, 0));
|
||||
fpCamRef.current.lookAt(fpCamRef.current.position.clone().add(lookForward));
|
||||
|
||||
// Движение с проверкой коллизий
|
||||
// Улучшенное движение с проверкой коллизий и предотвращением застревания
|
||||
const tryMove = (dirVec) => {
|
||||
const candidate = player.position.clone().addScaledVector(dirVec, speed * delta);
|
||||
// Обновляем AABB игрока (простая капсула не используется, только коробка)
|
||||
const half = 0.25; // чуточку уже, чтобы не цепляться за стены
|
||||
const height = 1.7; // немного ниже, чтобы не пересекать потолок
|
||||
const stepDistance = speed * delta;
|
||||
const candidate = player.position.clone().addScaledVector(dirVec, stepDistance);
|
||||
|
||||
// Обновляем AABB игрока с меньшими размерами для предотвращения застревания
|
||||
const half = 0.2; // Уменьшаем размер для лучшего прохождения
|
||||
const height = 1.6; // Немного ниже для предотвращения застревания в потолке
|
||||
const playerBox = new THREE.Box3(
|
||||
new THREE.Vector3(candidate.x - half, candidate.y, candidate.z - half),
|
||||
new THREE.Vector3(candidate.x + half, candidate.y + height, candidate.z + half)
|
||||
);
|
||||
|
||||
// Обновляем мировые матрицы статических коллайдеров для корректных AABB
|
||||
try { interiorGroupRef.current && interiorGroupRef.current.updateMatrixWorld(true); } catch (_) { }
|
||||
try {
|
||||
interiorGroupRef.current && interiorGroupRef.current.updateMatrixWorld(true);
|
||||
} catch (_) { }
|
||||
|
||||
// В интерьере учитываем только внутренние коллайдеры, без городских объектов
|
||||
// В интерьере учитываем только внутренние коллайдеры
|
||||
const blockingMeshes = Array.isArray(interiorCollidersRef.current)
|
||||
? interiorCollidersRef.current
|
||||
: [];
|
||||
|
||||
let hits = false;
|
||||
let closestDistance = Infinity;
|
||||
let slideDirection = null;
|
||||
|
||||
for (const mesh of blockingMeshes) {
|
||||
if (!mesh) continue;
|
||||
const box = new THREE.Box3().setFromObject(mesh);
|
||||
// небольшой зазор, чтобы скользить вдоль стен
|
||||
const expanded = box.clone().expandByScalar(0.01);
|
||||
if (expanded.intersectsBox(playerBox)) { hits = true; break; }
|
||||
const expanded = box.clone().expandByScalar(0.05); // Увеличиваем зазор
|
||||
|
||||
if (expanded.intersectsBox(playerBox)) {
|
||||
hits = true;
|
||||
|
||||
// Вычисляем направление скольжения вдоль стены
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const toPlayer = player.position.clone().sub(center);
|
||||
const distance = toPlayer.length();
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
// Нормализуем и создаем направление скольжения
|
||||
toPlayer.normalize();
|
||||
slideDirection = toPlayer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hits) {
|
||||
// Свободное движение
|
||||
player.position.copy(candidate);
|
||||
} else if (slideDirection) {
|
||||
// Скольжение вдоль стены
|
||||
const slideDistance = stepDistance * 0.7; // Уменьшаем дистанцию скольжения
|
||||
const slidePos = player.position.clone().addScaledVector(slideDirection, slideDistance);
|
||||
|
||||
// Проверяем, можно ли двигаться в направлении скольжения
|
||||
const slideBox = new THREE.Box3(
|
||||
new THREE.Vector3(slidePos.x - half, slidePos.y, slidePos.z - half),
|
||||
new THREE.Vector3(slidePos.x + half, slidePos.y + height, slidePos.z + half)
|
||||
);
|
||||
|
||||
let canSlide = true;
|
||||
for (const mesh of blockingMeshes) {
|
||||
if (!mesh) continue;
|
||||
const box = new THREE.Box3().setFromObject(mesh);
|
||||
const expanded = box.clone().expandByScalar(0.05);
|
||||
if (expanded.intersectsBox(slideBox)) {
|
||||
canSlide = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (canSlide) {
|
||||
player.position.copy(slidePos);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(player.quaternion);
|
||||
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(player.quaternion);
|
||||
|
||||
// Применяем движение с плавностью
|
||||
if (move.forward) tryMove(forward);
|
||||
if (move.backward) tryMove(forward.clone().multiplyScalar(-1));
|
||||
if (move.strafeLeft) tryMove(right.clone().multiplyScalar(-1));
|
||||
if (move.strafeRight) tryMove(right);
|
||||
|
||||
// Отправляем позицию внутри интерьера, чтобы нас видели другие внутри
|
||||
// Отправляем позицию внутри интерьера
|
||||
if (socketRef.current) {
|
||||
socketRef.current.emit('playerMovement', { x: player.position.x, y: player.position.y, z: player.position.z });
|
||||
}
|
||||
@@ -4028,9 +4441,38 @@ function Game({ avatarUrl, gender }) {
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', onWindowResize, false);
|
||||
|
||||
// Отключаем браузерное масштабирование
|
||||
document.addEventListener('wheel', (e) => {
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && (e.key === '+' || e.key === '-' || e.key === '=')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Показываем подсказку об управлении камерой
|
||||
setTimeout(() => {
|
||||
showCameraControlsHint();
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearInterval(balanceInterval);
|
||||
clearInterval(statusInterval);
|
||||
|
||||
// Очищаем overlay загрузки
|
||||
if (overlayEl) {
|
||||
removeLoadingOverlay();
|
||||
}
|
||||
|
||||
// Очищаем все таймеры overlay
|
||||
if (overlayTimeoutRef.current) {
|
||||
clearTimeout(overlayTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Очищаем таймеры throttling
|
||||
if (wheelTimeout) {
|
||||
@@ -5632,13 +6074,68 @@ function Game({ avatarUrl, gender }) {
|
||||
)}
|
||||
{telegramContacts.map((user) => (
|
||||
<div key={user.id} onClick={() => setActiveChat(user)} style={{ padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', color: '#111' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ width: 28, height: 28, borderRadius: 14, background: '#e5e7eb', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12 }}>
|
||||
{user.firstName?.[0]}{user.lastName?.[0]}
|
||||
</div>
|
||||
{user.isOnline && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
background: '#10b981',
|
||||
border: '2px solid #fff',
|
||||
boxShadow: '0 0 0 1px #e5e7eb'
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<div style={{ whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }}>{user.firstName} {user.lastName}</div>
|
||||
<div style={{ fontSize: 12, color: '#6b7280' }}>Онлайн</div>
|
||||
<div style={{ overflow: 'hidden', flex: 1 }}>
|
||||
<div style={{
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
fontWeight: user.unreadCount > 0 ? 'bold' : 'normal'
|
||||
}}>
|
||||
{`${user.firstName || ''} ${user.lastName || ''}`}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 12,
|
||||
color: user.isOnline ? '#10b981' : '#6b7280',
|
||||
fontWeight: user.isOnline ? 600 : 400
|
||||
}}>
|
||||
{user.isOnline ? 'Онлайн' : (
|
||||
user.lastSeen ?
|
||||
`Был(а) ${new Date(user.lastSeen).toLocaleString('ru-RU', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}` :
|
||||
'Офлайн'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Счетчик непрочитанных сообщений */}
|
||||
{user.unreadCount > 0 && (
|
||||
<div style={{
|
||||
background: '#ef4444',
|
||||
color: 'white',
|
||||
borderRadius: '50%',
|
||||
minWidth: '20px',
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
padding: '2px'
|
||||
}}>
|
||||
{user.unreadCount > 99 ? '99+' : user.unreadCount}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -33,4 +33,44 @@ export async function registerStep1(data) {
|
||||
});
|
||||
return r.json();
|
||||
}
|
||||
|
||||
export const getUsersStatus = async (token) => {
|
||||
try {
|
||||
const response = await fetch('/api/users/status', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch users status');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching users status:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const loadUserInfo = async (userId, token) => {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch user info');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching user info:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
217
test_telegram_fix.html
Normal file
217
test_telegram_fix.html
Normal file
@@ -0,0 +1,217 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Тест исправления Telegram</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.test-section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.input-group {
|
||||
margin: 10px 0;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input, button {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.result {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Тест исправления Telegram контактов</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>1. Тест загрузки контактов</h2>
|
||||
<div class="input-group">
|
||||
<label for="token1">JWT Token:</label>
|
||||
<input type="text" id="token1" placeholder="Введите JWT токен" style="width: 400px;">
|
||||
</div>
|
||||
<button onclick="testLoadContacts()">Загрузить контакты</button>
|
||||
<div id="contactsResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>2. Тест получения информации о пользователе</h2>
|
||||
<div class="input-group">
|
||||
<label for="token2">JWT Token:</label>
|
||||
<input type="text" id="token2" placeholder="Введите JWT токен" style="width: 400px;">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="userId">User ID:</label>
|
||||
<input type="number" id="userId" placeholder="Введите ID пользователя">
|
||||
</div>
|
||||
<button onclick="testGetUserInfo()">Получить информацию о пользователе</button>
|
||||
<div id="userInfoResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>3. Тест количества непрочитанных сообщений</h2>
|
||||
<div class="input-group">
|
||||
<label for="token3">JWT Token:</label>
|
||||
<input type="text" id="token3" placeholder="Введите JWT токен" style="width: 400px;">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="contactId">Contact ID:</label>
|
||||
<input type="number" id="contactId" placeholder="Введите ID контакта">
|
||||
</div>
|
||||
<button onclick="testUnreadCount()">Получить количество непрочитанных</button>
|
||||
<div id="unreadResult" class="result"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function testLoadContacts() {
|
||||
const token = document.getElementById('token1').value;
|
||||
const resultDiv = document.getElementById('contactsResult');
|
||||
|
||||
if (!token) {
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = 'Ошибка: Введите JWT токен';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users/status', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
resultDiv.className = 'result success';
|
||||
resultDiv.textContent = `✅ Успешно загружено ${data.length} контактов:\n\n${JSON.stringify(data, null, 2)}`;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = `❌ Ошибка ${response.status}: ${errorText}`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = `❌ Ошибка сети: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetUserInfo() {
|
||||
const token = document.getElementById('token2').value;
|
||||
const userId = document.getElementById('userId').value;
|
||||
const resultDiv = document.getElementById('userInfoResult');
|
||||
|
||||
if (!token || !userId) {
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = 'Ошибка: Введите JWT токен и User ID';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
resultDiv.className = 'result success';
|
||||
resultDiv.textContent = `✅ Информация о пользователе:\n\n${JSON.stringify(data, null, 2)}`;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = `❌ Ошибка ${response.status}: ${errorText}`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = `❌ Ошибка сети: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testUnreadCount() {
|
||||
const token = document.getElementById('token3').value;
|
||||
const contactId = document.getElementById('contactId').value;
|
||||
const resultDiv = document.getElementById('unreadResult');
|
||||
|
||||
if (!token || !contactId) {
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = 'Ошибка: Введите JWT токен и Contact ID';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/messages-read/${contactId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
resultDiv.className = 'result success';
|
||||
resultDiv.textContent = `✅ Количество непрочитанных сообщений:\n\n${JSON.stringify(data, null, 2)}`;
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = `❌ Ошибка ${response.status}: ${errorText}`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.className = 'result error';
|
||||
resultDiv.textContent = `❌ Ошибка сети: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Автоматически копируем токен между полями
|
||||
document.getElementById('token1').addEventListener('input', function() {
|
||||
document.getElementById('token2').value = this.value;
|
||||
document.getElementById('token3').value = this.value;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
201
test_telegram_improvements.html
Normal file
201
test_telegram_improvements.html
Normal file
@@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Тест улучшений Telegram</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
input[type="text"], input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
background: #0084ff;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0073e6;
|
||||
}
|
||||
.result {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 15px;
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Тест улучшений Telegram</h1>
|
||||
|
||||
<div class="container">
|
||||
<h2>1. Получение статуса пользователей</h2>
|
||||
<div class="form-group">
|
||||
<label for="token1">JWT Token:</label>
|
||||
<input type="text" id="token1" placeholder="Введите JWT токен">
|
||||
</div>
|
||||
<button onclick="testUsersStatus()">Получить статус пользователей</button>
|
||||
<div id="result1" class="result" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>2. Получение информации о пользователе по ID</h2>
|
||||
<div class="form-group">
|
||||
<label for="token2">JWT Token:</label>
|
||||
<input type="text" id="token2" placeholder="Введите JWT токен">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="userId">ID пользователя:</label>
|
||||
<input type="number" id="userId" placeholder="Введите ID пользователя">
|
||||
</div>
|
||||
<button onclick="testUserInfo()">Получить информацию о пользователе</button>
|
||||
<div id="result2" class="result" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>3. Получение количества непрочитанных сообщений</h2>
|
||||
<div class="form-group">
|
||||
<label for="token3">JWT Token:</label>
|
||||
<input type="text" id="token3" placeholder="Введите JWT токен">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="contactId">ID контакта:</label>
|
||||
<input type="number" id="contactId" placeholder="Введите ID контакта">
|
||||
</div>
|
||||
<button onclick="testUnreadCount()">Получить количество непрочитанных сообщений</button>
|
||||
<div id="result3" class="result" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showResult(elementId, data, isError = false) {
|
||||
const element = document.getElementById(elementId);
|
||||
element.style.display = 'block';
|
||||
element.className = `result ${isError ? 'error' : 'success'}`;
|
||||
element.textContent = JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
async function testUsersStatus() {
|
||||
const token = document.getElementById('token1').value;
|
||||
if (!token) {
|
||||
showResult('result1', { error: 'Введите JWT токен' }, true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users/status', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
showResult('result1', data);
|
||||
} else {
|
||||
showResult('result1', data, true);
|
||||
}
|
||||
} catch (error) {
|
||||
showResult('result1', { error: error.message }, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function testUserInfo() {
|
||||
const token = document.getElementById('token2').value;
|
||||
const userId = document.getElementById('userId').value;
|
||||
|
||||
if (!token || !userId) {
|
||||
showResult('result2', { error: 'Введите JWT токен и ID пользователя' }, true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
showResult('result2', data);
|
||||
} else {
|
||||
showResult('result2', data, true);
|
||||
}
|
||||
} catch (error) {
|
||||
showResult('result2', { error: error.message }, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function testUnreadCount() {
|
||||
const token = document.getElementById('token3').value;
|
||||
const contactId = document.getElementById('contactId').value;
|
||||
|
||||
if (!token || !contactId) {
|
||||
showResult('result3', { error: 'Введите JWT токен и ID контакта' }, true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/messages-read/${contactId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
showResult('result3', data);
|
||||
} else {
|
||||
showResult('result3', data, true);
|
||||
}
|
||||
} catch (error) {
|
||||
showResult('result3', { error: error.message }, true);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
154
test_telegram_status.html
Normal file
154
test_telegram_status.html
Normal file
@@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Тест Telegram Status API</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
margin: 5px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background: #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
}
|
||||
.online-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background: #10b981;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
.status {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
.status.online {
|
||||
color: #10b981;
|
||||
font-weight: 600;
|
||||
}
|
||||
.error {
|
||||
color: #dc2626;
|
||||
padding: 10px;
|
||||
background: #fef2f2;
|
||||
border-radius: 8px;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
background: #0084ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin: 10px 0;
|
||||
}
|
||||
button:hover {
|
||||
background: #0066cc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Тест Telegram Status API</h1>
|
||||
|
||||
<div>
|
||||
<label for="token">JWT Token:</label><br>
|
||||
<input type="text" id="token" placeholder="Введите JWT токен" style="width: 100%; padding: 10px; margin: 10px 0;">
|
||||
<br>
|
||||
<button onclick="testAPI()">Тестировать API</button>
|
||||
<button onclick="clearResults()">Очистить результаты</button>
|
||||
</div>
|
||||
|
||||
<div id="results"></div>
|
||||
|
||||
<script>
|
||||
async function testAPI() {
|
||||
const token = document.getElementById('token').value;
|
||||
const resultsDiv = document.getElementById('results');
|
||||
|
||||
if (!token) {
|
||||
resultsDiv.innerHTML = '<div class="error">Введите JWT токен</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resultsDiv.innerHTML = '<div>Загрузка...</div>';
|
||||
|
||||
const response = await fetch('/api/users/status', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const users = await response.json();
|
||||
|
||||
let html = '<h2>Результат:</h2>';
|
||||
html += `<p>Найдено пользователей: ${users.length}</p>`;
|
||||
|
||||
users.forEach(user => {
|
||||
const isOnline = user.isOnline;
|
||||
const lastSeen = user.lastSeen;
|
||||
|
||||
let statusText = isOnline ? 'Онлайн' : 'Офлайн';
|
||||
if (!isOnline && lastSeen) {
|
||||
statusText = `Был(а) ${new Date(lastSeen).toLocaleString('ru-RU')}`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="user-item">
|
||||
<div class="avatar">
|
||||
${user.firstName?.[0] || ''}${user.lastName?.[0] || ''}
|
||||
${isOnline ? '<div class="online-indicator"></div>' : ''}
|
||||
</div>
|
||||
<div>
|
||||
<div><strong>${user.firstName} ${user.lastName}</strong></div>
|
||||
<div class="status ${isOnline ? 'online' : ''}">${statusText}</div>
|
||||
<div style="font-size: 12px; color: #999;">
|
||||
ID: ${user.id} | Online: ${isOnline} | LastSeen: ${lastSeen || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
resultsDiv.innerHTML = html;
|
||||
|
||||
} catch (error) {
|
||||
resultsDiv.innerHTML = `<div class="error">Ошибка: ${error.message}</div>`;
|
||||
console.error('API Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
document.getElementById('results').innerHTML = '';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user