Tg with Notifications, bags fixxed

This commit is contained in:
2025-09-04 13:39:53 +03:00
parent f77d19975e
commit 51995c3695
13 changed files with 1775 additions and 34 deletions

134
GAME_IMPROVEMENTS_README.md Normal file
View 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. **Оптимизация**: Дальнейшее улучшение производительности

View 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
View 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
View 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
View 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 остановлен пользователем")

View File

@@ -1 +1 @@
{"time":"2025-04-23T00:30:00.608Z","lastReal":1756243565634}
{"time":"2025-06-16T00:46:41.600Z","lastReal":1756826890758}

152
server.js
View File

@@ -33,7 +33,7 @@ catch (e) {
console.error('Ошибка при импорте db1 - virtual_World:', e);
throw e;
}
/*
try {
new_quest_Base = require('./db2');
console.log('db2 - new_quest_Base - успешно импортирован');
@@ -42,7 +42,7 @@ catch (e) {
console.error('Ошибка при импорте db2 - new_quest_Base: ', e);
throw e;
}
*/
try {
db = require('./db');
console.log('db успешно импортирован');
@@ -99,7 +99,8 @@ const io = require('socket.io')(http, {
}
});
let onlineUsers = {};
let onlineUsers = new Map();
let lastSeenTimes = new Map(); // Добавляем отслеживание времени последнего онлайн
const organizationsRouter = require('./server/organizations')(io, onlineUsers);
app.use('/api/organizations', organizationsRouter);
@@ -120,7 +121,19 @@ io.use((socket, next) => {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
socket.userId = payload.id;
onlineUsers[socket.userId] = socket.id; // Добавить пользователя в онлайн
onlineUsers.set(socket.userId, socket.id); // Добавить пользователя в онлайн
// Обновляем время последнего онлайн
lastSeenTimes.set(socket.userId, new Date());
console.log(`Пользователь ${socket.userId} стал онлайн, время: ${lastSeenTimes.get(socket.userId)}`);
// Уведомляем всех клиентов о том, что пользователь стал онлайн
socket.broadcast.emit('userStatusChanged', {
userId: socket.userId,
isOnline: true
});
console.log(`Отправлено событие userStatusChanged для пользователя ${socket.userId} (онлайн)`);
next();
} catch (err) {
next(new Error('Invalid token'));
@@ -315,7 +328,7 @@ io.on('connection', socket => {
);
const newMessage = result.rows[0];
const receiverSocketId = onlineUsers[recvId];
const receiverSocketId = onlineUsers.get(recvId);
// Отправка получателю
if (receiverSocketId) {
@@ -404,7 +417,18 @@ io.on('connection', socket => {
// --- Отключение ---
socket.on('disconnect', async () => {
delete onlineUsers[socket.userId];
// Обновляем время последнего онлайн
lastSeenTimes.set(socket.userId, new Date());
console.log(`Пользователь ${socket.userId} стал офлайн, время: ${lastSeenTimes.get(socket.userId)}`);
// Уведомляем всех клиентов о том, что пользователь стал офлайн
socket.broadcast.emit('userStatusChanged', {
userId: socket.userId,
isOnline: false
});
console.log(`Отправлено событие userStatusChanged для пользователя ${socket.userId} (офлайн)`);
onlineUsers.delete(socket.userId);
const cityId = socket.cityId;
const player = playersByCity[cityId]?.[socket.id];
if (player) {
@@ -439,6 +463,82 @@ app.get('/api/users', authenticate, async (req, res) => {
}
});
// API endpoint для получения статуса пользователей для Telegram
app.get('/api/users/status', authenticate, async (req, res) => {
try {
console.log(`Запрос статуса пользователей от пользователя ${req.user.id}`);
const { rows } = await db.query(`
SELECT id, first_name AS "firstName", last_name AS "lastName", avatar_url AS "avatarURL"
FROM users
WHERE id != $1
`, [req.user.id]);
// Добавляем статус online и время последнего онлайн для каждого пользователя
const usersWithStatus = rows.map(user => ({
...user,
isOnline: onlineUsers.has(user.id),
lastSeen: lastSeenTimes.get(user.id) || null
}));
console.log(`Возвращено ${usersWithStatus.length} пользователей с статусом`);
console.log('Онлайн пользователи:', Array.from(onlineUsers.keys()));
res.json(usersWithStatus);
} catch (e) {
console.error('Ошибка получения статуса пользователей', e);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// API endpoint to get user information by ID
app.get('/api/users/:userId', authenticate, async (req, res) => {
const userId = parseInt(req.params.userId, 10);
try {
const { rows } = await db.query(`
SELECT id, first_name AS "firstName", last_name AS "lastName", avatar_url AS "avatarURL"
FROM users
WHERE id = $1
`, [userId]);
if (rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
const user = rows[0];
const isOnline = onlineUsers.has(user.id);
const lastSeen = lastSeenTimes.get(user.id) || new Date();
res.json({
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
avatarURL: user.avatarURL,
isOnline: isOnline,
lastSeen: lastSeen
});
} catch (e) {
console.error('Ошибка получения информации о пользователе по ID', e);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// API endpoint to get unread message count for a specific contact
app.get('/api/messages-read/:contactId', authenticate, async (req, res) => {
const userId = req.user.id;
const contactId = parseInt(req.params.contactId, 10);
try {
const { rows } = await db.query(`
SELECT COUNT(*) as unread_count
FROM messages
WHERE sender_id = $1 AND recipient_id = $2 AND is_read = false
`, [contactId, userId]);
res.json({ unreadCount: parseInt(rows[0].unread_count) });
} catch (e) {
console.error('Ошибка получения количества непрочитанных сообщений:', e);
res.status(500).json({ error: 'Ошибка сервера' });
}
});
// Новый маршрут для получения сообщений с конкретным контактом
app.get('/api/messages/:contactId', authenticate, async (req, res) => {
const userId = req.user.id;
@@ -595,7 +695,7 @@ app.post('/api/messages/send', authenticate, async (req, res) => {
);
const newMessage = result.rows[0];
const receiverSocketId = onlineUsers[recvId];
const receiverSocketId = onlineUsers.get(recvId);
if (receiverSocketId) {
io.to(receiverSocketId).emit('newMessage', {
id: newMessage.id,
@@ -1531,10 +1631,46 @@ app.use((req, res) => {
});
const PORT = process.env.PORT || 4000;
http.listen(PORT, () => {
const server = http.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
// Обработка сигналов для graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully...');
gracefulShutdown();
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully...');
gracefulShutdown();
});
function gracefulShutdown() {
console.log('Уведомляем всех клиентов о перезагрузке сервера...');
// Отправляем уведомление всем подключенным клиентам
io.emit('serverRestart', {
message: 'Сервер будет перезагружен через 5 секунд. Пожалуйста, сохраните прогресс.',
restartIn: 5000
});
// Даем время клиентам получить уведомление
setTimeout(() => {
console.log('Закрываем сервер...');
server.close(() => {
console.log('HTTP server closed');
process.exit(0);
});
// Принудительно закрываем через 10 секунд
setTimeout(() => {
console.error('Could not close connections in time, forcefully shutting down');
process.exit(1);
}, 10000);
}, 5000);
}
// Логирование всех маршрутов и middleware
['get', 'post', 'put', 'delete', 'use'].forEach(method => {
const orig = app[method];

View File

@@ -169,7 +169,7 @@ module.exports = function(io, onlineUsers) {
let thirst = parseFloat(rows[0].thirst ?? 100);
if (balance < price) {
const sock = onlineUsers[req.user.id];
const sock = onlineUsers.get(req.user.id);
if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вам недостаточно средств для покупки ${itemDef.name}` });
return res.status(400).json({ error: 'insufficient funds' });
}
@@ -196,7 +196,7 @@ module.exports = function(io, onlineUsers) {
satiety = Math.min(100, satiety + parseFloat(itemDef.hunger_gain));
thirst = Math.min(100, thirst + parseFloat(itemDef.thirst_gain));
const sock = onlineUsers[req.user.id];
const sock = onlineUsers.get(req.user.id);
if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вы купили ${itemDef.name}` });
res.json({ success: true, balance, satiety, thirst });

View File

@@ -15,6 +15,8 @@ import Inventory from './components/Inventory';
import OrgControlPanel from './components/OrgControlPanel';
import DoubleTapWrapper from './pages/DoubleTapWrapper';
import WaveformPlayer from './pages/WaveformPlayer';
import { getUsersStatus, loadUserInfo } from './api/auth.js';
function Game({ avatarUrl, gender }) {
// 1) реф для хранилища сцены
@@ -31,6 +33,7 @@ function Game({ avatarUrl, gender }) {
const cleanupTimerRef = useRef(null);
// Глобальный менеджер прогресса загрузки (используем в GLTFLoader)
const loadingManagerRef = useRef(null);
const overlayTimeoutRef = useRef(null);
// Кликабельные объекты внутри интерьера
const interiorInteractablesRef = useRef([]);
const npcMeshesRef = useRef([]);
@@ -1341,14 +1344,19 @@ function Game({ avatarUrl, gender }) {
const token = localStorage.getItem('token');
try {
setTgError(null);
const res = await fetch('/api/users', {
const res = await fetch('/api/users/status', {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
cache: 'no-cache'
});
if (res.ok) {
const data = await res.json();
setTelegramContacts(data);
// Добавляем счетчик непрочитанных сообщений для каждого пользователя
const dataWithUnread = data.map(user => ({
...user,
unreadCount: 0
}));
setTelegramContacts(dataWithUnread);
} else {
const txt = await res.text().catch(() => '');
console.error('Ошибка загрузки контактов Telegram', res.status, txt);
@@ -1367,6 +1375,223 @@ function Game({ avatarUrl, gender }) {
//const [readmes, setReadmes] = useState('false');
const [userProfile, setUserProfile] = useState(null);
// Функция показа уведомлений о сообщениях
const showMessageNotification = async (senderId, messageText) => {
try {
// Сначала пытаемся найти отправителя в контактах
let senderName = 'Неизвестный';
const contact = telegramContacts.find(c => c.id === senderId);
if (contact) {
senderName = contact.firstName || contact.lastName || 'Неизвестный';
} else {
// Если не найден в контактах, загружаем информацию о пользователе
try {
const userInfo = await loadUserInfo(senderId, localStorage.getItem('token'));
senderName = userInfo.firstName || userInfo.lastName || 'Неизвестный';
} catch (error) {
console.error('Ошибка загрузки информации о пользователе:', error);
senderName = 'Неизвестный';
}
}
// Создаем уведомление
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
border-radius: 10px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
z-index: 10000;
font-family: 'Arial', sans-serif;
font-size: 14px;
max-width: 300px;
transform: translateX(400px);
transition: transform 0.3s ease-out;
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
`;
notification.innerHTML = `
<div style="font-weight: bold; margin-bottom: 5px; color: #ffd700;">${senderName}</div>
<div style="opacity: 0.9;">${messageText.length > 50 ? messageText.substring(0, 50) + '...' : messageText}</div>
`;
document.body.appendChild(notification);
// Анимация появления
setTimeout(() => {
notification.style.transform = 'translateX(0)';
}, 100);
// Автоматическое скрытие через 5 секунд
setTimeout(() => {
notification.style.transform = 'translateX(400px)';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 5000);
} catch (error) {
console.error('Ошибка показа уведомления:', error);
}
};
// Функция для обновления счетчика непрочитанных сообщений
const updateUnreadCount = async (senderId) => {
try {
const token = localStorage.getItem('token');
if (!token) return;
// Получаем количество непрочитанных сообщений
const response = await fetch(`/api/messages-read/${senderId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
const unreadCount = data.unreadCount || 0;
// Обновляем счетчик в контактах
setTelegramContacts(prev =>
prev.map(contact =>
contact.id === senderId
? { ...contact, unreadCount: unreadCount }
: contact
)
);
}
} catch (error) {
console.error('Ошибка обновления счетчика непрочитанных сообщений:', error);
}
};
// Функция показа подсказки об управлении камерой
function showCameraControlsHint() {
const hint = document.createElement('div');
hint.style.cssText = `
position: fixed;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px 20px;
border-radius: 10px;
font-family: system-ui, Arial, sans-serif;
font-size: 14px;
z-index: 9999;
max-width: 300px;
animation: fadeIn 0.5s ease-in;
`;
hint.innerHTML = `
<div style="font-weight: 600; margin-bottom: 8px;">🎮 Управление камерой:</div>
<div style="margin-bottom: 5px;">• <strong>Ctrl + колесо</strong> = вертикальный поворот</div>
<div style="margin-bottom: 5px;">• <strong>Shift + Ctrl + колесо</strong> = горизонтальный поворот</div>
<div style="font-size: 12px; opacity: 0.8;">Подсказка исчезнет через 10 секунд</div>
`;
// Добавляем CSS анимацию
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
`;
document.head.appendChild(style);
document.body.appendChild(hint);
// Автоматически скрываем через 10 секунд
setTimeout(() => {
hint.style.animation = 'fadeOut 0.5s ease-out';
hint.style.opacity = '0';
setTimeout(() => hint.remove(), 500);
}, 10000);
// Добавляем CSS для fadeOut
if (!document.querySelector('#hint-styles')) {
const fadeOutStyle = document.createElement('style');
fadeOutStyle.id = 'hint-styles';
fadeOutStyle.textContent = `
@keyframes fadeOut {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(20px); }
}
`;
document.head.appendChild(fadeOutStyle);
}
}
// Функция показа уведомления о перезагрузке сервера
function showServerRestartNotification(message, restartIn) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #dc2626;
color: white;
padding: 20px 30px;
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
z-index: 10001;
max-width: 400px;
font-family: system-ui, Arial, sans-serif;
text-align: center;
animation: serverRestartPulse 2s infinite;
`;
notification.innerHTML = `
<div style="font-size: 18px; font-weight: 600; margin-bottom: 10px;">⚠️ Перезагрузка сервера</div>
<div style="font-size: 14px; margin-bottom: 15px;">${message}</div>
<div style="font-size: 12px; opacity: 0.8;">Перезагрузка через: <span id="restart-countdown">${Math.ceil(restartIn/1000)}</span> сек</div>
`;
// Добавляем CSS анимацию
const style = document.createElement('style');
style.textContent = `
@keyframes serverRestartPulse {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.05); }
}
`;
document.head.appendChild(style);
document.body.appendChild(notification);
// Обновляем счетчик
const countdownEl = notification.querySelector('#restart-countdown');
const startTime = Date.now();
const countdownInterval = setInterval(() => {
const remaining = Math.max(0, restartIn - (Date.now() - startTime));
if (countdownEl) {
countdownEl.textContent = Math.ceil(remaining/1000);
}
if (remaining <= 0) {
clearInterval(countdownInterval);
notification.remove();
}
}, 100);
// Автоматически скрываем через время перезагрузки
setTimeout(() => {
clearInterval(countdownInterval);
notification.remove();
}, restartIn);
}
// Функция загрузки сообщений
async function loadMessages(contactId) {
if (!contactId) return;
@@ -1910,11 +2135,18 @@ function Game({ avatarUrl, gender }) {
}
// ─────────────────────────────────────────────
// Красивый загрузочный оверлей + LoadingManager
// Улучшенный загрузочный оверлей + LoadingManager
// ─────────────────────────────────────────────
let overlayEl = null, barEl = null, textEl = null;
let isInitialLoad = true; // Флаг для определения начальной загрузки
function createLoadingOverlay() {
if (overlayEl) return;
// Дополнительная проверка - не показываем overlay для очень маленьких загрузок
if (!isInitialLoad && loadingManagerRef.current && loadingManagerRef.current.itemStart) {
const currentTotal = loadingManagerRef.current.itemStart.length || 0;
if (currentTotal <= 3) return; // Не показываем для загрузки 3 или меньше ресурсов
}
overlayEl = document.createElement('div');
Object.assign(overlayEl.style, {
position: 'fixed', inset: '0', zIndex: 2000,
@@ -1951,6 +2183,7 @@ function Game({ avatarUrl, gender }) {
overlayEl.appendChild(pct);
document.body.appendChild(overlayEl);
}
function updateLoadingOverlay(percent, text) {
if (!overlayEl) return;
const p = Math.max(0, Math.min(100, Math.round(percent || 0)));
@@ -1959,8 +2192,16 @@ function Game({ avatarUrl, gender }) {
if (pct) pct.textContent = p + '%';
if (text && textEl) textEl.textContent = text;
}
function removeLoadingOverlay() {
if (!overlayEl) return;
// Очищаем все таймеры overlay
if (overlayTimeoutRef.current) {
clearTimeout(overlayTimeoutRef.current);
overlayTimeoutRef.current = null;
}
overlayEl.style.transition = 'opacity .2s ease';
overlayEl.style.opacity = '0';
setTimeout(() => {
@@ -1968,19 +2209,62 @@ function Game({ avatarUrl, gender }) {
overlayEl = barEl = textEl = null;
}, 220);
}
// Общий менеджер загрузки (для GLTF/Texture и т.п.)
const loadingManager = new THREE.LoadingManager();
loadingManagerRef.current = loadingManager;
loadingManager.onStart = (_url, loaded, total) => {
createLoadingOverlay();
updateLoadingOverlay(total ? (loaded / total) * 100 : 5, 'Загрузка ресурсов...');
console.log(`LoadingManager.onStart: isInitialLoad=${isInitialLoad}, total=${total}, url=${_url}`);
// Показываем оверлей только при начальной загрузке или при загрузке большого количества ресурсов
if (isInitialLoad || total > 10) {
console.log('Показываем overlay для загрузки');
createLoadingOverlay();
updateLoadingOverlay(total ? (loaded / total) * 100 : 5, 'Загрузка ресурсов...');
} else {
console.log('Не показываем overlay - небольшая загрузка');
}
};
loadingManager.onProgress = (_url, loaded, total) => {
updateLoadingOverlay(total ? (loaded / total) * 100 : 50);
if (overlayEl && (isInitialLoad || total > 10)) {
updateLoadingOverlay(total ? (loaded / total) * 100 : 50);
}
};
loadingManager.onLoad = () => {
updateLoadingOverlay(100, 'Инициализация сцены...');
setTimeout(removeLoadingOverlay, 150);
console.log(`LoadingManager.onLoad: isInitialLoad=${isInitialLoad}, overlayEl=${!!overlayEl}`);
if (overlayEl) {
// Показываем "Инициализация сцены" только для начальной загрузки
if (isInitialLoad) {
console.log('Показываем "Инициализация сцены" для начальной загрузки');
updateLoadingOverlay(100, 'Инициализация сцены...');
setTimeout(removeLoadingOverlay, 150);
} else {
// Для небольших загрузок просто скрываем overlay
console.log('Скрываем overlay для небольшой загрузки');
removeLoadingOverlay();
}
}
isInitialLoad = false; // После первой загрузки сбрасываем флаг
// Дополнительная защита - принудительно скрываем overlay через 3 секунды
if (overlayEl) {
overlayTimeoutRef.current = setTimeout(() => {
if (overlayEl) {
console.log('Принудительно скрываем overlay по таймауту');
removeLoadingOverlay();
}
}, 3000);
}
// Глобальная защита - принудительно скрываем overlay через 5 секунд после начала игры
overlayTimeoutRef.current = setTimeout(() => {
if (overlayEl && !isInitialLoad) {
console.log('Глобальная защита: принудительно скрываем overlay');
removeLoadingOverlay();
}
}, 5000);
};
@@ -1989,7 +2273,7 @@ function Game({ avatarUrl, gender }) {
const baseOffset = new THREE.Vector3(-200, 150, -200);
const planarDist = Math.hypot(baseOffset.x, baseOffset.z);
const radius = Math.hypot(planarDist, baseOffset.y);
const baseAzimuth = Math.atan2(baseOffset.z, baseOffset.x);
let baseAzimuth = Math.atan2(baseOffset.z, baseOffset.x);
const basePolar = Math.atan2(baseOffset.y, planarDist);
let cameraPitchOffset = 0;
@@ -2068,6 +2352,19 @@ function Game({ avatarUrl, gender }) {
const p = JSON.parse(sessionStorage.getItem('user_profile') || '{}');
if (p?.id) socket.emit('economy:getBalance', { userId: p.id });
}, 3000);
// Периодическое обновление статуса пользователей для Telegram
const statusInterval = setInterval(() => {
if (activeApp === "Telegram" && telegramContacts.length > 0) {
loadTelegramContacts();
// Обновляем счетчики непрочитанных сообщений для всех контактов
telegramContacts.forEach(contact => {
if (contact.id !== profile.id) {
updateUnreadCount(contact.id);
}
});
}
}, 30000); // Обновляем каждые 30 секунд
socket.on('economy:balanceChanged', ({ userId, newBalance }) => {
if (userId === profile.id) {
setBalance(newBalance);
@@ -2078,6 +2375,38 @@ function Game({ avatarUrl, gender }) {
socket.emit('economy:getInventory', { userId: profile.id });
socket.on('economy:inventory', setInventory);
socket.on('gameTime:update', ({ time }) => setGameTime(time));
// Обработчик изменения статуса пользователей для Telegram
socket.on('userStatusChanged', ({ userId, isOnline }) => {
console.log('Статус пользователя изменился:', { userId, isOnline });
setTelegramContacts(prev => prev.map(user =>
user.id === userId ? { ...user, isOnline } : user
));
});
// Обработчик новых сообщений для уведомлений
socket.on('newMessage', ({ id, text, senderId, timestamp, isRead }) => {
console.log('Новое сообщение:', { id, text, senderId, timestamp, isRead });
// Показываем уведомление только если Telegram не открыт
if (activeApp !== "Telegram") {
showMessageNotification(senderId, text);
}
// Обновляем счетчик непрочитанных сообщений
updateUnreadCount(senderId);
// Обновляем список сообщений если открыт чат с этим пользователем
if (activeChat && activeChat.id === senderId) {
loadMessages(senderId);
}
});
// Обработчик перезагрузки сервера
socket.on('serverRestart', ({ message, restartIn }) => {
console.log('Сервер будет перезагружен:', { message, restartIn });
showServerRestartNotification(message, restartIn);
});
// Лоадеры, учитывающиеся в прогрессе через loadingManagerRef
const gltfLoader = new GLTFLoader(loadingManagerRef.current || undefined);
const animLoader = new GLTFLoader(loadingManagerRef.current || undefined);
@@ -2618,11 +2947,23 @@ function Game({ avatarUrl, gender }) {
const delta = -e.deltaY * 0.001;
if (e.ctrlKey) {
// При нажатом Ctrl управляем и вертикальным, и горизонтальным углом камеры
if (e.shiftKey) {
// Shift + Ctrl + колесо = горизонтальный поворот (влево-вправо)
const horizontalDelta = delta * 2; // Увеличиваем чувствительность
baseAzimuth = THREE.MathUtils.clamp(
baseAzimuth + horizontalDelta,
-Math.PI / 2, // -90 градусов
Math.PI / 2 // +90 градусов
);
} else {
// Ctrl + колесо = вертикальный поворот (вверх-вниз)
cameraPitchOffset = THREE.MathUtils.clamp(
cameraPitchOffset + delta,
-maxPitch,
maxPitch
);
}
} else {
if (cameraRef.current === orthoCamRef.current) {
zoom = THREE.MathUtils.clamp(zoom * (1 + delta), minZoom, maxZoom);
@@ -3531,6 +3872,26 @@ function Game({ avatarUrl, gender }) {
setShowInventory(v => !v);
}
// Ctrl + Arrow keys for camera control
if (event.ctrlKey) {
const key = event.key.toLowerCase();
if (key === 'arrowleft') {
const horizontalDelta = -0.1; // Поворот влево
baseAzimuth = THREE.MathUtils.clamp(
baseAzimuth + horizontalDelta,
-Math.PI / 2, // -90 градусов
Math.PI / 2 // +90 градусов
);
} else if (key === 'arrowright') {
const horizontalDelta = 0.1; // Поворот вправо
baseAzimuth = THREE.MathUtils.clamp(
baseAzimuth + horizontalDelta,
-Math.PI / 2, // -90 градусов
Math.PI / 2 // +90 градусов
);
}
}
// Сбрасываем назначение только если не в интерьере
if (!isInInteriorRef.current) {
destination = null;
@@ -3846,6 +4207,7 @@ function Game({ avatarUrl, gender }) {
// Поворот влево-вправо (A/D или стрелки)
if (move.left) player.rotation.y += rotSpeed * delta;
if (move.right) player.rotation.y -= rotSpeed * delta;
// Камера следует за вращением тела
const headHeight = 1.6;
const camBase = new THREE.Vector3(player.position.x, player.position.y + headHeight, player.position.z);
@@ -3854,45 +4216,96 @@ function Game({ avatarUrl, gender }) {
const lookForward = new THREE.Vector3(0, 0, -1).applyEuler(new THREE.Euler(0, player.rotation.y, 0));
fpCamRef.current.lookAt(fpCamRef.current.position.clone().add(lookForward));
// Движение с проверкой коллизий
// Улучшенное движение с проверкой коллизий и предотвращением застревания
const tryMove = (dirVec) => {
const candidate = player.position.clone().addScaledVector(dirVec, speed * delta);
// Обновляем AABB игрока (простая капсула не используется, только коробка)
const half = 0.25; // чуточку уже, чтобы не цепляться за стены
const height = 1.7; // немного ниже, чтобы не пересекать потолок
const stepDistance = speed * delta;
const candidate = player.position.clone().addScaledVector(dirVec, stepDistance);
// Обновляем AABB игрока с меньшими размерами для предотвращения застревания
const half = 0.2; // Уменьшаем размер для лучшего прохождения
const height = 1.6; // Немного ниже для предотвращения застревания в потолке
const playerBox = new THREE.Box3(
new THREE.Vector3(candidate.x - half, candidate.y, candidate.z - half),
new THREE.Vector3(candidate.x + half, candidate.y + height, candidate.z + half)
);
// Обновляем мировые матрицы статических коллайдеров для корректных AABB
try { interiorGroupRef.current && interiorGroupRef.current.updateMatrixWorld(true); } catch (_) { }
try {
interiorGroupRef.current && interiorGroupRef.current.updateMatrixWorld(true);
} catch (_) { }
// В интерьере учитываем только внутренние коллайдеры, без городских объектов
// В интерьере учитываем только внутренние коллайдеры
const blockingMeshes = Array.isArray(interiorCollidersRef.current)
? interiorCollidersRef.current
: [];
let hits = false;
let closestDistance = Infinity;
let slideDirection = null;
for (const mesh of blockingMeshes) {
if (!mesh) continue;
const box = new THREE.Box3().setFromObject(mesh);
// небольшой зазор, чтобы скользить вдоль стен
const expanded = box.clone().expandByScalar(0.01);
if (expanded.intersectsBox(playerBox)) { hits = true; break; }
const expanded = box.clone().expandByScalar(0.05); // Увеличиваем зазор
if (expanded.intersectsBox(playerBox)) {
hits = true;
// Вычисляем направление скольжения вдоль стены
const center = box.getCenter(new THREE.Vector3());
const toPlayer = player.position.clone().sub(center);
const distance = toPlayer.length();
if (distance < closestDistance) {
closestDistance = distance;
// Нормализуем и создаем направление скольжения
toPlayer.normalize();
slideDirection = toPlayer;
}
}
}
if (!hits) {
// Свободное движение
player.position.copy(candidate);
} else if (slideDirection) {
// Скольжение вдоль стены
const slideDistance = stepDistance * 0.7; // Уменьшаем дистанцию скольжения
const slidePos = player.position.clone().addScaledVector(slideDirection, slideDistance);
// Проверяем, можно ли двигаться в направлении скольжения
const slideBox = new THREE.Box3(
new THREE.Vector3(slidePos.x - half, slidePos.y, slidePos.z - half),
new THREE.Vector3(slidePos.x + half, slidePos.y + height, slidePos.z + half)
);
let canSlide = true;
for (const mesh of blockingMeshes) {
if (!mesh) continue;
const box = new THREE.Box3().setFromObject(mesh);
const expanded = box.clone().expandByScalar(0.05);
if (expanded.intersectsBox(slideBox)) {
canSlide = false;
break;
}
}
if (canSlide) {
player.position.copy(slidePos);
}
}
};
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(player.quaternion);
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(player.quaternion);
// Применяем движение с плавностью
if (move.forward) tryMove(forward);
if (move.backward) tryMove(forward.clone().multiplyScalar(-1));
if (move.strafeLeft) tryMove(right.clone().multiplyScalar(-1));
if (move.strafeRight) tryMove(right);
// Отправляем позицию внутри интерьера, чтобы нас видели другие внутри
// Отправляем позицию внутри интерьера
if (socketRef.current) {
socketRef.current.emit('playerMovement', { x: player.position.x, y: player.position.y, z: player.position.z });
}
@@ -4028,9 +4441,38 @@ function Game({ avatarUrl, gender }) {
}
}
window.addEventListener('resize', onWindowResize, false);
// Отключаем браузерное масштабирование
document.addEventListener('wheel', (e) => {
if (e.ctrlKey) {
e.preventDefault();
}
}, { passive: false });
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && (e.key === '+' || e.key === '-' || e.key === '=')) {
e.preventDefault();
}
});
// Показываем подсказку об управлении камерой
setTimeout(() => {
showCameraControlsHint();
}, 3000);
return () => {
clearInterval(balanceInterval);
clearInterval(statusInterval);
// Очищаем overlay загрузки
if (overlayEl) {
removeLoadingOverlay();
}
// Очищаем все таймеры overlay
if (overlayTimeoutRef.current) {
clearTimeout(overlayTimeoutRef.current);
}
// Очищаем таймеры throttling
if (wheelTimeout) {
@@ -5632,13 +6074,68 @@ function Game({ avatarUrl, gender }) {
)}
{telegramContacts.map((user) => (
<div key={user.id} onClick={() => setActiveChat(user)} style={{ padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', color: '#111' }}>
<div style={{ position: 'relative' }}>
<div style={{ width: 28, height: 28, borderRadius: 14, background: '#e5e7eb', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12 }}>
{user.firstName?.[0]}{user.lastName?.[0]}
</div>
{user.isOnline && (
<div style={{
position: 'absolute',
bottom: 0,
right: 0,
width: 10,
height: 10,
borderRadius: 5,
background: '#10b981',
border: '2px solid #fff',
boxShadow: '0 0 0 1px #e5e7eb'
}} />
)}
</div>
<div style={{ overflow: 'hidden' }}>
<div style={{ whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }}>{user.firstName} {user.lastName}</div>
<div style={{ fontSize: 12, color: '#6b7280' }}>Онлайн</div>
<div style={{ overflow: 'hidden', flex: 1 }}>
<div style={{
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
fontWeight: user.unreadCount > 0 ? 'bold' : 'normal'
}}>
{`${user.firstName || ''} ${user.lastName || ''}`}
</div>
<div style={{
fontSize: 12,
color: user.isOnline ? '#10b981' : '#6b7280',
fontWeight: user.isOnline ? 600 : 400
}}>
{user.isOnline ? 'Онлайн' : (
user.lastSeen ?
`Был(а) ${new Date(user.lastSeen).toLocaleString('ru-RU', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}` :
'Офлайн'
)}
</div>
</div>
{/* Счетчик непрочитанных сообщений */}
{user.unreadCount > 0 && (
<div style={{
background: '#ef4444',
color: 'white',
borderRadius: '50%',
minWidth: '20px',
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: 'bold',
padding: '2px'
}}>
{user.unreadCount > 99 ? '99+' : user.unreadCount}
</div>
)}
</div>
))}
</div>

View File

@@ -33,4 +33,44 @@ export async function registerStep1(data) {
});
return r.json();
}
export const getUsersStatus = async (token) => {
try {
const response = await fetch('/api/users/status', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch users status');
}
return await response.json();
} catch (error) {
console.error('Error fetching users status:', error);
throw error;
}
};
export const loadUserInfo = async (userId, token) => {
try {
const response = await fetch(`/api/users/${userId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch user info');
}
return await response.json();
} catch (error) {
console.error('Error fetching user info:', error);
throw error;
}
};

217
test_telegram_fix.html Normal file
View 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>

View 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
View 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>