refactor sending and receiving messages so delivery states can be shown in ui

This commit is contained in:
liamcottle
2024-04-30 03:15:42 +12:00
parent 855751ffbb
commit 2dca43116d
2 changed files with 160 additions and 49 deletions

View File

@@ -133,7 +133,7 @@
<!-- peer info -->
<div>
<div>{{ selectedPeer.app_data || "Anonymous Peer" }}</div>
<div>{{ selectedPeer.app_data || "Unknown" }}</div>
<div class="text-sm">@<{{ selectedPeer.destination_hash }}></div>
</div>
@@ -157,18 +157,13 @@
<div class="max-w-xl mx-auto">
<div v-if="selectedPeerChatItems.length > 0" class="flex flex-col space-y-3 py-4">
<div v-for="chatItem of selectedPeerChatItems">
<!-- message content -->
<div class="flex space-x-2 border border-gray-300 rounded-lg shadow px-2 py-1.5 bg-white">
<div>
<!-- error -->
<div v-if="chatItem.source_hash === 'error'" class="bg-red-500 text-white rounded-full p-1 shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
</div>
<!-- user -->
<div v-else class="bg-blue-500 text-white rounded-full p-1 shadow-md">
<div class="bg-blue-500 text-white rounded-full p-1 shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<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>
@@ -178,15 +173,14 @@
<div class="w-full">
<div class="font-semibold leading-5">
<span v-if="chatItem.is_outbound">You</span>
<span v-else-if="chatItem.source_hash === 'error'">Error</span>
<span v-else>@<{{ chatItem.source_hash }}></span>
<span v-else>{{ selectedPeer.app_data || "Unknown" }}</span>
</div>
<div v-if="chatItem.message.content" style="white-space:pre-wrap;word-wrap:break-word;font-family:inherit;">{{ chatItem.message.content }}</div>
<div v-if="chatItem.message.fields?.image" class="grid grid-cols-3 gap-2">
<img @click="openImage(`data:image/${chatItem.message.fields.image.image_type};base64,${chatItem.message.fields.image.image_bytes}`)" :src="`data:image/${chatItem.message.fields.image.image_type};base64,${chatItem.message.fields.image.image_bytes}`" class="w-full rounded-md shadow-md cursor-pointer"/>
<div v-if="chatItem.lxmf_message.content" style="white-space:pre-wrap;word-wrap:break-word;font-family:inherit;">{{ chatItem.lxmf_message.content }}</div>
<div v-if="chatItem.lxmf_message.fields?.image" class="grid grid-cols-3 gap-2">
<img @click="openImage(`data:image/${chatItem.lxmf_message.fields.image.image_type};base64,${chatItem.lxmf_message.fields.image.image_bytes}`)" :src="`data:image/${chatItem.lxmf_message.fields.image.image_type};base64,${chatItem.lxmf_message.fields.image.image_bytes}`" class="w-full rounded-md shadow-md cursor-pointer"/>
</div>
<div v-if="chatItem.message.fields?.file_attachments">
<a target="_blank" :download="file_attachment.file_name" :href="`data:application/octet-stream;base64,${file_attachment.file_bytes}`" v-for="file_attachment of chatItem.message.fields?.file_attachments ?? []" class="flex border border-gray-200 hover:bg-gray-100 rounded px-2 py-1 text-sm text-gray-700 font-semibold cursor-pointer">
<div v-if="chatItem.lxmf_message.fields?.file_attachments">
<a target="_blank" :download="file_attachment.file_name" :href="`data:application/octet-stream;base64,${file_attachment.file_bytes}`" v-for="file_attachment of chatItem.lxmf_message.fields?.file_attachments ?? []" class="flex border border-gray-200 hover:bg-gray-100 rounded px-2 py-1 text-sm text-gray-700 font-semibold cursor-pointer">
<div class="mr-2 my-auto">
<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="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13"></path>
@@ -202,6 +196,10 @@
</div>
</div>
</div>
<!-- message state -->
<div v-if="chatItem.is_outbound" class="text-gray-500">{{ chatItem.lxmf_message.state }}</div>
</div>
</div>
</div>
@@ -302,14 +300,51 @@
break;
}
case 'lxmf.delivery': {
// add inbound message to ui
this.chatItems.push({
"source_hash": json.source_hash,
"message": json.message,
})
"type": "lxmf_message",
"lxmf_message": json.lxmf_message,
});
// auto scroll to bottom if we want to
if(this.autoScrollOnNewMessage){
this.scrollMessagesToBottom();
}
break;
}
case 'lxmf_outbound_message_created': {
// add outbound message to ui
this.chatItems.push({
"type": "lxmf_message",
"lxmf_message": json.lxmf_message,
"is_outbound": true,
});
// always scroll to bottom since we just sent a message
this.scrollMessagesToBottom();
break;
}
case 'lxmf_message_state_updated': {
// find existing chat item by lxmf message hash
const lxmfMessageHash = json.lxmf_message.hash;
const chatItemIndex = this.chatItems.findIndex((chatItem) => chatItem.lxmf_message?.hash === lxmfMessageHash);
if(chatItemIndex === -1){
console.log("did not find existing chat item index for lxmf message hash: " + json.lxmf_message.hash);
return;
}
// update lxmf message from server, while ensuring ui updates from nested object change
this.chatItems[chatItemIndex].lxmf_message = json.lxmf_message;
break;
}
}
};
@@ -376,29 +411,14 @@
"message": messageText,
}));
// add sent message to ui
this.chatItems.push({
"is_outbound": true,
"source_hash": this.config.lxmf_address_hash, // from us
"destination_hash": this.selectedPeer.destination_hash, // to them
"message": {
"content": messageText,
},
});
// clear message input
this.newMessageText = "";
} catch(e) {
// todo handle error
console.error(e);
this.chatItems.push({
"source_hash": "error",
"type": "text",
"text": e.message ?? e ?? "Unknown Error...",
});
} finally {
this.isSendingMessage = false;
}
@@ -472,9 +492,16 @@
// get all chat items related to the selected peer
if(this.selectedPeer){
return this.chatItems.filter((chatItem) => {
const isFromSelectedPeer = chatItem.source_hash === this.selectedPeer.destination_hash;
const isToSelectedPeer = chatItem.destination_hash === this.selectedPeer.destination_hash;
if(chatItem.type === "lxmf_message"){
const isFromSelectedPeer = chatItem.lxmf_message.source_hash === this.selectedPeer.destination_hash;
const isToSelectedPeer = chatItem.lxmf_message.destination_hash === this.selectedPeer.destination_hash;
return isFromSelectedPeer || isToSelectedPeer;
}
return false;
});
}

104
web.py
View File

@@ -173,7 +173,7 @@ class ReticulumWebChat:
# send lxmf message to destination
destination_hash = data["destination_hash"]
message = data["message"]
self.send_message(destination_hash, message)
await self.send_message(destination_hash, message)
# # TODO: send response to client when marked as delivered?
# await client.send(json.dumps({
@@ -206,6 +206,71 @@ class ReticulumWebChat:
},
}))
# convert an lxmf message to a dictionary, for sending over websocket
def convert_lxmf_message_to_dict(self, lxmf_message: LXMF.LXMessage):
# handle fields
fields = {}
message_fields = lxmf_message.get_fields()
for field_type in message_fields:
value = message_fields[field_type]
# handle file attachments field
if field_type == LXMF.FIELD_FILE_ATTACHMENTS:
# process file attachments
file_attachments = []
for file_attachment in value:
file_name = file_attachment[0]
file_bytes = base64.b64encode(file_attachment[1]).decode("utf-8")
file_attachments.append({
"file_name": file_name,
"file_bytes": file_bytes,
})
# add to fields
fields["file_attachments"] = file_attachments
# handle image field
if field_type == LXMF.FIELD_IMAGE:
image_type = value[0]
image_bytes = base64.b64encode(value[1]).decode("utf-8")
fields["image"] = {
"image_type": image_type,
"image_bytes": image_bytes,
}
return {
"hash": lxmf_message.hash.hex(),
"source_hash": lxmf_message.source_hash.hex(),
"destination_hash": lxmf_message.destination_hash.hex(),
"state": self.convert_lxmf_state_to_string(lxmf_message),
"progress": lxmf_message.progress,
"content": lxmf_message.content.decode('utf-8'),
"fields": fields,
}
# convert lxmf state to a human friendly string
def convert_lxmf_state_to_string(self, lxmf_message: LXMF.LXMessage):
# convert state to string
lxmf_message_state = "unknown"
if lxmf_message.state == LXMF.LXMessage.DRAFT:
lxmf_message_state = "draft"
elif lxmf_message.state == LXMF.LXMessage.OUTBOUND:
lxmf_message_state = "outbound"
elif lxmf_message.state == LXMF.LXMessage.SENDING:
lxmf_message_state = "sending"
elif lxmf_message.state == LXMF.LXMessage.SENT:
lxmf_message_state = "sent"
elif lxmf_message.state == LXMF.LXMessage.DELIVERED:
lxmf_message_state = "delivered"
elif lxmf_message.state == LXMF.LXMessage.FAILED:
lxmf_message_state = "failed"
return lxmf_message_state
# handle an lxmf delivery from reticulum
# NOTE: cant be async, as Reticulum doesn't await it
def on_lxmf_delivery(self, message):
@@ -249,25 +314,36 @@ class ReticulumWebChat:
# send received lxmf message data to all websocket clients
asyncio.run(self.websocket_broadcast(json.dumps({
"type": "lxmf.delivery",
"source_hash": source_hash_text,
"message": {
"content": message_content,
"fields": fields,
},
"lxmf_message": self.convert_lxmf_message_to_dict(message),
})))
except Exception as e:
# do nothing on error
print("lxmf_delivery error: {}".format(e))
# handle delivery status update for an outbound lxmf message
def on_lxmf_sending_state_updated(self, lxmf_message):
# send lxmf message state to all websocket clients
asyncio.run(self.websocket_broadcast(json.dumps({
"type": "lxmf_message_state_updated",
"lxmf_message": self.convert_lxmf_message_to_dict(lxmf_message),
})))
# handle delivery failed for an outbound lxmf message
def on_lxmf_sending_failed(self, lxmf_message):
# just pass this on, we don't need to do anything special
self.on_lxmf_sending_state_updated(lxmf_message)
# handle sending an lxmf message to reticulum
def send_message(self, destination_hash, message_content):
async def send_message(self, destination_hash, message_content):
try:
# convert destination hash to bytes
destination_hash = bytes.fromhex(destination_hash)
# FIXME: can this be removed, and just rely on the router to check paths?
# find destination identity from hash
destination_identity = RNS.Identity.recall(destination_hash)
if destination_identity is None:
@@ -282,11 +358,19 @@ class ReticulumWebChat:
lxmf_destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
# create lxmf message
lxm = LXMF.LXMessage(lxmf_destination, self.local_lxmf_destination, message_content, desired_method=LXMF.LXMessage.DIRECT)
lxm.try_propagation_on_fail = True
lxmf_message = LXMF.LXMessage(lxmf_destination, self.local_lxmf_destination, message_content, desired_method=LXMF.LXMessage.DIRECT)
lxmf_message.try_propagation_on_fail = True
lxmf_message.register_delivery_callback(self.on_lxmf_sending_state_updated)
lxmf_message.register_failed_callback(self.on_lxmf_sending_failed)
# send lxmf message to be routed to destination
self.message_router.handle_outbound(lxm)
self.message_router.handle_outbound(lxmf_message)
# send outbound lxmf message to websocket (after passing to router so hash is available)
await self.websocket_broadcast(json.dumps({
"type": "lxmf_outbound_message_created",
"lxmf_message": self.convert_lxmf_message_to_dict(lxmf_message),
}))
except:
# FIXME send error to websocket?