chat ui improvements
This commit is contained in:
199
index.html
199
index.html
@@ -16,11 +16,11 @@
|
||||
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div id="app" class="h-full flex flex-col">
|
||||
<div id="app" class="h-full w-full flex flex-col">
|
||||
|
||||
<!-- header -->
|
||||
<div class="flex bg-white p-2 border-gray-300 border-b">
|
||||
<div class="max-w-xl mx-auto flex w-full">
|
||||
<div class="flex w-full">
|
||||
<div class="hidden sm:flex my-auto border border-gray-300 rounded-md w-10 h-10 mr-3 shadow">
|
||||
<div class="flex mx-auto my-auto">
|
||||
<img class="w-9 h-9" src="https://reticulum.network/gfx/reticulum_logo_512.png"/>
|
||||
@@ -61,82 +61,151 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div @click="onPeerClick(peer)" v-for="peer of Object.values(peers)" class="cursor-pointer">
|
||||
<span><{{ peer.destination_hash }}> {{ peer.app_data }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- chat items -->
|
||||
<div id="messages" class="h-full overflow-y-scroll px-3 sm:px-0">
|
||||
<div class="max-w-xl mx-auto">
|
||||
<div v-if="chatItems.length > 0" class="flex flex-col space-y-3 py-4">
|
||||
<div v-for="chatItem of chatItems">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<!-- middle -->
|
||||
<div class="flex h-full w-full px-2 space-x-2">
|
||||
|
||||
<!-- peers -->
|
||||
<div class="w-72 h-full py-2">
|
||||
<div class="border rounded-xl bg-white h-full shadow">
|
||||
<div class="flex border-b border-gray-300 text-gray-700 p-2">
|
||||
<div class="mr-2">
|
||||
<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 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>Peers Discovered ({{ Object.keys(peers).length }})</div>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div @click="onPeerClick(peer)" v-for="peer of Object.values(peers)" class="flex cursor-pointer p-2" :class="[ peer.destination_hash === selectedPeer?.destination_hash ? 'bg-gray-100' : 'bg-white hover:bg-gray-50' ]">
|
||||
<div class="my-auto mr-2">
|
||||
<img class="w-9 h-9 rounded-full" src="assets/images/user.png"/>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
<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 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>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="my-auto">{{ file_attachment.file_name }}</div>
|
||||
<div class="ml-auto 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="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-900">{{ peer.app_data || "Anonymous Peer" }}</div>
|
||||
<div class="text-gray-500 text-sm">@<{{ peer.destination_hash.substring(0, 8) }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- send message -->
|
||||
<div class="bg-white border-gray-300 border-t p-2">
|
||||
<div class="max-w-xl mx-auto">
|
||||
<!-- chat view -->
|
||||
<div class="flex flex-col flex-1">
|
||||
|
||||
<!-- peer selected -->
|
||||
<div v-if="selectedPeer" class="flex flex-col h-full my-2 border rounded-xl bg-white shadow">
|
||||
|
||||
<!-- header -->
|
||||
<div class="flex p-2 border-b border-gray-300">
|
||||
|
||||
<!-- peer info -->
|
||||
<div>
|
||||
<div>{{ selectedPeer.app_data || "Anonymous Peer" }}</div>
|
||||
<div class="text-sm">@<{{ selectedPeer.destination_hash }}></div>
|
||||
</div>
|
||||
|
||||
<!-- close button -->
|
||||
<div class="ml-auto my-auto mr-2">
|
||||
<div @click="selectedPeer = null" href="javascript:void(0)" class="cursor-pointer">
|
||||
<div class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-1 rounded-full">
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- message composer -->
|
||||
<div>
|
||||
<textarea id="message-input" :readonly="isSendingMessage" v-model="newMessageText" @keydown.enter.exact.native.prevent="onEnterPressed" @keydown.enter.shift.exact.native.prevent="onShiftEnterPressed" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" rows="3" placeholder="Send a message..."></textarea>
|
||||
<div class="flex">
|
||||
<button @click="sendMessage" type="button" class="ml-auto mt-2 my-auto inline-flex items-center gap-x-1 rounded-md bg-blue-500 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- chat items -->
|
||||
<div id="messages" class="h-full overflow-y-scroll px-3 sm:px-0">
|
||||
<div class="max-w-xl mx-auto">
|
||||
<div v-if="chatItems.length > 0" class="flex flex-col space-y-3 py-4">
|
||||
<div v-for="chatItem of chatItems">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
<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 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>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="my-auto">{{ file_attachment.file_name }}</div>
|
||||
<div class="ml-auto 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="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- send message -->
|
||||
<div class="w-full border-gray-300 border-t p-2">
|
||||
<div class=mx-auto">
|
||||
|
||||
<!-- message composer -->
|
||||
<div>
|
||||
<textarea id="message-input" :readonly="isSendingMessage" v-model="newMessageText" @keydown.enter.exact.native.prevent="onEnterPressed" @keydown.enter.shift.exact.native.prevent="onShiftEnterPressed" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" rows="3" placeholder="Send a message..."></textarea>
|
||||
<div class="flex">
|
||||
<button @click="sendMessage" type="button" class="ml-auto mt-2 my-auto inline-flex items-center gap-x-1 rounded-md bg-blue-500 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- no peer selected -->
|
||||
<div v-else class="flex flex-col mx-auto my-auto text-center leading-5">
|
||||
<div class="mx-auto mb-1">
|
||||
<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="M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h3.218a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H6.911a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-semibold">No Active Chat</div>
|
||||
<div>Select a Peer to start chatting!</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
BIN
public/assets/images/user.png
Normal file
BIN
public/assets/images/user.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
16
web.py
16
web.py
@@ -3,6 +3,8 @@
|
||||
import argparse
|
||||
import http
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
import RNS
|
||||
import LXMF
|
||||
@@ -43,7 +45,7 @@ class ReticulumWebChat:
|
||||
|
||||
# handle serving custom http paths
|
||||
async def process_request(self, path, request_headers):
|
||||
|
||||
|
||||
# serve index.html
|
||||
if path == "/":
|
||||
with open("index.html") as f:
|
||||
@@ -51,10 +53,20 @@ class ReticulumWebChat:
|
||||
return http.HTTPStatus.OK, [
|
||||
('Content-Type', 'text/html')
|
||||
], file_content.encode("utf-8")
|
||||
|
||||
# serve anything in public folder
|
||||
public_file_path = os.path.join("public", path.lstrip("/"))
|
||||
if os.path.isfile(public_file_path):
|
||||
mime_type, _ = mimetypes.guess_type(public_file_path)
|
||||
with open(public_file_path, "rb") as f:
|
||||
file_content = f.read()
|
||||
return http.HTTPStatus.OK, [
|
||||
('Content-Type', mime_type)
|
||||
], file_content
|
||||
|
||||
# by default, websocket is always served, but we only want it to be available at /ws
|
||||
# so we will return 404 for everything other than /ws
|
||||
elif path != "/ws":
|
||||
if path != "/ws":
|
||||
return http.HTTPStatus.NOT_FOUND, [
|
||||
('Content-Type', 'text/html')
|
||||
], b"Not Found"
|
||||
|
||||
Reference in New Issue
Block a user