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 = []