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);
|
console.error('Ошибка при импорте db1 - virtual_World:', e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
try {
|
try {
|
||||||
new_quest_Base = require('./db2');
|
new_quest_Base = require('./db2');
|
||||||
console.log('db2 - new_quest_Base - успешно импортирован');
|
console.log('db2 - new_quest_Base - успешно импортирован');
|
||||||
@@ -42,7 +42,7 @@ catch (e) {
|
|||||||
console.error('Ошибка при импорте db2 - new_quest_Base: ', e);
|
console.error('Ошибка при импорте db2 - new_quest_Base: ', e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
try {
|
try {
|
||||||
db = require('./db');
|
db = require('./db');
|
||||||
console.log('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);
|
const organizationsRouter = require('./server/organizations')(io, onlineUsers);
|
||||||
app.use('/api/organizations', organizationsRouter);
|
app.use('/api/organizations', organizationsRouter);
|
||||||
@@ -120,7 +121,19 @@ io.use((socket, next) => {
|
|||||||
try {
|
try {
|
||||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
socket.userId = payload.id;
|
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();
|
next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(new Error('Invalid token'));
|
next(new Error('Invalid token'));
|
||||||
@@ -315,7 +328,7 @@ io.on('connection', socket => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const newMessage = result.rows[0];
|
const newMessage = result.rows[0];
|
||||||
const receiverSocketId = onlineUsers[recvId];
|
const receiverSocketId = onlineUsers.get(recvId);
|
||||||
|
|
||||||
// Отправка получателю
|
// Отправка получателю
|
||||||
if (receiverSocketId) {
|
if (receiverSocketId) {
|
||||||
@@ -404,7 +417,18 @@ io.on('connection', socket => {
|
|||||||
|
|
||||||
// --- Отключение ---
|
// --- Отключение ---
|
||||||
socket.on('disconnect', async () => {
|
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 cityId = socket.cityId;
|
||||||
const player = playersByCity[cityId]?.[socket.id];
|
const player = playersByCity[cityId]?.[socket.id];
|
||||||
if (player) {
|
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) => {
|
app.get('/api/messages/:contactId', authenticate, async (req, res) => {
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
@@ -595,7 +695,7 @@ app.post('/api/messages/send', authenticate, async (req, res) => {
|
|||||||
);
|
);
|
||||||
const newMessage = result.rows[0];
|
const newMessage = result.rows[0];
|
||||||
|
|
||||||
const receiverSocketId = onlineUsers[recvId];
|
const receiverSocketId = onlineUsers.get(recvId);
|
||||||
if (receiverSocketId) {
|
if (receiverSocketId) {
|
||||||
io.to(receiverSocketId).emit('newMessage', {
|
io.to(receiverSocketId).emit('newMessage', {
|
||||||
id: newMessage.id,
|
id: newMessage.id,
|
||||||
@@ -1531,10 +1631,46 @@ app.use((req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PORT = process.env.PORT || 4000;
|
const PORT = process.env.PORT || 4000;
|
||||||
http.listen(PORT, () => {
|
const server = http.listen(PORT, () => {
|
||||||
console.log(`Server is running on port ${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
|
// Логирование всех маршрутов и middleware
|
||||||
['get', 'post', 'put', 'delete', 'use'].forEach(method => {
|
['get', 'post', 'put', 'delete', 'use'].forEach(method => {
|
||||||
const orig = app[method];
|
const orig = app[method];
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ module.exports = function(io, onlineUsers) {
|
|||||||
let thirst = parseFloat(rows[0].thirst ?? 100);
|
let thirst = parseFloat(rows[0].thirst ?? 100);
|
||||||
|
|
||||||
if (balance < price) {
|
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}` });
|
if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вам недостаточно средств для покупки ${itemDef.name}` });
|
||||||
return res.status(400).json({ error: 'insufficient funds' });
|
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));
|
satiety = Math.min(100, satiety + parseFloat(itemDef.hunger_gain));
|
||||||
thirst = Math.min(100, thirst + parseFloat(itemDef.thirst_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}` });
|
if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вы купили ${itemDef.name}` });
|
||||||
|
|
||||||
res.json({ success: true, balance, satiety, thirst });
|
res.json({ success: true, balance, satiety, thirst });
|
||||||
|
|||||||
545
src/Game.js
545
src/Game.js
@@ -15,6 +15,8 @@ import Inventory from './components/Inventory';
|
|||||||
import OrgControlPanel from './components/OrgControlPanel';
|
import OrgControlPanel from './components/OrgControlPanel';
|
||||||
import DoubleTapWrapper from './pages/DoubleTapWrapper';
|
import DoubleTapWrapper from './pages/DoubleTapWrapper';
|
||||||
import WaveformPlayer from './pages/WaveformPlayer';
|
import WaveformPlayer from './pages/WaveformPlayer';
|
||||||
|
import { getUsersStatus, loadUserInfo } from './api/auth.js';
|
||||||
|
|
||||||
function Game({ avatarUrl, gender }) {
|
function Game({ avatarUrl, gender }) {
|
||||||
|
|
||||||
// 1) реф для хранилища сцены
|
// 1) реф для хранилища сцены
|
||||||
@@ -31,6 +33,7 @@ function Game({ avatarUrl, gender }) {
|
|||||||
const cleanupTimerRef = useRef(null);
|
const cleanupTimerRef = useRef(null);
|
||||||
// Глобальный менеджер прогресса загрузки (используем в GLTFLoader)
|
// Глобальный менеджер прогресса загрузки (используем в GLTFLoader)
|
||||||
const loadingManagerRef = useRef(null);
|
const loadingManagerRef = useRef(null);
|
||||||
|
const overlayTimeoutRef = useRef(null);
|
||||||
// Кликабельные объекты внутри интерьера
|
// Кликабельные объекты внутри интерьера
|
||||||
const interiorInteractablesRef = useRef([]);
|
const interiorInteractablesRef = useRef([]);
|
||||||
const npcMeshesRef = useRef([]);
|
const npcMeshesRef = useRef([]);
|
||||||
@@ -1341,14 +1344,19 @@ function Game({ avatarUrl, gender }) {
|
|||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
try {
|
try {
|
||||||
setTgError(null);
|
setTgError(null);
|
||||||
const res = await fetch('/api/users', {
|
const res = await fetch('/api/users/status', {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
cache: 'no-cache'
|
cache: 'no-cache'
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setTelegramContacts(data);
|
// Добавляем счетчик непрочитанных сообщений для каждого пользователя
|
||||||
|
const dataWithUnread = data.map(user => ({
|
||||||
|
...user,
|
||||||
|
unreadCount: 0
|
||||||
|
}));
|
||||||
|
setTelegramContacts(dataWithUnread);
|
||||||
} else {
|
} else {
|
||||||
const txt = await res.text().catch(() => '');
|
const txt = await res.text().catch(() => '');
|
||||||
console.error('Ошибка загрузки контактов Telegram', res.status, txt);
|
console.error('Ошибка загрузки контактов Telegram', res.status, txt);
|
||||||
@@ -1367,6 +1375,223 @@ function Game({ avatarUrl, gender }) {
|
|||||||
//const [readmes, setReadmes] = useState('false');
|
//const [readmes, setReadmes] = useState('false');
|
||||||
const [userProfile, setUserProfile] = useState(null);
|
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) {
|
async function loadMessages(contactId) {
|
||||||
if (!contactId) return;
|
if (!contactId) return;
|
||||||
@@ -1910,11 +2135,18 @@ function Game({ avatarUrl, gender }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
// Красивый загрузочный оверлей + LoadingManager
|
// Улучшенный загрузочный оверлей + LoadingManager
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
let overlayEl = null, barEl = null, textEl = null;
|
let overlayEl = null, barEl = null, textEl = null;
|
||||||
|
let isInitialLoad = true; // Флаг для определения начальной загрузки
|
||||||
|
|
||||||
function createLoadingOverlay() {
|
function createLoadingOverlay() {
|
||||||
if (overlayEl) return;
|
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');
|
overlayEl = document.createElement('div');
|
||||||
Object.assign(overlayEl.style, {
|
Object.assign(overlayEl.style, {
|
||||||
position: 'fixed', inset: '0', zIndex: 2000,
|
position: 'fixed', inset: '0', zIndex: 2000,
|
||||||
@@ -1951,6 +2183,7 @@ function Game({ avatarUrl, gender }) {
|
|||||||
overlayEl.appendChild(pct);
|
overlayEl.appendChild(pct);
|
||||||
document.body.appendChild(overlayEl);
|
document.body.appendChild(overlayEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLoadingOverlay(percent, text) {
|
function updateLoadingOverlay(percent, text) {
|
||||||
if (!overlayEl) return;
|
if (!overlayEl) return;
|
||||||
const p = Math.max(0, Math.min(100, Math.round(percent || 0)));
|
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 (pct) pct.textContent = p + '%';
|
||||||
if (text && textEl) textEl.textContent = text;
|
if (text && textEl) textEl.textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeLoadingOverlay() {
|
function removeLoadingOverlay() {
|
||||||
if (!overlayEl) return;
|
if (!overlayEl) return;
|
||||||
|
|
||||||
|
// Очищаем все таймеры overlay
|
||||||
|
if (overlayTimeoutRef.current) {
|
||||||
|
clearTimeout(overlayTimeoutRef.current);
|
||||||
|
overlayTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
overlayEl.style.transition = 'opacity .2s ease';
|
overlayEl.style.transition = 'opacity .2s ease';
|
||||||
overlayEl.style.opacity = '0';
|
overlayEl.style.opacity = '0';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -1968,19 +2209,62 @@ function Game({ avatarUrl, gender }) {
|
|||||||
overlayEl = barEl = textEl = null;
|
overlayEl = barEl = textEl = null;
|
||||||
}, 220);
|
}, 220);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Общий менеджер загрузки (для GLTF/Texture и т.п.)
|
// Общий менеджер загрузки (для GLTF/Texture и т.п.)
|
||||||
const loadingManager = new THREE.LoadingManager();
|
const loadingManager = new THREE.LoadingManager();
|
||||||
loadingManagerRef.current = loadingManager;
|
loadingManagerRef.current = loadingManager;
|
||||||
|
|
||||||
loadingManager.onStart = (_url, loaded, total) => {
|
loadingManager.onStart = (_url, loaded, total) => {
|
||||||
createLoadingOverlay();
|
console.log(`LoadingManager.onStart: isInitialLoad=${isInitialLoad}, total=${total}, url=${_url}`);
|
||||||
updateLoadingOverlay(total ? (loaded / total) * 100 : 5, 'Загрузка ресурсов...');
|
// Показываем оверлей только при начальной загрузке или при загрузке большого количества ресурсов
|
||||||
|
if (isInitialLoad || total > 10) {
|
||||||
|
console.log('Показываем overlay для загрузки');
|
||||||
|
createLoadingOverlay();
|
||||||
|
updateLoadingOverlay(total ? (loaded / total) * 100 : 5, 'Загрузка ресурсов...');
|
||||||
|
} else {
|
||||||
|
console.log('Не показываем overlay - небольшая загрузка');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadingManager.onProgress = (_url, loaded, total) => {
|
loadingManager.onProgress = (_url, loaded, total) => {
|
||||||
updateLoadingOverlay(total ? (loaded / total) * 100 : 50);
|
if (overlayEl && (isInitialLoad || total > 10)) {
|
||||||
|
updateLoadingOverlay(total ? (loaded / total) * 100 : 50);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadingManager.onLoad = () => {
|
loadingManager.onLoad = () => {
|
||||||
updateLoadingOverlay(100, 'Инициализация сцены...');
|
console.log(`LoadingManager.onLoad: isInitialLoad=${isInitialLoad}, overlayEl=${!!overlayEl}`);
|
||||||
setTimeout(removeLoadingOverlay, 150);
|
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 baseOffset = new THREE.Vector3(-200, 150, -200);
|
||||||
const planarDist = Math.hypot(baseOffset.x, baseOffset.z);
|
const planarDist = Math.hypot(baseOffset.x, baseOffset.z);
|
||||||
const radius = Math.hypot(planarDist, baseOffset.y);
|
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);
|
const basePolar = Math.atan2(baseOffset.y, planarDist);
|
||||||
|
|
||||||
let cameraPitchOffset = 0;
|
let cameraPitchOffset = 0;
|
||||||
@@ -2068,6 +2352,19 @@ function Game({ avatarUrl, gender }) {
|
|||||||
const p = JSON.parse(sessionStorage.getItem('user_profile') || '{}');
|
const p = JSON.parse(sessionStorage.getItem('user_profile') || '{}');
|
||||||
if (p?.id) socket.emit('economy:getBalance', { userId: p.id });
|
if (p?.id) socket.emit('economy:getBalance', { userId: p.id });
|
||||||
}, 3000);
|
}, 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 }) => {
|
socket.on('economy:balanceChanged', ({ userId, newBalance }) => {
|
||||||
if (userId === profile.id) {
|
if (userId === profile.id) {
|
||||||
setBalance(newBalance);
|
setBalance(newBalance);
|
||||||
@@ -2078,6 +2375,38 @@ function Game({ avatarUrl, gender }) {
|
|||||||
socket.emit('economy:getInventory', { userId: profile.id });
|
socket.emit('economy:getInventory', { userId: profile.id });
|
||||||
socket.on('economy:inventory', setInventory);
|
socket.on('economy:inventory', setInventory);
|
||||||
socket.on('gameTime:update', ({ time }) => setGameTime(time));
|
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
|
// Лоадеры, учитывающиеся в прогрессе через loadingManagerRef
|
||||||
const gltfLoader = new GLTFLoader(loadingManagerRef.current || undefined);
|
const gltfLoader = new GLTFLoader(loadingManagerRef.current || undefined);
|
||||||
const animLoader = 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;
|
const delta = -e.deltaY * 0.001;
|
||||||
|
|
||||||
if (e.ctrlKey) {
|
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 = THREE.MathUtils.clamp(
|
||||||
cameraPitchOffset + delta,
|
cameraPitchOffset + delta,
|
||||||
-maxPitch,
|
-maxPitch,
|
||||||
maxPitch
|
maxPitch
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (cameraRef.current === orthoCamRef.current) {
|
if (cameraRef.current === orthoCamRef.current) {
|
||||||
zoom = THREE.MathUtils.clamp(zoom * (1 + delta), minZoom, maxZoom);
|
zoom = THREE.MathUtils.clamp(zoom * (1 + delta), minZoom, maxZoom);
|
||||||
@@ -3531,6 +3872,26 @@ function Game({ avatarUrl, gender }) {
|
|||||||
setShowInventory(v => !v);
|
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) {
|
if (!isInInteriorRef.current) {
|
||||||
destination = null;
|
destination = null;
|
||||||
@@ -3846,6 +4207,7 @@ function Game({ avatarUrl, gender }) {
|
|||||||
// Поворот влево-вправо (A/D или стрелки)
|
// Поворот влево-вправо (A/D или стрелки)
|
||||||
if (move.left) player.rotation.y += rotSpeed * delta;
|
if (move.left) player.rotation.y += rotSpeed * delta;
|
||||||
if (move.right) player.rotation.y -= rotSpeed * delta;
|
if (move.right) player.rotation.y -= rotSpeed * delta;
|
||||||
|
|
||||||
// Камера следует за вращением тела
|
// Камера следует за вращением тела
|
||||||
const headHeight = 1.6;
|
const headHeight = 1.6;
|
||||||
const camBase = new THREE.Vector3(player.position.x, player.position.y + headHeight, player.position.z);
|
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));
|
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));
|
fpCamRef.current.lookAt(fpCamRef.current.position.clone().add(lookForward));
|
||||||
|
|
||||||
// Движение с проверкой коллизий
|
// Улучшенное движение с проверкой коллизий и предотвращением застревания
|
||||||
const tryMove = (dirVec) => {
|
const tryMove = (dirVec) => {
|
||||||
const candidate = player.position.clone().addScaledVector(dirVec, speed * delta);
|
const stepDistance = speed * delta;
|
||||||
// Обновляем AABB игрока (простая капсула не используется, только коробка)
|
const candidate = player.position.clone().addScaledVector(dirVec, stepDistance);
|
||||||
const half = 0.25; // чуточку уже, чтобы не цепляться за стены
|
|
||||||
const height = 1.7; // немного ниже, чтобы не пересекать потолок
|
// Обновляем AABB игрока с меньшими размерами для предотвращения застревания
|
||||||
|
const half = 0.2; // Уменьшаем размер для лучшего прохождения
|
||||||
|
const height = 1.6; // Немного ниже для предотвращения застревания в потолке
|
||||||
const playerBox = new THREE.Box3(
|
const playerBox = new THREE.Box3(
|
||||||
new THREE.Vector3(candidate.x - half, candidate.y, candidate.z - half),
|
new THREE.Vector3(candidate.x - half, candidate.y, candidate.z - half),
|
||||||
new THREE.Vector3(candidate.x + half, candidate.y + height, candidate.z + half)
|
new THREE.Vector3(candidate.x + half, candidate.y + height, candidate.z + half)
|
||||||
);
|
);
|
||||||
// Обновляем мировые матрицы статических коллайдеров для корректных AABB
|
|
||||||
try { interiorGroupRef.current && interiorGroupRef.current.updateMatrixWorld(true); } catch (_) { }
|
|
||||||
|
|
||||||
// В интерьере учитываем только внутренние коллайдеры, без городских объектов
|
// Обновляем мировые матрицы статических коллайдеров для корректных AABB
|
||||||
|
try {
|
||||||
|
interiorGroupRef.current && interiorGroupRef.current.updateMatrixWorld(true);
|
||||||
|
} catch (_) { }
|
||||||
|
|
||||||
|
// В интерьере учитываем только внутренние коллайдеры
|
||||||
const blockingMeshes = Array.isArray(interiorCollidersRef.current)
|
const blockingMeshes = Array.isArray(interiorCollidersRef.current)
|
||||||
? interiorCollidersRef.current
|
? interiorCollidersRef.current
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
let hits = false;
|
let hits = false;
|
||||||
|
let closestDistance = Infinity;
|
||||||
|
let slideDirection = null;
|
||||||
|
|
||||||
for (const mesh of blockingMeshes) {
|
for (const mesh of blockingMeshes) {
|
||||||
if (!mesh) continue;
|
if (!mesh) continue;
|
||||||
const box = new THREE.Box3().setFromObject(mesh);
|
const box = new THREE.Box3().setFromObject(mesh);
|
||||||
// небольшой зазор, чтобы скользить вдоль стен
|
const expanded = box.clone().expandByScalar(0.05); // Увеличиваем зазор
|
||||||
const expanded = box.clone().expandByScalar(0.01);
|
|
||||||
if (expanded.intersectsBox(playerBox)) { hits = true; break; }
|
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) {
|
if (!hits) {
|
||||||
|
// Свободное движение
|
||||||
player.position.copy(candidate);
|
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 forward = new THREE.Vector3(0, 0, -1).applyQuaternion(player.quaternion);
|
||||||
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(player.quaternion);
|
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(player.quaternion);
|
||||||
|
|
||||||
|
// Применяем движение с плавностью
|
||||||
if (move.forward) tryMove(forward);
|
if (move.forward) tryMove(forward);
|
||||||
if (move.backward) tryMove(forward.clone().multiplyScalar(-1));
|
if (move.backward) tryMove(forward.clone().multiplyScalar(-1));
|
||||||
if (move.strafeLeft) tryMove(right.clone().multiplyScalar(-1));
|
if (move.strafeLeft) tryMove(right.clone().multiplyScalar(-1));
|
||||||
if (move.strafeRight) tryMove(right);
|
if (move.strafeRight) tryMove(right);
|
||||||
|
|
||||||
// Отправляем позицию внутри интерьера, чтобы нас видели другие внутри
|
// Отправляем позицию внутри интерьера
|
||||||
if (socketRef.current) {
|
if (socketRef.current) {
|
||||||
socketRef.current.emit('playerMovement', { x: player.position.x, y: player.position.y, z: player.position.z });
|
socketRef.current.emit('playerMovement', { x: player.position.x, y: player.position.y, z: player.position.z });
|
||||||
}
|
}
|
||||||
@@ -4029,8 +4442,37 @@ function Game({ avatarUrl, gender }) {
|
|||||||
}
|
}
|
||||||
window.addEventListener('resize', onWindowResize, false);
|
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 () => {
|
return () => {
|
||||||
clearInterval(balanceInterval);
|
clearInterval(balanceInterval);
|
||||||
|
clearInterval(statusInterval);
|
||||||
|
|
||||||
|
// Очищаем overlay загрузки
|
||||||
|
if (overlayEl) {
|
||||||
|
removeLoadingOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем все таймеры overlay
|
||||||
|
if (overlayTimeoutRef.current) {
|
||||||
|
clearTimeout(overlayTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
// Очищаем таймеры throttling
|
// Очищаем таймеры throttling
|
||||||
if (wheelTimeout) {
|
if (wheelTimeout) {
|
||||||
@@ -5632,13 +6074,68 @@ function Game({ avatarUrl, gender }) {
|
|||||||
)}
|
)}
|
||||||
{telegramContacts.map((user) => (
|
{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 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 }}>
|
<div style={{ width: 28, height: 28, borderRadius: 14, background: '#e5e7eb', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12 }}>
|
||||||
{user.firstName?.[0]}{user.lastName?.[0]}
|
{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>
|
||||||
<div style={{ overflow: 'hidden' }}>
|
<div style={{ overflow: 'hidden', flex: 1 }}>
|
||||||
<div style={{ whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }}>{user.firstName} {user.lastName}</div>
|
<div style={{
|
||||||
<div style={{ fontSize: 12, color: '#6b7280' }}>Онлайн</div>
|
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>
|
</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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,3 +34,43 @@ export async function registerStep1(data) {
|
|||||||
return r.json();
|
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