mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-10-30 23:51:57 +01:00
A channel used to have two settings: every message or nothing. Now it has three - muted, every message, or a named profile of rules (mentions me, message contains, sender contains; any or all must match). A channel on a profile counts only matching messages: the unread badge, the navbar bell, the app-icon badge and the browser/Android notification all follow the same rule. Direct messages are untouched. - app/notify_profiles.py: rule matching (case- and diacritic-insensitive, same table as the chat filter), validation, storage in app_settings - read_status.notify_profile column, written together with is_muted so a mute never leaves a profile behind; migration added - /api/notification-profiles CRUD, PUT /api/channels/<i>/notifications; /api/read_status and /api/messages/updates report channel_notify_profiles, the latter counts a profiled channel's unread through the profile - Manage Channels: the bell opens a menu (Off / Every message / profiles / Manage profiles...) instead of toggling; profiled rows get a green badge - Settings > Notifications: profile list with in-use counts and an editor - app.js evaluates the same rules for socket-delivered messages, and a state version guard stops an in-flight poll from undoing a fresh mute/favorite/profile change - i18n en/pl (43 keys), docs (user guide, architecture, whatsnew), tests Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
206 lines
6.7 KiB
Python
206 lines
6.7 KiB
Python
"""
|
|
Read Status Manager - DB-backed storage for message read status
|
|
|
|
Manages the last seen timestamps for channels and DM conversations,
|
|
providing cross-device synchronization for unread message tracking.
|
|
All data is stored in the read_status table of the SQLite database.
|
|
"""
|
|
|
|
import logging
|
|
from flask import current_app
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_db():
|
|
"""Get database instance from Flask app context."""
|
|
return getattr(current_app, 'db', None)
|
|
|
|
|
|
def load_read_status():
|
|
"""Load read status from database.
|
|
|
|
Returns:
|
|
dict: Read status with 'channels', 'dm', 'muted_channels',
|
|
'favorite_channels' and 'channel_notify_profiles' keys. The last is
|
|
{"<channel idx>": "<profile id>"} for channels in profile mode.
|
|
"""
|
|
try:
|
|
db = _get_db()
|
|
rows = db.get_read_status()
|
|
|
|
channels = {}
|
|
dm = {}
|
|
muted_channels = []
|
|
favorite_channels = []
|
|
channel_notify_profiles = {}
|
|
|
|
for key, row in rows.items():
|
|
if key.startswith('chan_'):
|
|
chan_idx = key[5:] # "chan_0" -> "0"
|
|
channels[chan_idx] = row['last_seen_ts']
|
|
if row.get('is_muted'):
|
|
try:
|
|
muted_channels.append(int(chan_idx))
|
|
except ValueError:
|
|
pass
|
|
if row.get('is_favorite'):
|
|
try:
|
|
favorite_channels.append(int(chan_idx))
|
|
except ValueError:
|
|
pass
|
|
if row.get('notify_profile') and not row.get('is_muted'):
|
|
channel_notify_profiles[chan_idx] = row['notify_profile']
|
|
elif key.startswith('dm_'):
|
|
conv_id = key[3:] # "dm_name_User1" -> "name_User1"
|
|
dm[conv_id] = row['last_seen_ts']
|
|
|
|
return {
|
|
'channels': channels,
|
|
'dm': dm,
|
|
'muted_channels': muted_channels,
|
|
'favorite_channels': favorite_channels,
|
|
'channel_notify_profiles': channel_notify_profiles,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error loading read status: {e}")
|
|
return {'channels': {}, 'dm': {}, 'muted_channels': [], 'favorite_channels': [],
|
|
'channel_notify_profiles': {}}
|
|
|
|
|
|
def save_read_status(status):
|
|
"""No-op — data is written per-operation via mark_* functions."""
|
|
return True
|
|
|
|
|
|
def mark_channel_read(channel_idx, timestamp):
|
|
"""Mark a channel as read up to a specific timestamp."""
|
|
try:
|
|
db = _get_db()
|
|
db.mark_read(f"chan_{channel_idx}", int(timestamp))
|
|
logger.debug(f"Marked channel {channel_idx} as read at timestamp {timestamp}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error marking channel {channel_idx} as read: {e}")
|
|
return False
|
|
|
|
|
|
def mark_dm_read(conversation_id, timestamp):
|
|
"""Mark a DM conversation as read up to a specific timestamp."""
|
|
try:
|
|
db = _get_db()
|
|
db.mark_read(f"dm_{conversation_id}", int(timestamp))
|
|
logger.debug(f"Marked DM conversation {conversation_id} as read at timestamp {timestamp}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error marking DM conversation {conversation_id} as read: {e}")
|
|
return False
|
|
|
|
|
|
def get_channel_last_seen(channel_idx):
|
|
"""Get last seen timestamp for a specific channel."""
|
|
try:
|
|
status = load_read_status()
|
|
return status['channels'].get(str(channel_idx), 0)
|
|
except Exception as e:
|
|
logger.error(f"Error getting last seen for channel {channel_idx}: {e}")
|
|
return 0
|
|
|
|
|
|
def get_dm_last_seen(conversation_id):
|
|
"""Get last seen timestamp for a specific DM conversation."""
|
|
try:
|
|
status = load_read_status()
|
|
return status['dm'].get(conversation_id, 0)
|
|
except Exception as e:
|
|
logger.error(f"Error getting last seen for DM {conversation_id}: {e}")
|
|
return 0
|
|
|
|
|
|
def get_muted_channels():
|
|
"""Get list of muted channel indices."""
|
|
try:
|
|
db = _get_db()
|
|
return db.get_muted_channels()
|
|
except Exception as e:
|
|
logger.error(f"Error getting muted channels: {e}")
|
|
return []
|
|
|
|
|
|
def set_channel_muted(channel_idx, muted):
|
|
"""Set mute state for a channel. Unmuting lands on 'every message'."""
|
|
return set_channel_notify(channel_idx, 'muted' if muted else 'all')
|
|
|
|
|
|
def set_channel_notify(channel_idx, mode, profile_id=None):
|
|
"""
|
|
Set how a channel asks for attention.
|
|
|
|
Args:
|
|
mode: 'muted' (never), 'all' (every message) or 'profile' (only
|
|
messages passing the notification profile `profile_id`)
|
|
"""
|
|
try:
|
|
db = _get_db()
|
|
muted = mode == 'muted'
|
|
db.set_channel_notify(int(channel_idx), muted,
|
|
profile_id if mode == 'profile' else None)
|
|
if mode == 'profile':
|
|
logger.info(f"Channel {channel_idx} notifications: profile {profile_id}")
|
|
else:
|
|
logger.info(f"Channel {channel_idx} {'muted' if muted else 'unmuted'}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error setting notifications for channel {channel_idx}: {e}")
|
|
return False
|
|
|
|
|
|
def get_channel_notify_profiles():
|
|
"""Channels in profile mode: {channel_idx: profile_id}."""
|
|
try:
|
|
db = _get_db()
|
|
return db.get_channel_notify_profiles()
|
|
except Exception as e:
|
|
logger.error(f"Error getting channel notification profiles: {e}")
|
|
return {}
|
|
|
|
|
|
def get_favorite_channels():
|
|
"""Get list of favorite channel indices."""
|
|
try:
|
|
db = _get_db()
|
|
return db.get_favorite_channels()
|
|
except Exception as e:
|
|
logger.error(f"Error getting favorite channels: {e}")
|
|
return []
|
|
|
|
|
|
def set_channel_favorite(channel_idx, favorite):
|
|
"""Set favorite state for a channel."""
|
|
try:
|
|
db = _get_db()
|
|
db.set_channel_favorite(int(channel_idx), favorite)
|
|
logger.info(f"Channel {channel_idx} {'favorited' if favorite else 'unfavorited'}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error setting favorite for channel {channel_idx}: {e}")
|
|
return False
|
|
|
|
|
|
def mark_all_channels_read(channel_timestamps):
|
|
"""Mark all channels as read in bulk.
|
|
|
|
Args:
|
|
channel_timestamps (dict): {"0": timestamp, "1": timestamp, ...}
|
|
"""
|
|
try:
|
|
db = _get_db()
|
|
for channel_key, timestamp in channel_timestamps.items():
|
|
db.mark_read(f"chan_{channel_key}", int(timestamp))
|
|
logger.info(f"Marked {len(channel_timestamps)} channels as read")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error marking all channels as read: {e}")
|
|
return False
|