This commit is contained in:
retoor 2025-11-03 18:08:13 +01:00
parent 2deb8a2069
commit f770dcf2db
4 changed files with 108 additions and 13 deletions

View File

@ -49,6 +49,7 @@ CREATE TABLE IF NOT EXISTS channel_member (
is_read_only BOOLEAN, is_read_only BOOLEAN,
label TEXT, label TEXT,
new_count BIGINT, new_count BIGINT,
last_read_at TEXT,
uid TEXT, uid TEXT,
updated_at TEXT, updated_at TEXT,
user_uid TEXT, user_uid TEXT,

View File

@ -1,4 +1,5 @@
from snek.system.service import BaseService from snek.system.service import BaseService
from snek.system.model import now
class ChannelMemberService(BaseService): class ChannelMemberService(BaseService):
@ -8,6 +9,7 @@ class ChannelMemberService(BaseService):
async def mark_as_read(self, channel_uid, user_uid): 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 = await self.get(channel_uid=channel_uid, user_uid=user_uid)
channel_member["new_count"] = 0 channel_member["new_count"] = 0
channel_member["last_read_at"] = now()
return await self.save(channel_member) return await self.save(channel_member)
async def get_user_uids(self, channel_uid): async def get_user_uids(self, channel_uid):

View File

@ -10,6 +10,9 @@
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script> <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
<div id="terminal" class="hidden"></div> <div id="terminal" class="hidden"></div>
<button id="jump-to-unread-btn" style="display: none; position: absolute; top: 10px; right: 10px; z-index: 1000; padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.2);">
Jump to First Unread
</button>
<message-list class="chat-messages"> <message-list class="chat-messages">
{% if not messages %} {% if not messages %}
@ -300,7 +303,76 @@ function isScrolledPastHalf() {
// --- Initial layout update --- // --- Initial layout update ---
updateLayout(true); 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);
</script> </script>
{% endblock %} {% endblock %}

View File

@ -175,6 +175,26 @@ class RPCView(BaseView):
messages.append(extended_dict) messages.append(extended_dict)
return messages 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): async def get_channels(self):
self._require_login() self._require_login()
channels = [] channels = []