add support for sideband/lxmf user appearance icons

This commit is contained in:
liamcottle
2024-12-04 01:19:47 +13:00
parent cff5b2023c
commit 8e2cd6c62c
6 changed files with 109 additions and 1 deletions

View File

@@ -123,3 +123,19 @@ class LxmfConversationReadState(BaseModel):
# define table name # define table name
class Meta: class Meta:
table_name = "lxmf_conversation_read_state" table_name = "lxmf_conversation_read_state"
class LxmfUserIcon(BaseModel):
id = BigAutoField()
destination_hash = CharField(unique=True) # unique destination hash
icon_name = CharField() # material design icon name for the destination hash
foreground_colour = CharField() # hex colour to use for foreground (icon colour)
background_colour = CharField() # hex colour to use for background (background colour)
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_user_icons"

View File

@@ -74,6 +74,7 @@ class ReticulumMeshChat:
database.CustomDestinationDisplayName, database.CustomDestinationDisplayName,
database.LxmfMessage, database.LxmfMessage,
database.LxmfConversationReadState, database.LxmfConversationReadState,
database.LxmfUserIcon,
]) ])
# init config # init config
@@ -1432,6 +1433,16 @@ class ReticulumMeshChat:
else: else:
other_user_hash = source_hash other_user_hash = source_hash
# find lxmf user icon from database
lxmf_user_icon = None
db_lxmf_user_icon = database.LxmfUserIcon.get_or_none(database.LxmfUserIcon.destination_hash == other_user_hash)
if db_lxmf_user_icon is not None:
lxmf_user_icon = {
"icon_name": db_lxmf_user_icon.icon_name,
"foreground_colour": db_lxmf_user_icon.foreground_colour,
"background_colour": db_lxmf_user_icon.background_colour,
}
# add to conversations # add to conversations
conversations.append({ conversations.append({
"display_name": self.get_lxmf_conversation_name(other_user_hash), "display_name": self.get_lxmf_conversation_name(other_user_hash),
@@ -1439,6 +1450,7 @@ class ReticulumMeshChat:
"destination_hash": other_user_hash, "destination_hash": other_user_hash,
"is_unread": self.is_lxmf_conversation_unread(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), "failed_messages_count": self.lxmf_conversation_failed_messages_count(other_user_hash),
"lxmf_user_icon": lxmf_user_icon,
# we say the conversation was updated when the latest message was created # we say the conversation was updated when the latest message was created
# otherwise this will go crazy when sending a message, as the updated_at on the latest message changes very frequently # otherwise this will go crazy when sending a message, as the updated_at on the latest message changes very frequently
"updated_at": created_at, "updated_at": created_at,
@@ -2000,6 +2012,26 @@ class ReticulumMeshChat:
"updated_at": db_lxmf_message.updated_at, "updated_at": db_lxmf_message.updated_at,
} }
# updates the lxmf user icon for the provided destination hash
def update_lxmf_user_icon(self, destination_hash: str, icon_name: str, foreground_colour: str, background_colour: str):
# log
print(f"updating lxmf user icon for {destination_hash} to icon_name={icon_name}, foreground_colour={foreground_colour}, background_colour={background_colour}")
# prepare data to insert or update
data = {
"destination_hash": destination_hash,
"icon_name": icon_name,
"foreground_colour": foreground_colour,
"background_colour": background_colour,
"updated_at": datetime.now(timezone.utc),
}
# upsert to database
query = database.LxmfUserIcon.insert(data)
query = query.on_conflict(conflict_target=[database.LxmfUserIcon.destination_hash], update=data)
query.execute()
# handle an lxmf delivery from reticulum # handle an lxmf delivery from reticulum
# NOTE: cant be async, as Reticulum doesn't await it # NOTE: cant be async, as Reticulum doesn't await it
def on_lxmf_delivery(self, lxmf_message: LXMF.LXMessage): def on_lxmf_delivery(self, lxmf_message: LXMF.LXMessage):
@@ -2008,6 +2040,20 @@ class ReticulumMeshChat:
# upsert lxmf message to database # upsert lxmf message to database
self.db_upsert_lxmf_message(lxmf_message) self.db_upsert_lxmf_message(lxmf_message)
# get icon appearance if available
try:
message_fields = lxmf_message.get_fields()
if LXMF.FIELD_ICON_APPEARANCE in message_fields:
icon_appearance = message_fields[LXMF.FIELD_ICON_APPEARANCE]
icon_name = icon_appearance[0]
foreground_colour = "#" + icon_appearance[1].hex()
background_colour = "#" + icon_appearance[2].hex()
self.update_lxmf_user_icon(lxmf_message.source_hash.hex(), icon_name, foreground_colour, background_colour)
except Exception as e:
print("failed to update lxmf user icon from lxmf message")
print(e)
pass
# find message from database # find message from database
db_lxmf_message = database.LxmfMessage.get_or_none(database.LxmfMessage.hash == lxmf_message.hash.hex()) db_lxmf_message = database.LxmfMessage.get_or_none(database.LxmfMessage.hash == lxmf_message.hash.hex())
if db_lxmf_message is None: if db_lxmf_message is None:

6
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "1.13.2", "version": "1.13.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mdi/js": "^7.4.47",
"@vitejs/plugin-vue": "^5.1.2", "@vitejs/plugin-vue": "^5.1.2",
"click-outside-vue3": "^4.0.1", "click-outside-vue3": "^4.0.1",
"electron-prompt": "^1.7.0", "electron-prompt": "^1.7.0",
@@ -877,6 +878,11 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/@mdi/js": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz",
"integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ=="
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",

View File

@@ -93,6 +93,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@mdi/js": "^7.4.47",
"@vitejs/plugin-vue": "^5.1.2", "@vitejs/plugin-vue": "^5.1.2",
"click-outside-vue3": "^4.0.1", "click-outside-vue3": "^4.0.1",
"electron-prompt": "^1.7.0", "electron-prompt": "^1.7.0",

View File

@@ -0,0 +1,34 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" :aria-label="iconName" fill="currentColor" style="display:inline-block;vertical-align:middle;">
<path :d="iconPath"/>
</svg>
</template>
<script>
import * as mdi from "@mdi/js";
export default {
name: "MaterialDesignIcon",
props: {
iconName: {
type: String,
required: true,
},
},
computed: {
mdiIconName() {
// convert icon name from lxmf icon appearance to format expected by the @mdi/js library
// e.g: alien-outline -> mdiAlienOutline
// https://pictogrammers.github.io/@mdi/font/5.4.55/
return "mdi" + this.iconName.split("-").map((word) => {
// capitalise first letter of each part
return word.charAt(0).toUpperCase() + word.slice(1);
}).join("");
},
iconPath() {
// find icon, otherwise fallback to question mark, and if that doesn't exist, show nothing...
return mdi[this.mdiIconName] || mdi["mdiProgressQuestion"] || "";
},
},
};
</script>

View File

@@ -22,7 +22,10 @@
<div v-if="searchedConversations.length > 0" class="w-full"> <div v-if="searchedConversations.length > 0" class="w-full">
<div @click="onConversationClick(conversation)" v-for="conversation of searchedConversations" class="flex cursor-pointer p-2 border-l-2" :class="[ conversation.destination_hash === selectedDestinationHash ? 'bg-gray-100 border-blue-500' : 'bg-white border-transparent hover:bg-gray-50 hover:border-gray-200' ]"> <div @click="onConversationClick(conversation)" v-for="conversation of searchedConversations" class="flex cursor-pointer p-2 border-l-2" :class="[ conversation.destination_hash === selectedDestinationHash ? 'bg-gray-100 border-blue-500' : 'bg-white border-transparent hover:bg-gray-50 hover:border-gray-200' ]">
<div class="my-auto mr-2"> <div class="my-auto mr-2">
<div class="bg-gray-200 text-gray-500 p-2 rounded"> <div v-if="conversation.lxmf_user_icon" class="p-2 rounded" :style="{ 'color': conversation.lxmf_user_icon.foreground_colour, 'background-color': conversation.lxmf_user_icon.background_colour }">
<MaterialDesignIcon :icon-name="conversation.lxmf_user_icon.icon_name" class="w-6 h-6"/>
</div>
<div v-else class="bg-gray-200 text-gray-500 p-2 rounded">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg> </svg>
@@ -130,9 +133,11 @@
<script> <script>
import Utils from "../../js/Utils"; import Utils from "../../js/Utils";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
export default { export default {
name: 'MessagesSidebar', name: 'MessagesSidebar',
components: {MaterialDesignIcon},
props: { props: {
peers: Object, peers: Object,
conversations: Array, conversations: Array,