From 475e2cf4cdedaab004cdecefd26d74413d34078e Mon Sep 17 00:00:00 2001 From: Oronemu Date: Sun, 4 Jan 2026 02:47:38 +0700 Subject: [PATCH] Add notification settings --- Makefile | 6 +- .../versions/022_add_notification_settings.py | 45 ++++++ backend/app/api/v1/telegram.py | 58 ++++++++ backend/app/api/v1/users.py | 29 +++- backend/app/models/user.py | 5 + backend/app/schemas/__init__.py | 4 + backend/app/schemas/user.py | 21 +++ backend/app/services/telegram_notifier.py | 80 +++++++++-- bot/handlers/marathons.py | 71 +++++++-- bot/keyboards/inline.py | 42 ++++++ bot/services/api_client.py | 16 +++ frontend/src/api/users.ts | 14 +- frontend/src/pages/ProfilePage.tsx | 135 +++++++++++++++++- frontend/src/types/index.ts | 17 +++ 14 files changed, 517 insertions(+), 26 deletions(-) create mode 100644 backend/alembic/versions/022_add_notification_settings.py diff --git a/Makefile b/Makefile index 8cc670d..a179e23 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help dev up down build build-no-cache logs restart clean migrate shell db-shell frontend-shell backend-shell lint test +.PHONY: help dev up down build build-no-cache logs logs-bot restart clean migrate shell db-shell frontend-shell backend-shell lint test DC = sudo docker-compose @@ -14,6 +14,7 @@ help: @echo " make logs - Show logs (all services)" @echo " make logs-b - Show backend logs" @echo " make logs-f - Show frontend logs" + @echo " make logs-bot - Show Telegram bot logs" @echo "" @echo " Build:" @echo " make build - Build all containers (with cache)" @@ -63,6 +64,9 @@ logs-b: logs-f: $(DC) logs -f frontend +logs-bot: + $(DC) logs -f bot + # Build build: $(DC) build diff --git a/backend/alembic/versions/022_add_notification_settings.py b/backend/alembic/versions/022_add_notification_settings.py new file mode 100644 index 0000000..118a2a1 --- /dev/null +++ b/backend/alembic/versions/022_add_notification_settings.py @@ -0,0 +1,45 @@ +"""Add notification settings to users + +Revision ID: 022_add_notification_settings +Revises: 021_add_bonus_disputes +Create Date: 2025-01-04 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect + + +# revision identifiers, used by Alembic. +revision: str = '022_add_notification_settings' +down_revision: Union[str, None] = '021_add_bonus_disputes' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def column_exists(table_name: str, column_name: str) -> bool: + bind = op.get_bind() + inspector = inspect(bind) + columns = [col['name'] for col in inspector.get_columns(table_name)] + return column_name in columns + + +def upgrade() -> None: + # Add notification settings (all enabled by default) + if not column_exists('users', 'notify_events'): + op.add_column('users', sa.Column('notify_events', sa.Boolean(), server_default='true', nullable=False)) + if not column_exists('users', 'notify_disputes'): + op.add_column('users', sa.Column('notify_disputes', sa.Boolean(), server_default='true', nullable=False)) + if not column_exists('users', 'notify_moderation'): + op.add_column('users', sa.Column('notify_moderation', sa.Boolean(), server_default='true', nullable=False)) + + +def downgrade() -> None: + if column_exists('users', 'notify_moderation'): + op.drop_column('users', 'notify_moderation') + if column_exists('users', 'notify_disputes'): + op.drop_column('users', 'notify_disputes') + if column_exists('users', 'notify_events'): + op.drop_column('users', 'notify_events') diff --git a/backend/app/api/v1/telegram.py b/backend/app/api/v1/telegram.py index 25193d1..c739ad4 100644 --- a/backend/app/api/v1/telegram.py +++ b/backend/app/api/v1/telegram.py @@ -73,6 +73,21 @@ class TelegramStatsResponse(BaseModel): best_streak: int +class TelegramNotificationSettings(BaseModel): + notify_events: bool = True + notify_disputes: bool = True + notify_moderation: bool = True + + class Config: + from_attributes = True + + +class TelegramNotificationSettingsUpdate(BaseModel): + notify_events: bool | None = None + notify_disputes: bool | None = None + notify_moderation: bool | None = None + + # Endpoints @router.post("/generate-link-token", response_model=TelegramLinkToken) async def generate_link_token(current_user: CurrentUser): @@ -391,3 +406,46 @@ async def get_user_stats(telegram_id: int, db: DbSession, _: BotSecretDep): total_points=total_points, best_streak=best_streak ) + + +@router.get("/notifications/{telegram_id}", response_model=TelegramNotificationSettings | None) +async def get_notification_settings(telegram_id: int, db: DbSession, _: BotSecretDep): + """Get user's notification settings by Telegram ID.""" + result = await db.execute( + select(User).where(User.telegram_id == telegram_id) + ) + user = result.scalar_one_or_none() + + if not user: + return None + + return TelegramNotificationSettings.model_validate(user) + + +@router.patch("/notifications/{telegram_id}", response_model=TelegramNotificationSettings | None) +async def update_notification_settings( + telegram_id: int, + data: TelegramNotificationSettingsUpdate, + db: DbSession, + _: BotSecretDep +): + """Update user's notification settings by Telegram ID.""" + result = await db.execute( + select(User).where(User.telegram_id == telegram_id) + ) + user = result.scalar_one_or_none() + + if not user: + return None + + if data.notify_events is not None: + user.notify_events = data.notify_events + if data.notify_disputes is not None: + user.notify_disputes = data.notify_disputes + if data.notify_moderation is not None: + user.notify_moderation = data.notify_moderation + + await db.commit() + await db.refresh(user) + + return TelegramNotificationSettings.model_validate(user) diff --git a/backend/app/api/v1/users.py b/backend/app/api/v1/users.py index 64186ce..60fe31d 100644 --- a/backend/app/api/v1/users.py +++ b/backend/app/api/v1/users.py @@ -9,7 +9,8 @@ from app.models.assignment import AssignmentStatus from app.models.marathon import MarathonStatus from app.schemas import ( UserPublic, UserPrivate, UserUpdate, TelegramLink, MessageResponse, - PasswordChange, UserStats, UserProfilePublic, + PasswordChange, UserStats, UserProfilePublic, NotificationSettings, + NotificationSettingsUpdate, ) from app.services.storage import storage_service @@ -189,6 +190,32 @@ async def change_password( return MessageResponse(message="Пароль успешно изменен") +@router.get("/me/notifications", response_model=NotificationSettings) +async def get_notification_settings(current_user: CurrentUser): + """Get current user's notification settings""" + return NotificationSettings.model_validate(current_user) + + +@router.patch("/me/notifications", response_model=NotificationSettings) +async def update_notification_settings( + data: NotificationSettingsUpdate, + current_user: CurrentUser, + db: DbSession, +): + """Update current user's notification settings""" + if data.notify_events is not None: + current_user.notify_events = data.notify_events + if data.notify_disputes is not None: + current_user.notify_disputes = data.notify_disputes + if data.notify_moderation is not None: + current_user.notify_moderation = data.notify_moderation + + await db.commit() + await db.refresh(current_user) + + return NotificationSettings.model_validate(current_user) + + @router.get("/me/stats", response_model=UserStats) async def get_my_stats(current_user: CurrentUser, db: DbSession): """Получить свою статистику""" diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 8339e5f..ed8ed38 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -34,6 +34,11 @@ class User(Base): banned_by_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"), nullable=True) ban_reason: Mapped[str | None] = mapped_column(String(500), nullable=True) + # Notification settings (all enabled by default) + notify_events: Mapped[bool] = mapped_column(Boolean, default=True) + notify_disputes: Mapped[bool] = mapped_column(Boolean, default=True) + notify_moderation: Mapped[bool] = mapped_column(Boolean, default=True) + # Relationships created_marathons: Mapped[list["Marathon"]] = relationship( "Marathon", diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index a034ff3..d7623c8 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -9,6 +9,8 @@ from app.schemas.user import ( PasswordChange, UserStats, UserProfilePublic, + NotificationSettings, + NotificationSettingsUpdate, ) from app.schemas.marathon import ( MarathonCreate, @@ -115,6 +117,8 @@ __all__ = [ "PasswordChange", "UserStats", "UserProfilePublic", + "NotificationSettings", + "NotificationSettingsUpdate", # Marathon "MarathonCreate", "MarathonUpdate", diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 62e866d..fb165cc 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -47,6 +47,10 @@ class UserPrivate(UserPublic): telegram_username: str | None = None telegram_first_name: str | None = None telegram_last_name: str | None = None + # Notification settings + notify_events: bool = True + notify_disputes: bool = True + notify_moderation: bool = True class TokenResponse(BaseModel): @@ -83,3 +87,20 @@ class UserProfilePublic(BaseModel): class Config: from_attributes = True + + +class NotificationSettings(BaseModel): + """Notification settings for Telegram bot""" + notify_events: bool = True + notify_disputes: bool = True + notify_moderation: bool = True + + class Config: + from_attributes = True + + +class NotificationSettingsUpdate(BaseModel): + """Update notification settings""" + notify_events: bool | None = None + notify_disputes: bool | None = None + notify_moderation: bool | None = None diff --git a/backend/app/services/telegram_notifier.py b/backend/app/services/telegram_notifier.py index 04ca4d0..7790b05 100644 --- a/backend/app/services/telegram_notifier.py +++ b/backend/app/services/telegram_notifier.py @@ -83,9 +83,15 @@ class TelegramNotifier: db: AsyncSession, marathon_id: int, message: str, - exclude_user_id: int | None = None + exclude_user_id: int | None = None, + check_setting: str | None = None ) -> int: - """Send notification to all marathon participants with linked Telegram.""" + """Send notification to all marathon participants with linked Telegram. + + Args: + check_setting: If provided, only send to users with this setting enabled. + Options: 'notify_events', 'notify_disputes', 'notify_moderation' + """ result = await db.execute( select(User) .join(Participant, Participant.user_id == User.id) @@ -100,6 +106,10 @@ class TelegramNotifier: for user in users: if exclude_user_id and user.id == exclude_user_id: continue + # Check notification setting if specified + if check_setting and not getattr(user, check_setting, True): + logger.info(f"[Notify] Skipping user {user.nickname} - {check_setting} is disabled") + continue if await self.send_message(user.telegram_id, message): sent_count += 1 @@ -113,7 +123,7 @@ class TelegramNotifier: event_type: str, marathon_title: str ) -> int: - """Notify participants about event start.""" + """Notify participants about event start (respects notify_events setting).""" event_messages = { "golden_hour": f"🌟 Начался Golden Hour в «{marathon_title}»!\n\nВсе очки x1.5 в течение часа!", "jackpot": f"🎰 JACKPOT в «{marathon_title}»!\n\nОчки x3 за следующий сложный челлендж!", @@ -128,7 +138,9 @@ class TelegramNotifier: f"📌 Новое событие в «{marathon_title}»!" ) - return await self.notify_marathon_participants(db, marathon_id, message) + return await self.notify_marathon_participants( + db, marathon_id, message, check_setting='notify_events' + ) async def notify_event_end( self, @@ -137,7 +149,7 @@ class TelegramNotifier: event_type: str, marathon_title: str ) -> int: - """Notify participants about event end.""" + """Notify participants about event end (respects notify_events setting).""" event_names = { "golden_hour": "Golden Hour", "jackpot": "Jackpot", @@ -150,7 +162,9 @@ class TelegramNotifier: event_name = event_names.get(event_type, "Событие") message = f"⏰ {event_name} в «{marathon_title}» завершён" - return await self.notify_marathon_participants(db, marathon_id, message) + return await self.notify_marathon_participants( + db, marathon_id, message, check_setting='notify_events' + ) async def notify_marathon_start( self, @@ -186,7 +200,14 @@ class TelegramNotifier: challenge_title: str, assignment_id: int ) -> bool: - """Notify user about dispute raised on their assignment.""" + """Notify user about dispute raised on their assignment (respects notify_disputes setting).""" + # Check user's notification settings + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if user and not user.notify_disputes: + logger.info(f"[Dispute] Skipping user {user.nickname} - notify_disputes is disabled") + return False + logger.info(f"[Dispute] Sending notification to user_id={user_id} for assignment_id={assignment_id}") dispute_url = f"{settings.FRONTEND_URL}/assignments/{assignment_id}" @@ -227,7 +248,14 @@ class TelegramNotifier: challenge_title: str, is_valid: bool ) -> bool: - """Notify user about dispute resolution.""" + """Notify user about dispute resolution (respects notify_disputes setting).""" + # Check user's notification settings + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if user and not user.notify_disputes: + logger.info(f"[Dispute] Skipping user {user.nickname} - notify_disputes is disabled") + return False + if is_valid: message = ( f"❌ Спор признан обоснованным\n\n" @@ -251,7 +279,14 @@ class TelegramNotifier: marathon_title: str, game_title: str ) -> bool: - """Notify user that their proposed game was approved.""" + """Notify user that their proposed game was approved (respects notify_moderation setting).""" + # Check user's notification settings + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if user and not user.notify_moderation: + logger.info(f"[Moderation] Skipping user {user.nickname} - notify_moderation is disabled") + return False + message = ( f"✅ Твоя игра одобрена!\n\n" f"Марафон: {marathon_title}\n" @@ -267,7 +302,14 @@ class TelegramNotifier: marathon_title: str, game_title: str ) -> bool: - """Notify user that their proposed game was rejected.""" + """Notify user that their proposed game was rejected (respects notify_moderation setting).""" + # Check user's notification settings + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if user and not user.notify_moderation: + logger.info(f"[Moderation] Skipping user {user.nickname} - notify_moderation is disabled") + return False + message = ( f"❌ Твоя игра отклонена\n\n" f"Марафон: {marathon_title}\n" @@ -284,7 +326,14 @@ class TelegramNotifier: game_title: str, challenge_title: str ) -> bool: - """Notify user that their proposed challenge was approved.""" + """Notify user that their proposed challenge was approved (respects notify_moderation setting).""" + # Check user's notification settings + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if user and not user.notify_moderation: + logger.info(f"[Moderation] Skipping user {user.nickname} - notify_moderation is disabled") + return False + message = ( f"✅ Твой челлендж одобрен!\n\n" f"Марафон: {marathon_title}\n" @@ -302,7 +351,14 @@ class TelegramNotifier: game_title: str, challenge_title: str ) -> bool: - """Notify user that their proposed challenge was rejected.""" + """Notify user that their proposed challenge was rejected (respects notify_moderation setting).""" + # Check user's notification settings + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalar_one_or_none() + if user and not user.notify_moderation: + logger.info(f"[Moderation] Skipping user {user.nickname} - notify_moderation is disabled") + return False + message = ( f"❌ Твой челлендж отклонён\n\n" f"Марафон: {marathon_title}\n" diff --git a/bot/handlers/marathons.py b/bot/handlers/marathons.py index 5d894cd..a2d05a7 100644 --- a/bot/handlers/marathons.py +++ b/bot/handlers/marathons.py @@ -3,7 +3,7 @@ from aiogram.filters import Command from aiogram.types import Message, CallbackQuery from keyboards.main_menu import get_main_menu -from keyboards.inline import get_marathons_keyboard, get_marathon_details_keyboard +from keyboards.inline import get_marathons_keyboard, get_marathon_details_keyboard, get_settings_keyboard from services.api_client import api_client router = Router() @@ -197,15 +197,66 @@ async def cmd_settings(message: Message): ) return + # Get current notification settings + settings = await api_client.get_notification_settings(message.from_user.id) + if not settings: + settings = {"notify_events": True, "notify_disputes": True, "notify_moderation": True} + await message.answer( - "⚙️ Настройки\n\n" - "Управление уведомлениями будет доступно в следующем обновлении.\n\n" - "Сейчас ты получаешь все уведомления:\n" - "• 🌟 События (Golden Hour, Jackpot и др.)\n" - "• 🚀 Старт/финиш марафонов\n" - "• ⚠️ Споры по заданиям\n\n" - "Команды:\n" - "/unlink - Отвязать аккаунт\n" - "/status - Проверить привязку", + "⚙️ Настройки уведомлений\n\n" + "Нажми на категорию, чтобы включить/выключить:\n\n" + "✅ — уведомления включены\n" + "❌ — уведомления выключены\n\n" + "Уведомления о старте/финише марафонов и коды безопасности нельзя отключить.", + reply_markup=get_settings_keyboard(settings) + ) + + +@router.callback_query(F.data.startswith("toggle:")) +async def toggle_notification(callback: CallbackQuery): + """Toggle notification setting.""" + setting_name = callback.data.split(":")[1] + + # Get current settings + current_settings = await api_client.get_notification_settings(callback.from_user.id) + if not current_settings: + await callback.answer("Не удалось загрузить настройки", show_alert=True) + return + + # Toggle the setting + current_value = current_settings.get(setting_name, True) + new_value = not current_value + + # Update on backend + result = await api_client.update_notification_settings( + callback.from_user.id, + {setting_name: new_value} + ) + + if not result or result.get("error"): + await callback.answer("Не удалось сохранить настройки", show_alert=True) + return + + # Update keyboard with new values + await callback.message.edit_reply_markup( + reply_markup=get_settings_keyboard(result) + ) + + status = "включены" if new_value else "выключены" + setting_names = { + "notify_events": "События", + "notify_disputes": "Споры", + "notify_moderation": "Модерация" + } + await callback.answer(f"{setting_names.get(setting_name, setting_name)}: {status}") + + +@router.callback_query(F.data == "back_to_menu") +async def back_to_menu(callback: CallbackQuery): + """Go back to main menu from settings.""" + await callback.message.delete() + await callback.message.answer( + "Главное меню", reply_markup=get_main_menu() ) + await callback.answer() diff --git a/bot/keyboards/inline.py b/bot/keyboards/inline.py index fee0f88..4cd13f1 100644 --- a/bot/keyboards/inline.py +++ b/bot/keyboards/inline.py @@ -40,3 +40,45 @@ def get_marathon_details_keyboard(marathon_id: int) -> InlineKeyboardMarkup: ] return InlineKeyboardMarkup(inline_keyboard=buttons) + + +def get_settings_keyboard(settings: dict) -> InlineKeyboardMarkup: + """Create keyboard for notification settings.""" + # Get current values with defaults + notify_events = settings.get("notify_events", True) + notify_disputes = settings.get("notify_disputes", True) + notify_moderation = settings.get("notify_moderation", True) + + # Status indicators + events_status = "✅" if notify_events else "❌" + disputes_status = "✅" if notify_disputes else "❌" + moderation_status = "✅" if notify_moderation else "❌" + + buttons = [ + [ + InlineKeyboardButton( + text=f"{events_status} События (Golden Hour, Jackpot...)", + callback_data="toggle:notify_events" + ) + ], + [ + InlineKeyboardButton( + text=f"{disputes_status} Споры", + callback_data="toggle:notify_disputes" + ) + ], + [ + InlineKeyboardButton( + text=f"{moderation_status} Модерация (игры/челленджи)", + callback_data="toggle:notify_moderation" + ) + ], + [ + InlineKeyboardButton( + text="◀️ Назад", + callback_data="back_to_menu" + ) + ] + ] + + return InlineKeyboardMarkup(inline_keyboard=buttons) diff --git a/bot/services/api_client.py b/bot/services/api_client.py index 7f37e84..047caac 100644 --- a/bot/services/api_client.py +++ b/bot/services/api_client.py @@ -124,6 +124,22 @@ class APIClient: """Get user's overall statistics.""" return await self._request("GET", f"/telegram/stats/{telegram_id}") + async def get_notification_settings(self, telegram_id: int) -> dict[str, Any] | None: + """Get user's notification settings.""" + return await self._request("GET", f"/telegram/notifications/{telegram_id}") + + async def update_notification_settings( + self, + telegram_id: int, + settings: dict[str, bool] + ) -> dict[str, Any] | None: + """Update user's notification settings.""" + return await self._request( + "PATCH", + f"/telegram/notifications/{telegram_id}", + json=settings + ) + async def close(self): """Close the HTTP session.""" if self._session and not self._session.closed: diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 52c62e0..24950d5 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -1,5 +1,5 @@ import client from './client' -import type { User, UserProfilePublic, UserStats, PasswordChangeData } from '@/types' +import type { User, UserProfilePublic, UserStats, PasswordChangeData, NotificationSettings, NotificationSettingsUpdate } from '@/types' export interface UpdateNicknameData { nickname: string @@ -48,4 +48,16 @@ export const usersApi = { }) return URL.createObjectURL(response.data) }, + + // Получить настройки уведомлений + getNotificationSettings: async (): Promise => { + const response = await client.get('/users/me/notifications') + return response.data + }, + + // Обновить настройки уведомлений + updateNotificationSettings: async (data: NotificationSettingsUpdate): Promise => { + const response = await client.patch('/users/me/notifications', data) + return response.data + }, } diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index 3ea6148..6053e96 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -12,7 +12,8 @@ import { import { User, Camera, Trophy, Target, CheckCircle, Flame, Loader2, MessageCircle, Link2, Link2Off, ExternalLink, - Eye, EyeOff, Save, KeyRound, Shield + Eye, EyeOff, Save, KeyRound, Shield, Bell, Sparkles, + AlertTriangle, FileCheck } from 'lucide-react' // Schemas @@ -51,6 +52,12 @@ export function ProfilePage() { const [isPolling, setIsPolling] = useState(false) const pollingRef = useRef | null>(null) + // Notification settings state + const [notifyEvents, setNotifyEvents] = useState(user?.notify_events ?? true) + const [notifyDisputes, setNotifyDisputes] = useState(user?.notify_disputes ?? true) + const [notifyModeration, setNotifyModeration] = useState(user?.notify_moderation ?? true) + const [notificationUpdating, setNotificationUpdating] = useState(null) + const fileInputRef = useRef(null) // Forms @@ -265,6 +272,29 @@ export function ProfilePage() { } } + // Update notification setting + const handleNotificationToggle = async ( + setting: 'notify_events' | 'notify_disputes' | 'notify_moderation', + currentValue: boolean, + setValue: (value: boolean) => void + ) => { + setNotificationUpdating(setting) + const newValue = !currentValue + setValue(newValue) + + try { + await usersApi.updateNotificationSettings({ [setting]: newValue }) + updateUser({ [setting]: newValue }) + toast.success('Настройки сохранены') + } catch { + // Revert on error + setValue(currentValue) + toast.error('Не удалось сохранить настройки') + } finally { + setNotificationUpdating(null) + } + } + const isLinked = !!user?.telegram_id const displayAvatar = avatarBlobUrl || user?.telegram_avatar_url @@ -544,6 +574,109 @@ export function ProfilePage() { )} + + {/* Notifications */} + {isLinked && ( + +
+
+ +
+
+

Уведомления

+

Настройте типы уведомлений в Telegram

+
+
+ +
+ {/* Events toggle */} + + + {/* Disputes toggle */} + + + {/* Moderation toggle */} + + + {/* Info about mandatory notifications */} +

+ Уведомления о старте/финише марафонов и коды безопасности нельзя отключить. +

+
+
+ )} ) } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 1858cf8..63d1e19 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -18,6 +18,23 @@ export interface User extends UserPublic { telegram_username?: string | null // Only visible to self telegram_first_name?: string | null // Only visible to self telegram_last_name?: string | null // Only visible to self + // Notification settings + notify_events?: boolean + notify_disputes?: boolean + notify_moderation?: boolean +} + +// Notification settings +export interface NotificationSettings { + notify_events: boolean + notify_disputes: boolean + notify_moderation: boolean +} + +export interface NotificationSettingsUpdate { + notify_events?: boolean + notify_disputes?: boolean + notify_moderation?: boolean } export interface TokenResponse {