mirror of
https://github.com/aljazceru/meshcore-web.git
synced 2025-12-17 16:24:18 +01:00
implement channel messages
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@liamcottle/meshcore.js": "^1.0.0",
|
"@liamcottle/meshcore.js": "^1.0.3",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
@@ -1116,9 +1116,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@liamcottle/meshcore.js": {
|
"node_modules/@liamcottle/meshcore.js": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@liamcottle/meshcore.js/-/meshcore.js-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@liamcottle/meshcore.js/-/meshcore.js-1.0.3.tgz",
|
||||||
"integrity": "sha512-N4cVn9V+2CP87o24z/Nv32KDcgs1yBWd59zp5qbidOFLg8848JRecKmu9/6BERrQ9QZIOROrDUE7LtxfhFBjKA=="
|
"integrity": "sha512-i3AZt2xjSi7kTu86ou+t93K2IPyr7r56ZUZa2A3FRGWzumQ/OxXt30sCox5W6UUcMSLKGzEkMviiTRj7JaDWKw=="
|
||||||
},
|
},
|
||||||
"node_modules/@mongodb-js/saslprep": {
|
"node_modules/@mongodb-js/saslprep": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"author": "Liam Cottle <liam@liamcottle.com>",
|
"author": "Liam Cottle <liam@liamcottle.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@liamcottle/meshcore.js": "^1.0.0",
|
"@liamcottle/meshcore.js": "^1.0.3",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
|||||||
20
src/components/channels/ChannelListItem.vue
Normal file
20
src/components/channels/ChannelListItem.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex cursor-pointer p-2 bg-white hover:bg-gray-50">
|
||||||
|
|
||||||
|
<!-- name and info -->
|
||||||
|
<div class="mr-auto">
|
||||||
|
<div>{{ channel.name }}</div>
|
||||||
|
<div class="text-sm text-gray-500">{{ channel.description }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ChannelListItem',
|
||||||
|
props: {
|
||||||
|
channel: Object,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
32
src/components/channels/ChannelsList.vue
Normal file
32
src/components/channels/ChannelsList.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full w-full overflow-hidden">
|
||||||
|
|
||||||
|
<!-- channels -->
|
||||||
|
<div class="h-full overflow-y-auto">
|
||||||
|
<ChannelListItem :key="channel.idx" v-for="channel of channels" :channel="channel" @click="onChannelClick(channel)"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ChannelListItem from "./ChannelListItem.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChannelsList',
|
||||||
|
components: {
|
||||||
|
ChannelListItem,
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
"channel-click",
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
channels: Array,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChannelClick(channel) {
|
||||||
|
this.$emit("channel-click", channel);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -22,13 +22,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- timestamp -->
|
<!-- inbound timestamp -->
|
||||||
<div v-if="isMessageInbound(message)" class="text-xs text-gray-500">
|
<div v-if="isMessageInbound(message)" class="text-xs text-gray-500">
|
||||||
<span>{{ formatMessageTimestamp(message.timestamp) }}</span>
|
<span>{{ formatMessageTimestamp(message.timestamp) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- outbound timestamp -->
|
||||||
|
<div v-if="isMessageOutbound(message) && type === 'channel'" class="ml-auto text-xs text-gray-500">
|
||||||
|
<span>{{ formatMessageTimestamp(message.timestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- message state -->
|
<!-- message state -->
|
||||||
<div v-if="isMessageOutbound(message)" class="flex text-right" :class="[ isMessageFailed(message) ? 'text-red-500' : 'text-gray-500' ]">
|
<div v-if="isMessageOutbound(message) && type === 'contact'" class="flex text-right" :class="[ isMessageFailed(message) ? 'text-red-500' : 'text-gray-500' ]">
|
||||||
<div class="flex ml-auto space-x-1">
|
<div class="flex ml-auto space-x-1">
|
||||||
|
|
||||||
<!-- state label -->
|
<!-- state label -->
|
||||||
@@ -108,7 +113,9 @@ import Utils from "../../js/Utils.js";
|
|||||||
export default {
|
export default {
|
||||||
name: 'MessageViewer',
|
name: 'MessageViewer',
|
||||||
props: {
|
props: {
|
||||||
|
type: String,
|
||||||
contact: Object,
|
contact: Object,
|
||||||
|
channel: Object,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -125,7 +132,11 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
|
|
||||||
// init database subscription for messages
|
// init database subscription for messages
|
||||||
|
if(this.type === "contact"){
|
||||||
this.messagesSubscription = Database.Message.getContactMessages(this.contact.publicKey).$.subscribe(this.onMessagesUpdated);
|
this.messagesSubscription = Database.Message.getContactMessages(this.contact.publicKey).$.subscribe(this.onMessagesUpdated);
|
||||||
|
} else if(this.type === "channel") {
|
||||||
|
this.messagesSubscription = Database.ChannelMessage.getChannelMessages(this.channel.idx).$.subscribe(this.onMessagesUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
// update read state
|
// update read state
|
||||||
this.updateMessagesLastReadAt();
|
this.updateMessagesLastReadAt();
|
||||||
@@ -161,8 +172,11 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// send message
|
if(this.type === "contact"){
|
||||||
await Connection.sendMessage(this.contact.publicKey, newMessageText);
|
await Connection.sendMessage(this.contact.publicKey, newMessageText);
|
||||||
|
} else if(this.type === "channel") {
|
||||||
|
await Connection.sendChannelMessage(this.channel.idx, newMessageText);
|
||||||
|
}
|
||||||
|
|
||||||
// clear new message input
|
// clear new message input
|
||||||
this.newMessageText = "";
|
this.newMessageText = "";
|
||||||
@@ -248,7 +262,9 @@ export default {
|
|||||||
updateMessagesLastReadAt() {
|
updateMessagesLastReadAt() {
|
||||||
|
|
||||||
// update last read at for contact messages
|
// update last read at for contact messages
|
||||||
|
if(this.type === "contact"){
|
||||||
Database.ContactMessagesReadState.touch(this.contact.publicKey);
|
Database.ContactMessagesReadState.touch(this.contact.publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
formatMessageTimestamp(timestamp) {
|
formatMessageTimestamp(timestamp) {
|
||||||
@@ -259,7 +275,12 @@ export default {
|
|||||||
canSendMessage() {
|
canSendMessage() {
|
||||||
|
|
||||||
// can't send if contact is not selected
|
// can't send if contact is not selected
|
||||||
if(this.contact == null){
|
if(this.type === 'contact' && this.contact == null){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// can't send if channel is not selected
|
||||||
|
if(this.type === 'channel' && this.channel == null){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
54
src/components/pages/ChannelMessagesPage.vue
Normal file
54
src/components/pages/ChannelMessagesPage.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<Page>
|
||||||
|
|
||||||
|
<!-- app bar -->
|
||||||
|
<AppBar title="Channel Messages" :subtitle="subtitle"/>
|
||||||
|
|
||||||
|
<!-- list -->
|
||||||
|
<div class="flex h-full w-full overflow-hidden">
|
||||||
|
<MessageViewer v-if="channel != null" :type="'channel'" :channel="channel"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Page from "./Page.vue";
|
||||||
|
import AppBar from "../AppBar.vue";
|
||||||
|
import MessageViewer from "../messages/MessageViewer.vue";
|
||||||
|
import GlobalState from "../../js/GlobalState.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChannelMessagesPage',
|
||||||
|
components: {
|
||||||
|
MessageViewer,
|
||||||
|
AppBar,
|
||||||
|
Page,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
channelIdx: String,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
// redirect to main page if channel not found
|
||||||
|
if(!this.channel){
|
||||||
|
this.$router.push({
|
||||||
|
name: "main",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
GlobalState() {
|
||||||
|
return GlobalState;
|
||||||
|
},
|
||||||
|
channel() {
|
||||||
|
return GlobalState.channels.find((channel) => channel.idx.toString() === this.channelIdx.toString());
|
||||||
|
},
|
||||||
|
subtitle() {
|
||||||
|
return this.channel ? this.channel.name : "Unknown Channel";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<!-- list -->
|
<!-- list -->
|
||||||
<div class="flex h-full w-full overflow-hidden">
|
<div class="flex h-full w-full overflow-hidden">
|
||||||
<MessageViewer v-if="contact != null" :contact="contact"/>
|
<MessageViewer v-if="contact != null" :type="'contact'" :contact="contact"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -4,9 +4,18 @@
|
|||||||
<!-- header -->
|
<!-- header -->
|
||||||
<Header/>
|
<Header/>
|
||||||
|
|
||||||
|
<!-- tabs -->
|
||||||
|
<div v-if="GlobalState.selfInfo" class="bg-white border-b border-gray-200">
|
||||||
|
<div class="-mb-px flex">
|
||||||
|
<div @click="tab = 'contacts'" class="w-full border-b-2 py-3 px-1 text-center text-sm font-medium cursor-pointer" :class="[ tab === 'contacts' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700']">Contacts</div>
|
||||||
|
<div @click="tab = 'channels'" class="w-full border-b-2 py-3 px-1 text-center text-sm font-medium cursor-pointer" :class="[ tab === 'channels' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700']">Channels</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- tab content -->
|
<!-- tab content -->
|
||||||
<div v-if="contacts.length > 0" class="flex h-full w-full overflow-hidden">
|
<div v-if="GlobalState.selfInfo" class="flex h-full w-full overflow-hidden">
|
||||||
<ContactsList :contacts="contacts" @contact-click="onContactClick"/>
|
<ContactsList v-if="tab === 'contacts'" :contacts="contacts" @contact-click="onContactClick"/>
|
||||||
|
<ChannelsList v-if="tab === 'channels'" :channels="channels" @channel-click="onChannelClick"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- not connected and no content -->
|
<!-- not connected and no content -->
|
||||||
@@ -25,10 +34,12 @@ import GlobalState from "../../js/GlobalState.js";
|
|||||||
import ConnectButtons from "../connect/ConnectButtons.vue";
|
import ConnectButtons from "../connect/ConnectButtons.vue";
|
||||||
import ContactsList from "../contacts/ContactsList.vue";
|
import ContactsList from "../contacts/ContactsList.vue";
|
||||||
import Utils from "../../js/Utils.js";
|
import Utils from "../../js/Utils.js";
|
||||||
|
import ChannelsList from "../channels/ChannelsList.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MainPage',
|
name: 'MainPage',
|
||||||
components: {
|
components: {
|
||||||
|
ChannelsList,
|
||||||
ContactsList,
|
ContactsList,
|
||||||
ConnectButtons,
|
ConnectButtons,
|
||||||
Page,
|
Page,
|
||||||
@@ -52,6 +63,14 @@ export default {
|
|||||||
alert("Messaging this contact type is not supported.");
|
alert("Messaging this contact type is not supported.");
|
||||||
|
|
||||||
},
|
},
|
||||||
|
async onChannelClick(channel) {
|
||||||
|
this.$router.push({
|
||||||
|
name: "channel.messages",
|
||||||
|
params: {
|
||||||
|
channelIdx: channel.idx.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
GlobalState() {
|
GlobalState() {
|
||||||
@@ -60,6 +79,22 @@ export default {
|
|||||||
contacts() {
|
contacts() {
|
||||||
return GlobalState.contacts;
|
return GlobalState.contacts;
|
||||||
},
|
},
|
||||||
|
channels() {
|
||||||
|
return GlobalState.channels;
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
get(){
|
||||||
|
return this.$route.query.tab ?? 'contacts';
|
||||||
|
},
|
||||||
|
set(value){
|
||||||
|
this.$router.replace({
|
||||||
|
query: {
|
||||||
|
...this.$route.query,
|
||||||
|
tab: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -198,6 +198,23 @@ class Connection {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async sendChannelMessage(channelIdx, text) {
|
||||||
|
|
||||||
|
// send message
|
||||||
|
await GlobalState.connection.sendChannelTextMessage(channelIdx, text);
|
||||||
|
|
||||||
|
// save to database
|
||||||
|
await Database.ChannelMessage.insert({
|
||||||
|
channel_idx: channelIdx,
|
||||||
|
from: GlobalState.selfInfo.publicKey,
|
||||||
|
path_len: null,
|
||||||
|
txt_type: Constants.TxtTypes.Plain,
|
||||||
|
sender_timestamp: Date.now(),
|
||||||
|
text: text,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
static async syncMessages() {
|
static async syncMessages() {
|
||||||
while(true){
|
while(true){
|
||||||
|
|
||||||
@@ -208,14 +225,18 @@ class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle received message
|
// handle received message
|
||||||
await this.onMessageReceived(message);
|
if(message.contactMessage){
|
||||||
|
await this.onContactMessageReceived(message.contactMessage);
|
||||||
|
} else if(message.channelMessage) {
|
||||||
|
await this.onChannelMessageReceived(message.channelMessage);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async onMessageReceived(message) {
|
static async onContactMessageReceived(message) {
|
||||||
|
|
||||||
console.log("onMessageReceived", message);
|
console.log("onContactMessageReceived", message);
|
||||||
|
|
||||||
// find first contact that matches this public key prefix
|
// find first contact that matches this public key prefix
|
||||||
// todo, maybe use the most recently updated contact in case of collision? ideally we should be given the full hash by firmware anyway...
|
// todo, maybe use the most recently updated contact in case of collision? ideally we should be given the full hash by firmware anyway...
|
||||||
@@ -242,7 +263,6 @@ class Connection {
|
|||||||
txt_type: message.txtType,
|
txt_type: message.txtType,
|
||||||
sender_timestamp: message.senderTimestamp,
|
sender_timestamp: message.senderTimestamp,
|
||||||
text: message.text,
|
text: message.text,
|
||||||
timestamp: Date.now(),
|
|
||||||
expected_ack_crc: null,
|
expected_ack_crc: null,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
@@ -252,6 +272,22 @@ class Connection {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async onChannelMessageReceived(message) {
|
||||||
|
|
||||||
|
console.log("onChannelMessageReceived", message);
|
||||||
|
|
||||||
|
// save message to database
|
||||||
|
await Database.ChannelMessage.insert({
|
||||||
|
channel_idx: message.channelIdx,
|
||||||
|
from: null,
|
||||||
|
path_len: message.pathLen,
|
||||||
|
txt_type: message.txtType,
|
||||||
|
sender_timestamp: message.senderTimestamp,
|
||||||
|
text: message.text,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Connection;
|
export default Connection;
|
||||||
|
|||||||
@@ -83,6 +83,40 @@ async function initDatabase(publicKeyHex) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
channel_messages: {
|
||||||
|
schema: {
|
||||||
|
version: 0,
|
||||||
|
primaryKey: 'id',
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
maxLength: 36,
|
||||||
|
},
|
||||||
|
channel_idx: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
path_len: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
txt_type: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
sender_timestamp: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -262,8 +296,48 @@ class ContactMessagesReadState {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChannelMessage {
|
||||||
|
|
||||||
|
// insert a channel message into the database
|
||||||
|
static async insert(data) {
|
||||||
|
return await database.channel_messages.insert({
|
||||||
|
id: v4(),
|
||||||
|
channel_idx: data.channel_idx,
|
||||||
|
from: data.from != null ? Utils.bytesToHex(data.from) : null,
|
||||||
|
path_len: data.path_len,
|
||||||
|
txt_type: data.txt_type,
|
||||||
|
sender_timestamp: data.sender_timestamp,
|
||||||
|
text: data.text,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// get channel messages for the provided channel idx
|
||||||
|
static getChannelMessages(channelIdx) {
|
||||||
|
return database.channel_messages.find({
|
||||||
|
selector: {
|
||||||
|
channel_idx: {
|
||||||
|
$eq: channelIdx,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sort: [
|
||||||
|
{
|
||||||
|
timestamp: "asc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete channel messages for the provided channel idx
|
||||||
|
static async deleteChannelMessages(channelIdx) {
|
||||||
|
await this.getChannelMessages(channelIdx).remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
initDatabase,
|
initDatabase,
|
||||||
Message,
|
Message,
|
||||||
ContactMessagesReadState,
|
ContactMessagesReadState,
|
||||||
|
ChannelMessage,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ const globalState = reactive({
|
|||||||
connection: null,
|
connection: null,
|
||||||
selfInfo: null,
|
selfInfo: null,
|
||||||
contacts: [],
|
contacts: [],
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
idx: 0,
|
||||||
|
name: "Public Channel",
|
||||||
|
description: "This is the default public channel.",
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default globalState;
|
export default globalState;
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ const routes = [
|
|||||||
props: true,
|
props: true,
|
||||||
component: () => import("./components/pages/ContactMessagesPage.vue"),
|
component: () => import("./components/pages/ContactMessagesPage.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "channel.messages",
|
||||||
|
path: '/channels/:channelIdx/messages',
|
||||||
|
props: true,
|
||||||
|
component: () => import("./components/pages/ChannelMessagesPage.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "settings.radio",
|
name: "settings.radio",
|
||||||
path: '/settings/radio',
|
path: '/settings/radio',
|
||||||
|
|||||||
Reference in New Issue
Block a user