diff --git a/database.py b/database.py index 5670703..a895989 100644 --- a/database.py +++ b/database.py @@ -59,3 +59,17 @@ class LxmfMessage(BaseModel): # define table name class Meta: table_name = "lxmf_messages" + + +class LxmfConversationReadState(BaseModel): + + id = BigAutoField() + destination_hash = CharField(unique=True) # unique destination hash + last_read_at = DateTimeField() + + created_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) + updated_at = DateTimeField(default=lambda: datetime.now(timezone.utc)) + + # define table name + class Meta: + table_name = "lxmf_conversation_read_state" diff --git a/public/index.html b/public/index.html index a35e7e5..4a60792 100644 --- a/public/index.html +++ b/public/index.html @@ -2043,8 +2043,13 @@ this.loadNodePage(node.destination_hash, "/page/index.mu"); }, onConversationClick: function(conversation) { + // object must stay compatible with format of peers this.onPeerClick(conversation); + + // mark conversation as read + this.markConversationAsRead(conversation.destination_hash); + }, parseSeconds: function(secondsToFormat) { secondsToFormat = Number(secondsToFormat); @@ -2192,6 +2197,20 @@ // reload conversation await this.loadLxmfMessages(this.selectedPeer.destination_hash); + }, + async markConversationAsRead(destinationHash) { + + // mark conversation as read + try { + await window.axios.get(`/api/v1/lxmf/conversations/${this.selectedPeer.destination_hash}/mark-as-read`); + } catch(e) { + // do nothing if failed to mark as read + console.log(e); + } + + // reload conversations + await this.getConversations(); + }, async enableInterface(interfaceName) { diff --git a/web.py b/web.py index 8aa20e1..5c8e95e 100644 --- a/web.py +++ b/web.py @@ -65,6 +65,7 @@ class ReticulumWebChat: database.Config, database.Announce, database.LxmfMessage, + database.LxmfConversationReadState, ]) # vacuum database on start to shrink its file size @@ -852,6 +853,20 @@ class ReticulumWebChat: "conversations": conversations, }) + # mark lxmf conversation as read + @routes.get("/api/v1/lxmf/conversations/{destination_hash}/mark-as-read") + async def index(request): + + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # mark lxmf conversation as read + self.db_mark_lxmf_conversation_as_read(destination_hash) + + return web.json_response({ + "message": "ok", + }) + # called when web app has started async def on_startup(app): @@ -1299,6 +1314,21 @@ class ReticulumWebChat: query = query.on_conflict(conflict_target=[database.Announce.destination_hash], update=data) query.execute() + # upserts lxmf conversation read state to the database + def db_mark_lxmf_conversation_as_read(self, destination_hash: str): + + # prepare data to insert or update + data = { + "destination_hash": destination_hash, + "last_read_at": datetime.now(timezone.utc), + "updated_at": datetime.now(timezone.utc), + } + + # upsert to database + query = database.LxmfConversationReadState.insert(data) + query = query.on_conflict(conflict_target=[database.LxmfConversationReadState.destination_hash], update=data) + query.execute() + # handle sending an lxmf message to reticulum async def send_message(self, destination_hash, content: str, image_field: LxmfImageField = None,