diff --git a/package-lock.json b/package-lock.json index 02193a5..aeb1a09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@liamcottle/meshcore.js": "^1.0.0", + "@liamcottle/meshcore.js": "^1.0.3", "@tailwindcss/forms": "^0.5.10", "@vitejs/plugin-vue": "^5.2.1", "autoprefixer": "^10.4.20", @@ -1116,9 +1116,9 @@ } }, "node_modules/@liamcottle/meshcore.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@liamcottle/meshcore.js/-/meshcore.js-1.0.0.tgz", - "integrity": "sha512-N4cVn9V+2CP87o24z/Nv32KDcgs1yBWd59zp5qbidOFLg8848JRecKmu9/6BERrQ9QZIOROrDUE7LtxfhFBjKA==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@liamcottle/meshcore.js/-/meshcore.js-1.0.3.tgz", + "integrity": "sha512-i3AZt2xjSi7kTu86ou+t93K2IPyr7r56ZUZa2A3FRGWzumQ/OxXt30sCox5W6UUcMSLKGzEkMviiTRj7JaDWKw==" }, "node_modules/@mongodb-js/saslprep": { "version": "1.2.0", diff --git a/package.json b/package.json index ab5c001..3eaef14 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "author": "Liam Cottle ", "license": "MIT", "dependencies": { - "@liamcottle/meshcore.js": "^1.0.0", + "@liamcottle/meshcore.js": "^1.0.3", "@tailwindcss/forms": "^0.5.10", "@vitejs/plugin-vue": "^5.2.1", "autoprefixer": "^10.4.20", diff --git a/src/components/channels/ChannelListItem.vue b/src/components/channels/ChannelListItem.vue new file mode 100644 index 0000000..cbb4791 --- /dev/null +++ b/src/components/channels/ChannelListItem.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/channels/ChannelsList.vue b/src/components/channels/ChannelsList.vue new file mode 100644 index 0000000..5dd5057 --- /dev/null +++ b/src/components/channels/ChannelsList.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/components/messages/MessageViewer.vue b/src/components/messages/MessageViewer.vue index 1e1f139..31c7261 100644 --- a/src/components/messages/MessageViewer.vue +++ b/src/components/messages/MessageViewer.vue @@ -22,13 +22,18 @@ - +
{{ formatMessageTimestamp(message.timestamp) }}
+ +
+ {{ formatMessageTimestamp(message.timestamp) }} +
+ -
+
@@ -108,7 +113,9 @@ import Utils from "../../js/Utils.js"; export default { name: 'MessageViewer', props: { + type: String, contact: Object, + channel: Object, }, data() { return { @@ -125,7 +132,11 @@ export default { mounted() { // init database subscription for messages - this.messagesSubscription = Database.Message.getContactMessages(this.contact.publicKey).$.subscribe(this.onMessagesUpdated); + if(this.type === "contact"){ + 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 this.updateMessagesLastReadAt(); @@ -161,8 +172,11 @@ export default { try { - // send message - await Connection.sendMessage(this.contact.publicKey, newMessageText); + if(this.type === "contact"){ + await Connection.sendMessage(this.contact.publicKey, newMessageText); + } else if(this.type === "channel") { + await Connection.sendChannelMessage(this.channel.idx, newMessageText); + } // clear new message input this.newMessageText = ""; @@ -248,7 +262,9 @@ export default { updateMessagesLastReadAt() { // update last read at for contact messages - Database.ContactMessagesReadState.touch(this.contact.publicKey); + if(this.type === "contact"){ + Database.ContactMessagesReadState.touch(this.contact.publicKey); + } }, formatMessageTimestamp(timestamp) { @@ -259,7 +275,12 @@ export default { canSendMessage() { // 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; } diff --git a/src/components/pages/ChannelMessagesPage.vue b/src/components/pages/ChannelMessagesPage.vue new file mode 100644 index 0000000..59344c6 --- /dev/null +++ b/src/components/pages/ChannelMessagesPage.vue @@ -0,0 +1,54 @@ + + + diff --git a/src/components/pages/ContactMessagesPage.vue b/src/components/pages/ContactMessagesPage.vue index 20ac898..7eb70a7 100644 --- a/src/components/pages/ContactMessagesPage.vue +++ b/src/components/pages/ContactMessagesPage.vue @@ -14,7 +14,7 @@
- +
diff --git a/src/components/pages/MainPage.vue b/src/components/pages/MainPage.vue index 9e5e72c..9bb1e89 100644 --- a/src/components/pages/MainPage.vue +++ b/src/components/pages/MainPage.vue @@ -4,9 +4,18 @@
+ +
+
+
Contacts
+
Channels
+
+
+ -
- +
+ +
@@ -25,10 +34,12 @@ import GlobalState from "../../js/GlobalState.js"; import ConnectButtons from "../connect/ConnectButtons.vue"; import ContactsList from "../contacts/ContactsList.vue"; import Utils from "../../js/Utils.js"; +import ChannelsList from "../channels/ChannelsList.vue"; export default { name: 'MainPage', components: { + ChannelsList, ContactsList, ConnectButtons, Page, @@ -52,6 +63,14 @@ export default { alert("Messaging this contact type is not supported."); }, + async onChannelClick(channel) { + this.$router.push({ + name: "channel.messages", + params: { + channelIdx: channel.idx.toString(), + }, + }); + }, }, computed: { GlobalState() { @@ -60,6 +79,22 @@ export default { 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, + }, + }); + }, + }, }, } diff --git a/src/js/Connection.js b/src/js/Connection.js index 41f6b6c..2711032 100644 --- a/src/js/Connection.js +++ b/src/js/Connection.js @@ -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() { while(true){ @@ -208,14 +225,18 @@ class Connection { } // 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 // 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, sender_timestamp: message.senderTimestamp, text: message.text, - timestamp: Date.now(), expected_ack_crc: 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; diff --git a/src/js/Database.js b/src/js/Database.js index 7bd0038..9c401d7 100644 --- a/src/js/Database.js +++ b/src/js/Database.js @@ -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 { initDatabase, Message, ContactMessagesReadState, + ChannelMessage, }; diff --git a/src/js/GlobalState.js b/src/js/GlobalState.js index f4f0e92..1140f6c 100644 --- a/src/js/GlobalState.js +++ b/src/js/GlobalState.js @@ -5,6 +5,13 @@ const globalState = reactive({ connection: null, selfInfo: null, contacts: [], + channels: [ + { + idx: 0, + name: "Public Channel", + description: "This is the default public channel.", + }, + ], }); export default globalState; diff --git a/src/main.js b/src/main.js index daad81d..0beda50 100644 --- a/src/main.js +++ b/src/main.js @@ -32,6 +32,12 @@ const routes = [ props: true, component: () => import("./components/pages/ContactMessagesPage.vue"), }, + { + name: "channel.messages", + path: '/channels/:channelIdx/messages', + props: true, + component: () => import("./components/pages/ChannelMessagesPage.vue"), + }, { name: "settings.radio", path: '/settings/radio',