mirror of
https://github.com/liamcottle/reticulum-meshchat.git
synced 2026-04-28 00:20:48 +00:00
use router for messages page
This commit is contained in:
parent
a91964a463
commit
165e08131f
6 changed files with 240 additions and 214 deletions
|
|
@ -41,15 +41,17 @@
|
|||
|
||||
<!-- messages -->
|
||||
<li>
|
||||
<button @click="tab = 'messages'" type="button" :class="[ tab === 'messages' ? 'bg-blue-100 text-blue-800 group:text-blue-800 hover:bg-blue-100' : '']" class="w-full text-gray-800 hover:bg-gray-100 group flex gap-x-3 rounded-r-full p-2 mr-2 text-sm leading-6 font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
|
||||
<span class="my-auto">
|
||||
<SidebarLink :to="{ name: 'messages' }">
|
||||
<template v-slot:icon>
|
||||
<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="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="my-auto">Messages</span>
|
||||
<span v-if="unreadConversationsCount > 0" class="my-auto ml-auto mr-2">{{ unreadConversationsCount }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-slot:text>
|
||||
<span>Messages</span>
|
||||
<span v-if="unreadConversationsCount > 0" class="ml-auto mr-2">{{ unreadConversationsCount }}</span>
|
||||
</template>
|
||||
</SidebarLink>
|
||||
</li>
|
||||
|
||||
<!-- nomad network -->
|
||||
|
|
@ -243,30 +245,6 @@
|
|||
|
||||
<RouterView/>
|
||||
|
||||
<!-- <!– messages sidebar –>-->
|
||||
<!-- <MessagesSidebar-->
|
||||
<!-- v-if="tab === 'messages'"-->
|
||||
<!-- :conversations="conversations"-->
|
||||
<!-- :peers="peers"-->
|
||||
<!-- :selected-destination-hash="selectedPeer?.destination_hash"-->
|
||||
<!-- @conversation-click="onConversationClick"-->
|
||||
<!-- @peer-click="onPeerClick"/>-->
|
||||
|
||||
<!-- <!– main view –>-->
|
||||
<!-- <div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px]">-->
|
||||
|
||||
<!-- <!– messages tab –>-->
|
||||
<!-- <ConversationViewer-->
|
||||
<!-- v-if="tab === 'messages'"-->
|
||||
<!-- ref="conversation-viewer"-->
|
||||
<!-- :my-lxmf-address-hash="config?.lxmf_address_hash"-->
|
||||
<!-- :selected-peer="selectedPeer"-->
|
||||
<!-- :conversations="conversations"-->
|
||||
<!-- @close="selectedPeer = null"-->
|
||||
<!-- @reload-conversations="getConversations"/>-->
|
||||
|
||||
<!-- </div>-->
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -274,17 +252,13 @@
|
|||
|
||||
<script>
|
||||
import SidebarLink from "./SidebarLink.vue";
|
||||
import MessagesSidebar from "./messages/MessagesSidebar.vue";
|
||||
import ConversationViewer from "./messages/ConversationViewer.vue";
|
||||
import DialogUtils from "../js/DialogUtils";
|
||||
import Utils from "../js/Utils";
|
||||
import WebSocketConnection from "../js/WebSocketConnection";
|
||||
import GlobalState from "../js/GlobalState";
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
ConversationViewer,
|
||||
MessagesSidebar,
|
||||
SidebarLink,
|
||||
},
|
||||
data() {
|
||||
|
|
@ -299,14 +273,6 @@ export default {
|
|||
appInfo: null,
|
||||
|
||||
audioCalls: [],
|
||||
lxmfDeliveryAnnounces: [],
|
||||
|
||||
tab: "messages",
|
||||
|
||||
peers: {},
|
||||
selectedPeer: null,
|
||||
|
||||
conversations: [],
|
||||
|
||||
};
|
||||
},
|
||||
|
|
@ -321,13 +287,6 @@ export default {
|
|||
mounted() {
|
||||
|
||||
this.getAppInfo();
|
||||
this.getLxmfDeliveryAnnounces();
|
||||
this.getConversations();
|
||||
|
||||
// fixme: this is called by the micron-parser.js
|
||||
window.onNodePageUrlClick = (url) => {
|
||||
this.onNodePageUrlClick(url);
|
||||
};
|
||||
|
||||
// update calls list
|
||||
this.updateCallsList();
|
||||
|
|
@ -335,7 +294,6 @@ export default {
|
|||
// update info every few seconds
|
||||
setInterval(() => {
|
||||
this.updateCallsList();
|
||||
this.getConversations();
|
||||
}, 3000);
|
||||
|
||||
},
|
||||
|
|
@ -348,13 +306,6 @@ export default {
|
|||
this.displayName = json.config.display_name;
|
||||
break;
|
||||
}
|
||||
case 'announce': {
|
||||
const aspect = json.announce.aspect;
|
||||
if(aspect === "lxmf.delivery"){
|
||||
this.updatePeerFromAnnounce(json.announce);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'announced': {
|
||||
// we just announced, update config so we can show the new last updated at
|
||||
this.getConfig();
|
||||
|
|
@ -371,50 +322,6 @@ export default {
|
|||
});
|
||||
break;
|
||||
}
|
||||
case 'lxmf.delivery': {
|
||||
|
||||
// pass lxmf message to conversation viewer
|
||||
const conversationViewer = this.$refs["conversation-viewer"];
|
||||
if(conversationViewer){
|
||||
conversationViewer.onLxmfMessageReceived(json.lxmf_message);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
case 'lxmf_message_created': {
|
||||
|
||||
// pass lxmf message to conversation viewer
|
||||
const conversationViewer = this.$refs["conversation-viewer"];
|
||||
if(conversationViewer){
|
||||
conversationViewer.onLxmfMessageCreated(json.lxmf_message);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
case 'lxmf_message_state_updated': {
|
||||
|
||||
// pass lxmf message to conversation viewer
|
||||
const conversationViewer = this.$refs["conversation-viewer"];
|
||||
if(conversationViewer){
|
||||
conversationViewer.onLxmfMessageUpdated(json.lxmf_message);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
case 'lxmf_message_deleted': {
|
||||
|
||||
// pass lxmf message hash to conversation viewer
|
||||
const conversationViewer = this.$refs["conversation-viewer"];
|
||||
if(conversationViewer){
|
||||
conversationViewer.onLxmfMessageDeleted(json.hash);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
async getAppInfo() {
|
||||
|
|
@ -481,6 +388,8 @@ export default {
|
|||
},
|
||||
openLXMFConversation(destinationHash) {
|
||||
|
||||
// fixme: reimplement
|
||||
|
||||
// attempt to find existing peer so we can show their name
|
||||
const existingPeer = this.peers[destinationHash];
|
||||
if(existingPeer){
|
||||
|
|
@ -500,95 +409,6 @@ export default {
|
|||
destination_hash: destinationHash,
|
||||
});
|
||||
|
||||
},
|
||||
async getLxmfDeliveryAnnounces() {
|
||||
try {
|
||||
|
||||
// fetch announces for "lxmf.delivery" aspect
|
||||
const response = await window.axios.get(`/api/v1/announces`, {
|
||||
params: {
|
||||
aspect: "lxmf.delivery",
|
||||
},
|
||||
});
|
||||
|
||||
// update ui
|
||||
const lxmfDeliveryAnnounces = response.data.announces;
|
||||
for(const lxmfDeliveryAnnounce of lxmfDeliveryAnnounces){
|
||||
this.updatePeerFromAnnounce(lxmfDeliveryAnnounce);
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// do nothing if failed to load announces
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
async getConversations() {
|
||||
try {
|
||||
const response = await window.axios.get(`/api/v1/lxmf/conversations`);
|
||||
this.conversations = response.data.conversations;
|
||||
} catch(e) {
|
||||
// do nothing if failed to load conversations
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
getPeerNameFromAppData: function(appData) {
|
||||
try {
|
||||
// app data should be peer name, and our server provides it base64 encoded
|
||||
return Utils.decodeBase64ToUtf8String(appData);
|
||||
} catch(e){
|
||||
return "Anonymous Peer";
|
||||
}
|
||||
},
|
||||
updatePeerFromAnnounce: function(announce) {
|
||||
this.peers[announce.destination_hash] = {
|
||||
...announce,
|
||||
// helper property for easily grabbing peer name from app data
|
||||
name: this.getPeerNameFromAppData(announce.app_data),
|
||||
};
|
||||
},
|
||||
downloadFileFromBase64: async function(fileName, fileBytesBase64) {
|
||||
|
||||
// create blob from base64 encoded file bytes
|
||||
const byteCharacters = atob(fileBytesBase64);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for(let i = 0; i < byteCharacters.length; i++){
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray]);
|
||||
|
||||
// create object url for blob
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
|
||||
// create link element to download blob
|
||||
const link = document.createElement('a');
|
||||
link.href = objectUrl;
|
||||
link.download = fileName;
|
||||
link.style.display = "none";
|
||||
document.body.append(link);
|
||||
|
||||
// click link to download file in browser
|
||||
link.click();
|
||||
|
||||
// link element is no longer needed
|
||||
link.remove();
|
||||
|
||||
// revoke object url to clear memory
|
||||
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
|
||||
|
||||
},
|
||||
onPeerClick: function(peer) {
|
||||
this.selectedPeer = peer;
|
||||
this.tab = "messages";
|
||||
},
|
||||
onConversationClick: function(conversation) {
|
||||
|
||||
// object must stay compatible with format of peers
|
||||
this.onPeerClick(conversation);
|
||||
|
||||
// mark conversation as read
|
||||
this.$refs["conversation-viewer"].markConversationAsRead(conversation);
|
||||
|
||||
},
|
||||
parseSeconds: function(secondsToFormat) {
|
||||
secondsToFormat = Number(secondsToFormat);
|
||||
|
|
@ -647,25 +467,6 @@ export default {
|
|||
}
|
||||
|
||||
},
|
||||
|
||||
formatBytes: function(bytes) {
|
||||
|
||||
if(bytes === 0){
|
||||
return '0 Bytes';
|
||||
}
|
||||
|
||||
const k = 1024;
|
||||
const decimals = 0;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
|
||||
},
|
||||
onDestinationPathClick: function(path) {
|
||||
DialogUtils.alert(`${path.hops} ${ path.hops === 1 ? 'hop' : 'hops' } away via ${path.next_hop_interface}`);
|
||||
},
|
||||
async updateCallsList() {
|
||||
try {
|
||||
|
||||
|
|
@ -717,9 +518,7 @@ export default {
|
|||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
},
|
||||
unreadConversationsCount() {
|
||||
return this.conversations.filter((conversation) => {
|
||||
return conversation.is_unread;
|
||||
}).length;
|
||||
return GlobalState.unreadConversationsCount;
|
||||
},
|
||||
activeAudioCalls() {
|
||||
return this.audioCalls.filter(function(audioCall) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<span class="my-auto">
|
||||
<slot name="icon"></slot>
|
||||
</span>
|
||||
<span class="my-auto">
|
||||
<span class="my-auto flex w-full">
|
||||
<slot name="text"></slot>
|
||||
</span>
|
||||
</a>
|
||||
|
|
|
|||
210
src/frontend/components/messages/MessagesPage.vue
Normal file
210
src/frontend/components/messages/MessagesPage.vue
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
<template>
|
||||
|
||||
<MessagesSidebar
|
||||
:conversations="conversations"
|
||||
:peers="peers"
|
||||
:selected-destination-hash="selectedPeer?.destination_hash"
|
||||
@conversation-click="onConversationClick"
|
||||
@peer-click="onPeerClick"/>
|
||||
|
||||
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px]">
|
||||
|
||||
<!-- messages tab -->
|
||||
<ConversationViewer
|
||||
ref="conversation-viewer"
|
||||
:my-lxmf-address-hash="config?.lxmf_address_hash"
|
||||
:selected-peer="selectedPeer"
|
||||
:conversations="conversations"
|
||||
@close="selectedPeer = null"
|
||||
@reload-conversations="getConversations"/>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||
import MessagesSidebar from "./MessagesSidebar.vue";
|
||||
import ConversationViewer from "./ConversationViewer.vue";
|
||||
import Utils from "../../js/Utils";
|
||||
import GlobalState from "../../js/GlobalState";
|
||||
|
||||
export default {
|
||||
name: 'MessagesPage',
|
||||
components: {
|
||||
ConversationViewer,
|
||||
MessagesSidebar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
config: null,
|
||||
peers: {},
|
||||
selectedPeer: null,
|
||||
|
||||
conversations: [],
|
||||
lxmfDeliveryAnnounces: [],
|
||||
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// listen for websocket messages
|
||||
WebSocketConnection.on("message", this.onWebsocketMessage);
|
||||
},
|
||||
beforeDestroy() {
|
||||
// stop listening for websocket messages
|
||||
WebSocketConnection.off("message", this.onWebsocketMessage);
|
||||
},
|
||||
mounted() {
|
||||
|
||||
this.getConfig();
|
||||
this.getConversations();
|
||||
this.getLxmfDeliveryAnnounces();
|
||||
|
||||
// update info every few seconds
|
||||
setInterval(() => {
|
||||
this.getConversations();
|
||||
}, 3000);
|
||||
|
||||
},
|
||||
methods: {
|
||||
async getConfig() {
|
||||
try {
|
||||
const response = await window.axios.get(`/api/v1/config`);
|
||||
this.config = response.data.config;
|
||||
} catch(e) {
|
||||
// do nothing if failed to load config
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
async onWebsocketMessage(message) {
|
||||
const json = JSON.parse(message.data);
|
||||
switch(json.type){
|
||||
case 'config': {
|
||||
this.config = json.config;
|
||||
break;
|
||||
}
|
||||
case 'announce': {
|
||||
const aspect = json.announce.aspect;
|
||||
if(aspect === "lxmf.delivery"){
|
||||
this.updatePeerFromAnnounce(json.announce);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'lxmf.delivery': {
|
||||
|
||||
// pass lxmf message to conversation viewer
|
||||
const conversationViewer = this.$refs["conversation-viewer"];
|
||||
if(conversationViewer){
|
||||
conversationViewer.onLxmfMessageReceived(json.lxmf_message);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
case 'lxmf_message_created': {
|
||||
|
||||
// pass lxmf message to conversation viewer
|
||||
const conversationViewer = this.$refs["conversation-viewer"];
|
||||
if(conversationViewer){
|
||||
conversationViewer.onLxmfMessageCreated(json.lxmf_message);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
case 'lxmf_message_state_updated': {
|
||||
|
||||
// pass lxmf message to conversation viewer
|
||||
const conversationViewer = this.$refs["conversation-viewer"];
|
||||
if(conversationViewer){
|
||||
conversationViewer.onLxmfMessageUpdated(json.lxmf_message);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
case 'lxmf_message_deleted': {
|
||||
|
||||
// pass lxmf message hash to conversation viewer
|
||||
const conversationViewer = this.$refs["conversation-viewer"];
|
||||
if(conversationViewer){
|
||||
conversationViewer.onLxmfMessageDeleted(json.hash);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
async getLxmfDeliveryAnnounces() {
|
||||
try {
|
||||
|
||||
// fetch announces for "lxmf.delivery" aspect
|
||||
const response = await window.axios.get(`/api/v1/announces`, {
|
||||
params: {
|
||||
aspect: "lxmf.delivery",
|
||||
},
|
||||
});
|
||||
|
||||
// update ui
|
||||
const lxmfDeliveryAnnounces = response.data.announces;
|
||||
for(const lxmfDeliveryAnnounce of lxmfDeliveryAnnounces){
|
||||
this.updatePeerFromAnnounce(lxmfDeliveryAnnounce);
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
// do nothing if failed to load announces
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
async getConversations() {
|
||||
try {
|
||||
const response = await window.axios.get(`/api/v1/lxmf/conversations`);
|
||||
this.conversations = response.data.conversations;
|
||||
} catch(e) {
|
||||
// do nothing if failed to load conversations
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
getPeerNameFromAppData: function(appData) {
|
||||
try {
|
||||
// app data should be peer name, and our server provides it base64 encoded
|
||||
return Utils.decodeBase64ToUtf8String(appData);
|
||||
} catch(e){
|
||||
return "Anonymous Peer";
|
||||
}
|
||||
},
|
||||
updatePeerFromAnnounce: function(announce) {
|
||||
this.peers[announce.destination_hash] = {
|
||||
...announce,
|
||||
// helper property for easily grabbing peer name from app data
|
||||
name: this.getPeerNameFromAppData(announce.app_data),
|
||||
};
|
||||
},
|
||||
onPeerClick: function(peer) {
|
||||
this.selectedPeer = peer;
|
||||
},
|
||||
onConversationClick: function(conversation) {
|
||||
|
||||
// object must stay compatible with format of peers
|
||||
this.onPeerClick(conversation);
|
||||
|
||||
// mark conversation as read
|
||||
this.$refs["conversation-viewer"].markConversationAsRead(conversation);
|
||||
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
conversations() {
|
||||
|
||||
// update global state
|
||||
GlobalState.unreadConversationsCount = this.conversations.filter((conversation) => {
|
||||
return conversation.is_unread;
|
||||
}).length;
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -150,7 +150,14 @@ export default {
|
|||
WebSocketConnection.off("message", this.onWebsocketMessage);
|
||||
},
|
||||
mounted() {
|
||||
|
||||
// fixme: this is called by the micron-parser.js
|
||||
window.onNodePageUrlClick = (url) => {
|
||||
this.onNodePageUrlClick(url);
|
||||
};
|
||||
|
||||
this.getNomadnetworkNodeAnnounces();
|
||||
|
||||
},
|
||||
methods: {
|
||||
async onWebsocketMessage(message) {
|
||||
|
|
|
|||
8
src/frontend/js/GlobalState.js
Normal file
8
src/frontend/js/GlobalState.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { reactive } from "vue";
|
||||
|
||||
// global state
|
||||
const globalState = reactive({
|
||||
unreadConversationsCount: 0,
|
||||
});
|
||||
|
||||
export default globalState;
|
||||
|
|
@ -7,12 +7,14 @@ import SettingsPage from "./components/settings/SettingsPage.vue";
|
|||
import NetworkVisualiserPage from "./components/network/NetworkVisualiserPage.vue";
|
||||
import InterfacesPage from "./components/interfaces/InterfacesPage.vue";
|
||||
import NomadNetworkPage from "./components/nomadnetwork/NomadNetworkPage.vue";
|
||||
import MessagesPage from "./components/messages/MessagesPage.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{ path: '/' },
|
||||
{ path: '/about', name: "about", component: AboutPage },
|
||||
{ path: '/messages', name: "messages", component: MessagesPage },
|
||||
{ path: '/nomadnetwork', name: "nomadnetwork", component: NomadNetworkPage },
|
||||
{ path: '/settings', name: "settings", component: SettingsPage },
|
||||
{ path: '/interfaces', name: "interfaces", component: InterfacesPage },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue