refactor sending and receiving messages so delivery states can be shown in ui
This commit is contained in:
105
index.html
105
index.html
@@ -133,7 +133,7 @@
|
|||||||
|
|
||||||
<!-- peer info -->
|
<!-- peer info -->
|
||||||
<div>
|
<div>
|
||||||
<div>{{ selectedPeer.app_data || "Anonymous Peer" }}</div>
|
<div>{{ selectedPeer.app_data || "Unknown" }}</div>
|
||||||
<div class="text-sm">@<{{ selectedPeer.destination_hash }}></div>
|
<div class="text-sm">@<{{ selectedPeer.destination_hash }}></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -157,18 +157,13 @@
|
|||||||
<div class="max-w-xl mx-auto">
|
<div class="max-w-xl mx-auto">
|
||||||
<div v-if="selectedPeerChatItems.length > 0" class="flex flex-col space-y-3 py-4">
|
<div v-if="selectedPeerChatItems.length > 0" class="flex flex-col space-y-3 py-4">
|
||||||
<div v-for="chatItem of selectedPeerChatItems">
|
<div v-for="chatItem of selectedPeerChatItems">
|
||||||
<div class="flex space-x-2 border border-gray-300 rounded-lg shadow px-2 py-1.5 bg-white">
|
|
||||||
|
<!-- message content -->
|
||||||
|
<div class="flex space-x-2 border border-gray-300 rounded-lg shadow px-2 py-1.5 bg-white">
|
||||||
<div>
|
<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 -->
|
<!-- 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">
|
<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" />
|
<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>
|
||||||
@@ -178,15 +173,14 @@
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="font-semibold leading-5">
|
<div class="font-semibold leading-5">
|
||||||
<span v-if="chatItem.is_outbound">You</span>
|
<span v-if="chatItem.is_outbound">You</span>
|
||||||
<span v-else-if="chatItem.source_hash === 'error'">Error</span>
|
<span v-else>{{ selectedPeer.app_data || "Unknown" }}</span>
|
||||||
<span v-else>@<{{ chatItem.source_hash }}></span>
|
|
||||||
</div>
|
</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.lxmf_message.content" style="white-space:pre-wrap;word-wrap:break-word;font-family:inherit;">{{ chatItem.lxmf_message.content }}</div>
|
||||||
<div v-if="chatItem.message.fields?.image" class="grid grid-cols-3 gap-2">
|
<div v-if="chatItem.lxmf_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"/>
|
<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>
|
||||||
<div v-if="chatItem.message.fields?.file_attachments">
|
<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.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">
|
<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">
|
<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">
|
<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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- message state -->
|
||||||
|
<div v-if="chatItem.is_outbound" class="text-gray-500">{{ chatItem.lxmf_message.state }}</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,14 +300,51 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'lxmf.delivery': {
|
case 'lxmf.delivery': {
|
||||||
|
|
||||||
|
// add inbound message to ui
|
||||||
this.chatItems.push({
|
this.chatItems.push({
|
||||||
"source_hash": json.source_hash,
|
"type": "lxmf_message",
|
||||||
"message": json.message,
|
"lxmf_message": json.lxmf_message,
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// auto scroll to bottom if we want to
|
||||||
if(this.autoScrollOnNewMessage){
|
if(this.autoScrollOnNewMessage){
|
||||||
this.scrollMessagesToBottom();
|
this.scrollMessagesToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
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,
|
"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
|
// clear message input
|
||||||
this.newMessageText = "";
|
this.newMessageText = "";
|
||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|
||||||
|
// todo handle error
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
||||||
this.chatItems.push({
|
|
||||||
"source_hash": "error",
|
|
||||||
"type": "text",
|
|
||||||
"text": e.message ?? e ?? "Unknown Error...",
|
|
||||||
});
|
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
this.isSendingMessage = false;
|
this.isSendingMessage = false;
|
||||||
}
|
}
|
||||||
@@ -472,9 +492,16 @@
|
|||||||
// get all chat items related to the selected peer
|
// get all chat items related to the selected peer
|
||||||
if(this.selectedPeer){
|
if(this.selectedPeer){
|
||||||
return this.chatItems.filter((chatItem) => {
|
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"){
|
||||||
return isFromSelectedPeer || isToSelectedPeer;
|
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
104
web.py
@@ -173,7 +173,7 @@ class ReticulumWebChat:
|
|||||||
# send lxmf message to destination
|
# send lxmf message to destination
|
||||||
destination_hash = data["destination_hash"]
|
destination_hash = data["destination_hash"]
|
||||||
message = data["message"]
|
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?
|
# # TODO: send response to client when marked as delivered?
|
||||||
# await client.send(json.dumps({
|
# 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
|
# 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, message):
|
def on_lxmf_delivery(self, message):
|
||||||
@@ -249,25 +314,36 @@ class ReticulumWebChat:
|
|||||||
# send received lxmf message data to all websocket clients
|
# send received lxmf message data to all websocket clients
|
||||||
asyncio.run(self.websocket_broadcast(json.dumps({
|
asyncio.run(self.websocket_broadcast(json.dumps({
|
||||||
"type": "lxmf.delivery",
|
"type": "lxmf.delivery",
|
||||||
"source_hash": source_hash_text,
|
"lxmf_message": self.convert_lxmf_message_to_dict(message),
|
||||||
"message": {
|
|
||||||
"content": message_content,
|
|
||||||
"fields": fields,
|
|
||||||
},
|
|
||||||
})))
|
})))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# do nothing on error
|
# do nothing on error
|
||||||
print("lxmf_delivery error: {}".format(e))
|
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
|
# 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:
|
try:
|
||||||
|
|
||||||
# convert destination hash to bytes
|
# convert destination hash to bytes
|
||||||
destination_hash = bytes.fromhex(destination_hash)
|
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
|
# find destination identity from hash
|
||||||
destination_identity = RNS.Identity.recall(destination_hash)
|
destination_identity = RNS.Identity.recall(destination_hash)
|
||||||
if destination_identity is None:
|
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")
|
lxmf_destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery")
|
||||||
|
|
||||||
# create lxmf message
|
# create lxmf message
|
||||||
lxm = LXMF.LXMessage(lxmf_destination, self.local_lxmf_destination, message_content, desired_method=LXMF.LXMessage.DIRECT)
|
lxmf_message = LXMF.LXMessage(lxmf_destination, self.local_lxmf_destination, message_content, desired_method=LXMF.LXMessage.DIRECT)
|
||||||
lxm.try_propagation_on_fail = True
|
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
|
# 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:
|
except:
|
||||||
# FIXME send error to websocket?
|
# FIXME send error to websocket?
|
||||||
|
|||||||
Reference in New Issue
Block a user