diff --git a/database.py b/database.py index b7880b4..c861556 100644 --- a/database.py +++ b/database.py @@ -70,6 +70,20 @@ class Announce(BaseModel): table_name = "announces" +class CustomDestinationDisplayName(BaseModel): + + id = BigAutoField() + destination_hash = CharField(unique=True) # unique destination hash + display_name = CharField() # custom display name for the destination hash + + 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 = "custom_destination_display_names" + + class LxmfMessage(BaseModel): id = BigAutoField() diff --git a/meshchat.py b/meshchat.py index b2920bb..85af673 100644 --- a/meshchat.py +++ b/meshchat.py @@ -71,6 +71,7 @@ class ReticulumMeshChat: self.db.create_tables([ database.Config, database.Announce, + database.CustomDestinationDisplayName, database.LxmfMessage, database.LxmfConversationReadState, ]) @@ -1045,6 +1046,42 @@ class ReticulumMeshChat: }, }) + # get custom destination display name + @routes.get("/api/v1/destination/{destination_hash}/custom-display-name") + async def index(request): + + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + return web.json_response({ + "custom_display_name": self.get_custom_destination_display_name(destination_hash), + }) + + # set custom destination display name + @routes.post("/api/v1/destination/{destination_hash}/custom-display-name/update") + async def index(request): + + # get path params + destination_hash = request.match_info.get("destination_hash", "") + + # get request data + data = await request.json() + display_name = data.get('display_name') + + # update display name if provided + if len(display_name) > 0: + self.db_upsert_custom_destination_display_name(destination_hash, display_name) + return web.json_response({ + "message": "Custom display name has been updated", + }) + + # otherwise remove display name + else: + database.CustomDestinationDisplayName.delete().where(database.CustomDestinationDisplayName.destination_hash == destination_hash).execute() + return web.json_response({ + "message": "Custom display name has been removed", + }) + # get interface stats @routes.get("/api/v1/interface-stats") async def index(request): @@ -1273,6 +1310,7 @@ class ReticulumMeshChat: # add to conversations conversations.append({ "display_name": self.get_lxmf_conversation_name(other_user_hash), + "custom_display_name": self.get_custom_destination_display_name(other_user_hash), "destination_hash": other_user_hash, "is_unread": self.is_lxmf_conversation_unread(other_user_hash), "failed_messages_count": self.lxmf_conversation_failed_messages_count(other_user_hash), @@ -1782,6 +1820,7 @@ class ReticulumMeshChat: "identity_public_key": announce.identity_public_key, "app_data": announce.app_data, "display_name": display_name, + "custom_display_name": self.get_custom_destination_display_name(announce.destination_hash), "created_at": announce.created_at, "updated_at": announce.updated_at, } @@ -1926,6 +1965,21 @@ class ReticulumMeshChat: query = query.on_conflict(conflict_target=[database.Announce.destination_hash], update=data) query.execute() + # upserts a custom destination display name to the database + def db_upsert_custom_destination_display_name(self, destination_hash: str, display_name: str): + + # prepare data to insert or update + data = { + "destination_hash": destination_hash, + "display_name": display_name, + "updated_at": datetime.now(timezone.utc), + } + + # upsert to database + query = database.CustomDestinationDisplayName.insert(data) + query = query.on_conflict(conflict_target=[database.CustomDestinationDisplayName.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): @@ -2207,6 +2261,16 @@ class ReticulumMeshChat: "announce": self.convert_db_announce_to_dict(announce), }))) + # gets the custom display name a user has set for the provided destination hash + def get_custom_destination_display_name(self, destination_hash: str): + + # get display name from database + db_destination_display_name = database.CustomDestinationDisplayName.get_or_none(database.CustomDestinationDisplayName.destination_hash == destination_hash) + if db_destination_display_name is not None: + return db_destination_display_name.display_name + + return None + # get name to show for an lxmf conversation # currently, this will use the app data from the most recent announce # TODO: we should fetch this from our contacts database, when it gets implemented, and if not found, fallback to app data diff --git a/src/frontend/components/messages/ConversationViewer.vue b/src/frontend/components/messages/ConversationViewer.vue index da16069..44397d8 100644 --- a/src/frontend/components/messages/ConversationViewer.vue +++ b/src/frontend/components/messages/ConversationViewer.vue @@ -8,7 +8,15 @@
-
{{ selectedPeer.display_name }}
+
+
+ + + + +
+
{{ selectedPeer.custom_display_name ?? selectedPeer.display_name }}
+
<{{ selectedPeer.destination_hash }}> {{ selectedPeerPath.hops }} {{ selectedPeerPath.hops === 1 ? 'hop' : 'hops' }} away
@@ -638,6 +646,53 @@ export default { isLxmfMessageInUi: function(hash) { return this.chatItems.findIndex((chatItem) => chatItem.lxmf_message?.hash === hash) !== -1; }, + async getCustomDisplayName() { + if(this.selectedPeer){ + try { + + // get custom display name + const response = await window.axios.get(`/api/v1/destination/${this.selectedPeer.destination_hash}/custom-display-name`); + + // update ui + this.selectedPeer.custom_display_name = response.data.custom_display_name; + + } catch(e) { + console.log(e); + } + } + }, + async updateCustomDisplayName() { + + // do nothing if no peer selected + if(!this.selectedPeer){ + return; + } + + // ask user for new display name + const displayName = await DialogUtils.prompt("Enter a custom display name"); + if(displayName == null){ + return; + } + + try { + + // update display name on server + await axios.post(`/api/v1/destination/${this.selectedPeer.destination_hash}/custom-display-name/update`, { + display_name: displayName, + }); + + // update display name in ui + await this.getCustomDisplayName(); + + // reload conversations (so conversations list updates name) + this.$emit("reload-conversations"); + + } catch(e) { + console.log(e); + DialogUtils.alert("Failed to update display name"); + } + + }, async deleteConversation() { // do nothing if no peer selected diff --git a/src/frontend/components/messages/MessagesSidebar.vue b/src/frontend/components/messages/MessagesSidebar.vue index d9289c5..afe5b4a 100644 --- a/src/frontend/components/messages/MessagesSidebar.vue +++ b/src/frontend/components/messages/MessagesSidebar.vue @@ -29,7 +29,7 @@
-
{{ conversation.display_name }}
+
{{ conversation.custom_display_name ?? conversation.display_name }}
{{ formatTimeAgo(conversation.updated_at) }}
@@ -90,7 +90,7 @@
-
{{ peer.display_name }}
+
{{ peer.custom_display_name ?? peer.display_name }}
{{ formatTimeAgo(peer.updated_at) }}
@@ -161,8 +161,9 @@ export default { return this.conversations.filter((conversation) => { const search = this.conversationsSearchTerm.toLowerCase(); const matchesDisplayName = conversation.display_name.toLowerCase().includes(search); + const matchesCustomDisplayName = conversation.custom_display_name?.toLowerCase()?.includes(search) === true; const matchesDestinationHash = conversation.destination_hash.toLowerCase().includes(search); - return matchesDisplayName || matchesDestinationHash; + return matchesDisplayName || matchesCustomDisplayName || matchesDestinationHash; }); }, peersCount() { @@ -181,8 +182,9 @@ export default { return this.peersOrderedByLatestAnnounce.filter((peer) => { const search = this.peersSearchTerm.toLowerCase(); const matchesDisplayName = peer.display_name.toLowerCase().includes(search); + const matchesCustomDisplayName = peer.custom_display_name?.toLowerCase()?.includes(search) === true; const matchesDestinationHash = peer.destination_hash.toLowerCase().includes(search); - return matchesDisplayName || matchesDestinationHash; + return matchesDisplayName || matchesCustomDisplayName || matchesDestinationHash; }); }, },