implement ui for viewing received text messages in real time
This commit is contained in:
12
src/index.js
12
src/index.js
@ -472,6 +472,7 @@ app.get('/api/v1/text-messages', async (req, res) => {
|
|||||||
const from = req.query.from ?? undefined;
|
const from = req.query.from ?? undefined;
|
||||||
const channelId = req.query.channel_id ?? undefined;
|
const channelId = req.query.channel_id ?? undefined;
|
||||||
const gatewayId = req.query.gateway_id ?? undefined;
|
const gatewayId = req.query.gateway_id ?? undefined;
|
||||||
|
const lastId = req.query.last_id ? parseInt(req.query.last_id) : undefined;
|
||||||
const count = req.query.count ? parseInt(req.query.count) : 50;
|
const count = req.query.count ? parseInt(req.query.count) : 50;
|
||||||
const order = req.query.order ?? "asc";
|
const order = req.query.order ?? "asc";
|
||||||
|
|
||||||
@ -482,6 +483,13 @@ app.get('/api/v1/text-messages', async (req, res) => {
|
|||||||
from: from,
|
from: from,
|
||||||
channel_id: channelId,
|
channel_id: channelId,
|
||||||
gateway_id: gatewayId,
|
gateway_id: gatewayId,
|
||||||
|
// when ordered oldest to newest (asc), only get records after last id
|
||||||
|
// when ordered newest to oldest (desc), only get records before last id
|
||||||
|
id: order === "asc" ? {
|
||||||
|
gt: lastId,
|
||||||
|
} : {
|
||||||
|
lt: lastId,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: order,
|
id: order,
|
||||||
@ -500,6 +508,10 @@ app.get('/api/v1/text-messages', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/v1/text-messages/embed', async (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public/text-messages-embed.html'));
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/v1/waypoints', async (req, res) => {
|
app.get('/api/v1/waypoints', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
276
src/public/text-messages-embed.html
Normal file
276
src/public/text-messages-embed.html
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
<title>Meshtastic Messages</title>
|
||||||
|
|
||||||
|
<!-- tailwind css -->
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
||||||
|
|
||||||
|
<!-- moment -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.min.js"></script>
|
||||||
|
|
||||||
|
<!-- vuejs -->
|
||||||
|
<script src="https://unpkg.com/vue@3"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
/* used to prevent ui flicker before vuejs loads */
|
||||||
|
[v-cloak] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="h-full">
|
||||||
|
<div id="app" v-cloak>
|
||||||
|
<div class="h-full flex flex-col overflow-hidden">
|
||||||
|
|
||||||
|
<!-- load previous -->
|
||||||
|
<button @click="loadPrevious" type="button" class="w-full bg-gray-200 p-2 border-b hover:bg-gray-300">Load Previous</button>
|
||||||
|
|
||||||
|
<!-- messages -->
|
||||||
|
<div id="messages" class="h-full flex flex-col space-y-3 p-3 overflow-y-scroll">
|
||||||
|
<div :key="message.id" v-for="message of messages" class="flex flex-col max-w-xl items-start">
|
||||||
|
|
||||||
|
<!-- sender -->
|
||||||
|
<div class="text-xs text-gray-500">{{ getNodeName(message.from) }}</div>
|
||||||
|
|
||||||
|
<!-- message -->
|
||||||
|
<div class="border border-gray-300 rounded-xl shadow overflow-hidden bg-[#efefef]">
|
||||||
|
<div class="w-full space-y-0.5 px-2.5 py-1" v-html="escapeMessageText(message.text)"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
Vue.createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
to: null,
|
||||||
|
from: null,
|
||||||
|
channelId: null,
|
||||||
|
gatewayId: null,
|
||||||
|
|
||||||
|
isLoadingPrevious: false,
|
||||||
|
isLoadingMore: false,
|
||||||
|
shouldAutoScroll: true,
|
||||||
|
|
||||||
|
messages: [],
|
||||||
|
nodesById: {},
|
||||||
|
|
||||||
|
moment: window.moment,
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted: function() {
|
||||||
|
|
||||||
|
// parse url params
|
||||||
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
this.to = queryParams.get('to');
|
||||||
|
this.from = queryParams.get('from');
|
||||||
|
this.channelId = queryParams.get('channel_id');
|
||||||
|
this.gatewayId = queryParams.get('gateway_id');
|
||||||
|
this.count = queryParams.get('count');
|
||||||
|
|
||||||
|
// listen for scrolling of messages list
|
||||||
|
document.getElementById("messages").addEventListener("scroll", (event) => {
|
||||||
|
|
||||||
|
// check if messages is scrolled to bottom
|
||||||
|
const element = event.target;
|
||||||
|
const isAtBottom = element.scrollTop === (element.scrollHeight - element.offsetHeight);
|
||||||
|
|
||||||
|
// we want to auto scroll if user is at bottom of messages list
|
||||||
|
this.shouldAutoScroll = isAtBottom;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
this.initialLoad();
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async initialLoad() {
|
||||||
|
|
||||||
|
// load 1 page of previous messages
|
||||||
|
await this.loadPrevious();
|
||||||
|
|
||||||
|
// scroll to bottom
|
||||||
|
this.scrollToBottom();
|
||||||
|
|
||||||
|
// load more every few seconds
|
||||||
|
setInterval(async () => {
|
||||||
|
await this.loadMore();
|
||||||
|
}, 2500);
|
||||||
|
|
||||||
|
},
|
||||||
|
async loadPrevious() {
|
||||||
|
|
||||||
|
// do nothing if already loading
|
||||||
|
if(this.isLoadingPrevious){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoadingPrevious = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await window.axios.get('/api/v1/text-messages', {
|
||||||
|
params: {
|
||||||
|
to: this.to,
|
||||||
|
from: this.from,
|
||||||
|
channelId: this.channelId,
|
||||||
|
gatewayId: this.gatewayId,
|
||||||
|
count: this.count,
|
||||||
|
order: "desc",
|
||||||
|
last_id: this.oldestMessageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// add messages to start of existing messages
|
||||||
|
const messages = response.data.text_messages;
|
||||||
|
for(const message of messages){
|
||||||
|
this.messages.unshift(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch node info
|
||||||
|
for(const message of messages){
|
||||||
|
await this.fetchNodeInfo(message.to);
|
||||||
|
await this.fetchNodeInfo(message.from);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// do nothing
|
||||||
|
} finally {
|
||||||
|
this.isLoadingPrevious = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
async loadMore() {
|
||||||
|
|
||||||
|
// do nothing if already loading
|
||||||
|
if(this.isLoadingMore){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoadingMore = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await window.axios.get('/api/v1/text-messages', {
|
||||||
|
params: {
|
||||||
|
to: this.to,
|
||||||
|
from: this.from,
|
||||||
|
channelId: this.channelId,
|
||||||
|
gatewayId: this.gatewayId,
|
||||||
|
count: this.count,
|
||||||
|
order: "asc",
|
||||||
|
last_id: this.latestMessageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// add messages to end of existing messages
|
||||||
|
const messages = response.data.text_messages;
|
||||||
|
for(const message of messages){
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll to bottom
|
||||||
|
if(this.shouldAutoScroll){
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch node info
|
||||||
|
for(const message of messages){
|
||||||
|
await this.fetchNodeInfo(message.to);
|
||||||
|
await this.fetchNodeInfo(message.from);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// do nothing
|
||||||
|
} finally {
|
||||||
|
this.isLoadingMore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
async fetchNodeInfo(nodeId) {
|
||||||
|
|
||||||
|
// do nothing if already fetched
|
||||||
|
if(nodeId in this.nodesById){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do nothing if broadcast address
|
||||||
|
if(nodeId.toString() === "4294967295"){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const response = await window.axios.get(`/api/v1/nodes/${nodeId}`);
|
||||||
|
const node = response.data.node;
|
||||||
|
|
||||||
|
if(node){
|
||||||
|
this.nodesById[node.node_id] = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
scrollToBottom: function() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
var container = this.$el.querySelector("#messages");
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getNodeName(nodeId) {
|
||||||
|
|
||||||
|
// find node by id
|
||||||
|
const node = this.nodesById[nodeId];
|
||||||
|
if(!node){
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `[${node.short_name}] ${node.long_name}`;
|
||||||
|
|
||||||
|
},
|
||||||
|
escapeMessageText(text) {
|
||||||
|
return text.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('\n', '<br/>');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
oldestMessageId() {
|
||||||
|
|
||||||
|
if(this.messages.length > 0){
|
||||||
|
return this.messages[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
},
|
||||||
|
latestMessageId() {
|
||||||
|
|
||||||
|
if(this.messages.length > 0){
|
||||||
|
return this.messages[this.messages.length - 1].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}).mount('#app');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user