516 lines
25 KiB
HTML
516 lines
25 KiB
HTML
<html>
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
<title>Reticulum WebChat</title>
|
|
|
|
<!-- tailwind css -->
|
|
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
|
|
|
<!-- scripts -->
|
|
<script src="https://unpkg.com/vue@3"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.min.js"></script>
|
|
|
|
</head>
|
|
<body class="bg-gray-100">
|
|
<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="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"/>
|
|
</div>
|
|
</div>
|
|
<div class="my-auto">
|
|
<div class="font-bold">Reticulum WebChat</div>
|
|
<div class="text-sm">Developed by <a target="_blank" href="https://liamcottle.com" class="text-blue-500">Liam Cottle</a></div>
|
|
</div>
|
|
<div class="flex my-auto ml-auto mr-0 sm:mr-2 space-x-1 sm:space-x-2">
|
|
<div class="rounded-full">
|
|
<div class="flex text-gray-700 bg-gray-100 px-2 py-1 rounded-full">
|
|
<div>
|
|
<svg v-if="isWebsocketConnected" 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="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
</svg>
|
|
<svg v-else 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="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
</svg>
|
|
</div>
|
|
<div class="my-auto mx-1 text-sm">
|
|
<span v-if="isWebsocketConnected">Connected</span>
|
|
<span v-else>Disconnected</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<a @click="sendAnnounce" href="javascript:void(0)" class="rounded-full">
|
|
<div class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded-full">
|
|
<div>
|
|
<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="M8.288 15.038a5.25 5.25 0 0 1 7.424 0M5.106 11.856c3.807-3.808 9.98-3.808 13.788 0M1.924 8.674c5.565-5.565 14.587-5.565 20.152 0M12.53 18.22l-.53.53-.53-.53a.75.75 0 0 1 1.06 0Z" />
|
|
</svg>
|
|
</div>
|
|
<div class="my-auto mx-1 text-sm">Announce</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- middle -->
|
|
<div class="flex h-full w-full px-2 space-x-2">
|
|
|
|
<!-- sidebar -->
|
|
<div class="flex flex-col w-80 h-full py-2 space-y-2">
|
|
|
|
<!-- peers -->
|
|
<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>
|
|
<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>
|
|
|
|
<!-- my identity -->
|
|
<div v-if="config" class="border rounded-xl bg-white shadow">
|
|
<div class="flex border-b border-gray-300 text-gray-700 p-2">
|
|
<div class="my-auto 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="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
|
</svg>
|
|
</div>
|
|
<div class="my-auto">My Identity</div>
|
|
<div class="ml-auto">
|
|
<button @click="saveDisplayName" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
|
Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="divide-y text-gray-900">
|
|
<div class="p-1">
|
|
<input v-model="displayName" type="text" 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">
|
|
</div>
|
|
<div class="p-1">
|
|
<div>Identity Hash</div>
|
|
<div class="text-sm text-gray-700">{{ config.identity_hash }}</div>
|
|
</div>
|
|
<div class="p-1">
|
|
<div>LXMF Address Hash</div>
|
|
<div class="text-sm text-gray-700">{{ config.lxmf_address_hash }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- 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 || "Unknown" }}</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>
|
|
|
|
</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="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>
|
|
|
|
<!-- user -->
|
|
<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>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="w-full">
|
|
<div class="font-semibold leading-5">
|
|
<span v-if="chatItem.is_outbound">You</span>
|
|
<span v-else>{{ selectedPeer.app_data || "Unknown" }}</span>
|
|
</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.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.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>
|
|
</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>
|
|
|
|
<!-- message state -->
|
|
<div v-if="chatItem.is_outbound" class="text-gray-500">{{ chatItem.lxmf_message.state }}</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>
|
|
<script>
|
|
Vue.createApp({
|
|
data() {
|
|
return {
|
|
|
|
isWebsocketConnected: false,
|
|
autoReconnectWebsocket: true,
|
|
|
|
newMessageText: "",
|
|
isSendingMessage: false,
|
|
autoScrollOnNewMessage: true,
|
|
|
|
displayName: "Anonymous Peer",
|
|
config: null,
|
|
|
|
peers: {},
|
|
selectedPeer: null,
|
|
chatItems: [],
|
|
|
|
};
|
|
},
|
|
mounted: function() {
|
|
this.connectWebsocket();
|
|
},
|
|
methods: {
|
|
connectWebsocket: function() {
|
|
|
|
// connect to websocket
|
|
this.ws = new WebSocket(location.origin.replace(/^http/, 'ws') + "/ws");
|
|
|
|
this.ws.addEventListener('open', () => {
|
|
this.isWebsocketConnected = true;
|
|
});
|
|
|
|
this.ws.addEventListener('close', () => {
|
|
this.isWebsocketConnected = false;
|
|
if(this.autoReconnectWebsocket){
|
|
setTimeout(() => {
|
|
this.connectWebsocket();
|
|
}, 1000);
|
|
}
|
|
});
|
|
|
|
// handle data from reticulum
|
|
this.ws.onmessage = (message) => {
|
|
const json = JSON.parse(message.data);
|
|
switch(json.type){
|
|
case 'config': {
|
|
this.config = json.config;
|
|
this.displayName = json.config.display_name;
|
|
break;
|
|
}
|
|
case 'announce': {
|
|
this.peers[json.destination_hash] = {
|
|
destination_hash: json.destination_hash,
|
|
app_data: json.app_data,
|
|
}
|
|
break;
|
|
}
|
|
case 'lxmf.delivery': {
|
|
|
|
// add inbound message to ui
|
|
this.chatItems.push({
|
|
"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;
|
|
|
|
}
|
|
}
|
|
};
|
|
|
|
},
|
|
disconnectWebsocket: function() {
|
|
if(this.ws){
|
|
this.ws.close();
|
|
}
|
|
},
|
|
scrollMessagesToBottom: function() {
|
|
Vue.nextTick(() => {
|
|
const container = document.getElementById("messages");
|
|
container.scrollTop = container.scrollHeight;
|
|
});
|
|
},
|
|
async sendAnnounce() {
|
|
|
|
// do nothing if not connected to websocket
|
|
if(!this.isWebsocketConnected){
|
|
alert("Not connected to WebSocket!");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
|
|
// ask reticulum to announce
|
|
this.ws.send(JSON.stringify({
|
|
"type": "announce",
|
|
}));
|
|
|
|
} catch(e) {
|
|
console.error(e);
|
|
}
|
|
|
|
},
|
|
async sendMessage() {
|
|
|
|
// do nothing if empty message
|
|
const messageText = this.newMessageText.trim();
|
|
if(messageText == null || messageText === ""){
|
|
return;
|
|
}
|
|
|
|
// do nothing if not connected to websocket
|
|
if(!this.isWebsocketConnected){
|
|
alert("Not connected to WebSocket!");
|
|
return;
|
|
}
|
|
|
|
// do nothing if no peer selected
|
|
if(!this.selectedPeer){
|
|
return;
|
|
}
|
|
|
|
this.isSendingMessage = true;
|
|
|
|
try {
|
|
|
|
// send message to reticulum via websocket
|
|
this.ws.send(JSON.stringify({
|
|
"type": "lxmf.delivery",
|
|
"destination_hash": this.selectedPeer.destination_hash,
|
|
"message": messageText,
|
|
}));
|
|
|
|
// clear message input
|
|
this.newMessageText = "";
|
|
|
|
} catch(e) {
|
|
|
|
// todo handle error
|
|
console.error(e);
|
|
|
|
} finally {
|
|
this.isSendingMessage = false;
|
|
}
|
|
|
|
// scroll to bottom
|
|
this.scrollMessagesToBottom();
|
|
|
|
},
|
|
async updateConfig(config) {
|
|
|
|
// do nothing if not connected to websocket
|
|
if(!this.isWebsocketConnected){
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.ws.send(JSON.stringify({
|
|
"type": "config.set",
|
|
"config": config,
|
|
}));
|
|
} catch(e) {
|
|
console.error(e);
|
|
}
|
|
|
|
},
|
|
async saveDisplayName() {
|
|
await this.updateConfig({
|
|
"display_name": this.displayName,
|
|
});
|
|
},
|
|
addNewLine: function() {
|
|
this.newMessageText += "\n";
|
|
},
|
|
onEnterPressed: function() {
|
|
|
|
// add new line on mobile
|
|
if(this.isMobile){
|
|
this.addNewLine();
|
|
return;
|
|
}
|
|
|
|
// send message on desktop
|
|
this.sendMessage();
|
|
|
|
},
|
|
onShiftEnterPressed: function() {
|
|
this.addNewLine();
|
|
},
|
|
openImage: async function(url) {
|
|
|
|
// convert data uri to blob
|
|
const blob = await (await fetch(url)).blob();
|
|
|
|
// create blob url
|
|
const fileUrl = window.URL.createObjectURL(blob);
|
|
|
|
// open new tab
|
|
window.open(fileUrl);
|
|
|
|
},
|
|
onPeerClick: function(peer) {
|
|
this.selectedPeer = peer;
|
|
},
|
|
},
|
|
computed: {
|
|
isMobile() {
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
},
|
|
selectedPeerChatItems() {
|
|
|
|
// get all chat items related to the selected peer
|
|
if(this.selectedPeer){
|
|
return this.chatItems.filter((chatItem) => {
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
}
|
|
|
|
// no peer, so no chat items!
|
|
return [];
|
|
|
|
}
|
|
},
|
|
}).mount('#app');
|
|
</script>
|
|
</body>
|
|
</html> |