add support for sideband/lxmf user appearance icons
This commit is contained in:
16
database.py
16
database.py
@@ -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"
|
||||||
|
|||||||
46
meshchat.py
46
meshchat.py
@@ -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
6
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
34
src/frontend/components/MaterialDesignIcon.vue
Normal file
34
src/frontend/components/MaterialDesignIcon.vue
Normal 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>
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user