From f770dcf2dbced5d12d11838b2d28d584af4b3e51 Mon Sep 17 00:00:00 2001 From: retoor Date: Mon, 3 Nov 2025 18:08:13 +0100 Subject: [PATCH] Update. --- src/snek/schema.sql | 27 +++++------ src/snek/service/channel_member.py | 2 + src/snek/templates/web.html | 72 ++++++++++++++++++++++++++++++ src/snek/view/rpc.py | 20 +++++++++ 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/src/snek/schema.sql b/src/snek/schema.sql index 5b9c9a5..f89158e 100644 --- a/src/snek/schema.sql +++ b/src/snek/schema.sql @@ -39,19 +39,20 @@ CREATE TABLE IF NOT EXISTS channel ( ); CREATE INDEX IF NOT EXISTS ix_channel_e2577dd78b54fe28 ON channel (uid); CREATE TABLE IF NOT EXISTS channel_member ( - id INTEGER NOT NULL, - channel_uid TEXT, - created_at TEXT, - deleted_at TEXT, - is_banned BOOLEAN, - is_moderator BOOLEAN, - is_muted BOOLEAN, - is_read_only BOOLEAN, - label TEXT, - new_count BIGINT, - uid TEXT, - updated_at TEXT, - user_uid TEXT, + id INTEGER NOT NULL, + channel_uid TEXT, + created_at TEXT, + deleted_at TEXT, + is_banned BOOLEAN, + is_moderator BOOLEAN, + is_muted BOOLEAN, + is_read_only BOOLEAN, + label TEXT, + new_count BIGINT, + last_read_at TEXT, + uid TEXT, + updated_at TEXT, + user_uid TEXT, PRIMARY KEY (id) ); CREATE INDEX IF NOT EXISTS ix_channel_member_e2577dd78b54fe28 ON channel_member (uid); diff --git a/src/snek/service/channel_member.py b/src/snek/service/channel_member.py index df96786..b9b88d8 100644 --- a/src/snek/service/channel_member.py +++ b/src/snek/service/channel_member.py @@ -1,4 +1,5 @@ from snek.system.service import BaseService +from snek.system.model import now class ChannelMemberService(BaseService): @@ -8,6 +9,7 @@ class ChannelMemberService(BaseService): async def mark_as_read(self, channel_uid, user_uid): channel_member = await self.get(channel_uid=channel_uid, user_uid=user_uid) channel_member["new_count"] = 0 + channel_member["last_read_at"] = now() return await self.save(channel_member) async def get_user_uids(self, channel_uid): diff --git a/src/snek/templates/web.html b/src/snek/templates/web.html index 216f63e..225c825 100644 --- a/src/snek/templates/web.html +++ b/src/snek/templates/web.html @@ -10,6 +10,9 @@ + {% if not messages %} @@ -300,7 +303,76 @@ function isScrolledPastHalf() { // --- Initial layout update --- updateLayout(true); +// --- Jump to unread functionality --- +const jumpToUnreadBtn = document.getElementById('jump-to-unread-btn'); +let firstUnreadMessageUid = null; +async function checkForUnreadMessages() { + try { + const uid = await app.rpc.getFirstUnreadMessageUid(channelUid); + if (uid) { + firstUnreadMessageUid = uid; + const messageElement = messagesContainer.querySelector(`[data-uid="${uid}"]`); + if (messageElement && !messagesContainer.isElementVisible(messageElement)) { + jumpToUnreadBtn.style.display = 'block'; + } else { + jumpToUnreadBtn.style.display = 'none'; + } + } else { + jumpToUnreadBtn.style.display = 'none'; + } + } catch (error) { + console.error('Error checking for unread messages:', error); + } +} + +async function jumpToUnread() { + if (!firstUnreadMessageUid) { + await checkForUnreadMessages(); + } + + if (firstUnreadMessageUid) { + let messageElement = messagesContainer.querySelector(`[data-uid="${firstUnreadMessageUid}"]`); + + if (!messageElement) { + const messages = await app.rpc.getMessages(channelUid, 0, null); + const targetMessage = messages.find(m => m.uid === firstUnreadMessageUid); + + if (targetMessage) { + const temp = document.createElement("div"); + temp.innerHTML = targetMessage.html; + const newMessageElement = temp.firstChild; + + messagesContainer.endOfMessages.after(newMessageElement); + messagesContainer.messageMap.set(targetMessage.uid, newMessageElement); + messagesContainer._observer.observe(newMessageElement); + + messageElement = newMessageElement; + } + } + + if (messageElement) { + messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); + messageElement.style.animation = 'highlight-fade 2s'; + setTimeout(() => { + messageElement.style.animation = ''; + }, 2000); + jumpToUnreadBtn.style.display = 'none'; + } + } +} + +jumpToUnreadBtn.addEventListener('click', jumpToUnread); +checkForUnreadMessages(); + +const style = document.createElement('style'); +style.textContent = ` + @keyframes highlight-fade { + 0% { background-color: rgba(255, 255, 0, 0.3); } + 100% { background-color: transparent; } + } +`; +document.head.appendChild(style); {% endblock %} diff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py index b25c996..7d73db3 100644 --- a/src/snek/view/rpc.py +++ b/src/snek/view/rpc.py @@ -175,6 +175,26 @@ class RPCView(BaseView): messages.append(extended_dict) return messages + async def get_first_unread_message_uid(self, channel_uid): + self._require_login() + channel_member = await self.services.channel_member.get( + channel_uid=channel_uid, user_uid=self.user_uid + ) + if not channel_member: + return None + + last_read_at = channel_member.get("last_read_at") + if not last_read_at: + return None + + async for message in self.services.channel_message.query( + "SELECT uid FROM channel_message WHERE channel_uid=:channel_uid AND created_at > :last_read_at AND deleted_at IS NULL ORDER BY created_at ASC LIMIT 1", + {"channel_uid": channel_uid, "last_read_at": last_read_at} + ): + return message["uid"] + + return None + async def get_channels(self): self._require_login() channels = []