Added notification support to mobile application.

Ylian Saint-Hilaire committed Jan 4, 2021 at 19:14 UTC 9a1eb534c41b26326e0a17801bace95884b4ef00
4 files changed +303 -13
meshcentral.js
+1
@@ -159,6 +159,7 @@ function CreateMeshCentralServer(config, args) {
159 console.log(' --redirport [number] Creates an additional HTTP server to redirect users to the HTTPS server.');
160 console.log(' --exactports Server must run with correct ports or exit.');
161 console.log(' --noagentupdate Server will not update mesh agent native binaries.');
162 + console.log(' --nedbtodb Transfer all NeDB records into current database.');
163 console.log(' --listuserids Show a list of a user identifiers in the database.');
164 console.log(' --cert [name], (country), (org) Create a web server certificate with [name] server name.');
165 console.log(' country and organization can optionally be set.');
views/default-mobile.handlebars
+289 -4
@@ -417,10 +417,65 @@
417 margin:10px;
418 z-index:1000;
419 }
420 +
421 + #notificationCount {
422 + min-width: 28px;
423 + font-size: 20px;
424 + background-color: orange;
425 + text-align: center;
426 + cursor: pointer;
427 + color: black;
428 + }
429 +
430 + .notifiyBox {
431 + font-size: 16px;
432 + position: absolute;
433 + z-index: 1000;
434 + top: 60px;
435 + right: 76px;
436 + width: 300px;
437 + text-align: left;
438 + background-color: #F0ECCD;
439 + border: 4px solid #666;
440 + -webkit-border-radius: 10px;
441 + -moz-border-radius: 10px;
442 + border-radius: 10px;
443 + -webkit-box-shadow: 2px 2px 4px #888;
444 + -moz-box-shadow: 2px 2px 4px #888;
445 + box-shadow: 2px 2px 4px #888;
446 + max-height: 200px;
447 + }
448 +
449 + .night .notifiyBox {
450 + color: black;
451 + }
452 +
453 + .notifiyBox:before {
454 + content: ' ';
455 + position: absolute;
456 + width: 0;
457 + height: 0;
458 + right: 5px;
459 + top: -30px;
460 + border: 15px solid;
461 + border-color: transparent #666 #666 transparent;
462 + }
463 +
464 + .notifiyBox:after {
465 + content: ' ';
466 + position: absolute;
467 + width: 0;
468 + height: 0;
469 + right: 7px;
470 + top: -24px;
471 + border: 12px solid;
472 + border-color: transparent #F0ECCD #F0ECCD transparent;
473 + }
474 </style>
475 </head>
476 <body id="body" onload="if (typeof(startup) !== 'undefined') startup();" style="overflow-y:hidden;margin:0;padding:0;border:0;color:black;font-size:13px;font-family:\'Trebuchet MS\', Arial, Helvetica, sans-serif">
477 <div id=container>
478 + <div id="notifiyBox" class="notifiyBox" style="display:none"></div>
479 <div id=mastheadx></div>
480 <div id=masthead style="background:url(logo.png) 0px 0px;background-size:341px 50px;background-color:#036;background-repeat:no-repeat;height:50px;width:100%;overflow:hidden">
481 <div style="width:calc(100% - 50px);overflow:hidden">
@@ -431,7 +486,8 @@
486 <strong><font style="font-size:12px;font-family:Arial,Helvetica,sans-serif">{{{title2}}}</font></strong>
487 </div>
488 </div>
434 - <img id="topMenuIcon" class=noselect style="position:absolute;right:0;top:10px;bottom:50px;color:#c8c8c8;font-size:44px;margin-right:8px;cursor:pointer;display:none" onclick=topMenu() src="/images/3bars-30.png" width=30 height=30 />
489 + <div id=notificationCount onclick="clickNotificationIcon()" class="unselectable" style="position:absolute;right:50px;top:0px;font-size:28px;width:50px;height:50px;cursor:pointer;display:none" title="Click to view current notifications"><div id="notificationCount2" style="padding-top:8px">0</div></div>
490 + <img id="topMenuIcon" class=noselect style="position:absolute;right:0;top:10px;color:#c8c8c8;font-size:44px;margin-right:8px;cursor:pointer;display:none" onclick=topMenu() src="/images/3bars-30.png" width=30 height=30 />
491 </div>
492 <div id=page_content style="position:absolute;bottom:32px;top:50px;width:100%">
493 <div id=column_l style="width:100%;padding:0;position:absolute;bottom:0px;top:0px">
@@ -879,6 +935,7 @@
935 <div style="padding:12px;border-top:1px solid gray;color:black;cursor:pointer" onclick=topMenu(1)>My Account</div>
936 <div id="logoutMenuOption"><a href=/logout><div style="padding:12px;border-top:1px solid gray;color:black;cursor:pointer">Logout</div></a></div>
937 </div>
938 + <audio id="chimes"><source src="sounds/chimes.mp3" type="audio/mp3" /></audio>
939 <iframe name="fileUploadFrame" style=display:none></iframe>
940 <script>
941 'use strict';
@@ -1170,6 +1227,36 @@
1227 }
1228 break;
1229 }
1230 + case 'msg': {
1231 + // Check if this is a message from a node
1232 + if (message.nodeid != null) {
1233 + var index = -1;
1234 + if (nodes != null) { for (var i in nodes) { if (nodes[i]._id == message.nodeid) { index = i; break; } } }
1235 + if (index != -1) {
1236 + if (message.type == 'notify') { // This is a notification message.
1237 + var n = getstore('notifications', 0);
1238 + if (((n & 8) == 0) && (message.amtMessage != null)) { break; } // Intel AMT desktop & terminal messages should be ignored.
1239 + var n = { text: message.value, title: message.title, icon: message.icon, titleid: message.titleid, msgid: message.msgid, args: message.args };
1240 + if (message.id != null) { n.id = message.id; }
1241 + if (message.nodeid != null) { n.nodeid = message.nodeid; }
1242 + if (message.tag != null) { n.tag = message.tag; }
1243 + if (message.username != null) { n.username = message.username; }
1244 + if (typeof message.maxtime == 'number') { n.maxtime = message.maxtime; }
1245 + addNotification(n);
1246 + }
1247 + }
1248 + } else {
1249 + if (message.type == 'notify') { // This is a notification message.
1250 + var n = { text: message.value, title: message.title, icon: message.icon, titleid: message.titleid, msgid: message.msgid, args: message.args };
1251 + if (message.id != null) { n.id = message.id; }
1252 + if (message.tag != null) { n.tag = message.tag; }
1253 + if (message.username != null) { n.username = message.username; }
1254 + if (typeof message.maxtime == 'number') { n.maxtime = message.maxtime; }
1255 + addNotification(n);
1256 + }
1257 + }
1258 + break;
1259 + }
1260 case 'getnetworkinfo': {
1261 if (currentNode._id != message.nodeid) return;
1262 updateDeviceDetails(getNodeFromId(message.nodeid), null, message);
@@ -1536,9 +1623,11 @@
1623 break;
1624 }
1625 case 'notify': {
1539 - //var n = { text: message.event.value };
1540 - //if (message.event.tag != null) { n.tag = message.event.tag; }
1541 - //addNotification(n);
1626 + var n = { text: message.event.value, title: message.event.title, icon: message.event.icon, titleid: message.titleid, msgid: message.msgid, args: message.args };
1627 + if (message.id != null) { n.id = message.id; }
1628 + if (message.event.tag != null) { n.tag = message.event.tag; }
1629 + if (typeof message.maxtime == 'number') { n.maxtime = message.maxtime; }
1630 + addNotification(n);
1631 break;
1632 }
1633 case 'sysinfohash': {
@@ -4449,6 +4538,200 @@
4538 function p20deleteUser(e, userid) { haltEvent(e); p20viewuserEx(2, decodeURIComponent(userid)); }
4539 function p20viewuserEx2(button, userid) { meshserver.send({ action: 'removemeshuser', meshid: currentMesh._id, meshname: currentMesh.name, userid: userid }); }
4540
4541 +
4542 + //
4543 + // NOTIFICATIONS
4544 + //
4545 +
4546 + var notifications = [];
4547 +
4548 + // Toggle showing notifications
4549 + function clickNotificationIcon(show) {
4550 + //addNotification({ icon:0, text:'test' });
4551 + if (show == true) { QV('notifiyBox', true); } else if (show == false) { QV('notifiyBox', false); } else { QV('notifiyBox', QS('notifiyBox')['display'] == 'none'); }
4552 + drawNotifications();
4553 + }
4554 +
4555 + // Set the notification count on the upper right oft he screen
4556 + function setNotificationCount(c) {
4557 + if (parseInt(Q('notificationCount').innerHTML) == c) return; // If the count did not change, exit now.
4558 + QH('notificationCount2', c);
4559 + QV('notificationCount', c > 0);
4560 + }
4561 +
4562 + // Refresh the notification box
4563 + function drawNotifications() {
4564 + var notifySettings = getstore('notifications', 0);
4565 + var r = '';
4566 + if (notifications.length == 0) {
4567 + r = '<div style=margin:5px>' + "There are currently no notifications" + '</div>';
4568 + } else {
4569 + for (var i in notifications) {
4570 + var n = notifications[i], t = '', d = new Date(n.time), icon = 0;
4571 + if (n.title != null) { t = '<b>' + EscapeHtml(n.title) + '</b>: ' }
4572 + if (n.nodeid != null) {
4573 + var node = getNodeFromId(n.nodeid);
4574 + if (node != null) {
4575 + icon = node.icon;
4576 + if (notifySettings & 16) { t = '<b>' + EscapeHtml(meshes[node.meshid].name) + ' / ' + EscapeHtml(node.name) + '</b>: '; } else { t = '<b>' + EscapeHtml(node.name) + '</b>: '; } // Display with or without group name
4577 + }
4578 + }
4579 +
4580 + r += '<div title="' + format("Occured at {0}", printDateTime(d)) + '" id="notifyx' + n.id + '" class=notification style="cursor:pointer;border-top:1px solid ' + ((r == '') ? 'transparent' : 'orange') + '">';
4581 + if (icon) { r += '<div class=j' + icon + ' onclick="notificationSelected(' + n.id + ')" style=margin:5px;float:left></div>'; }
4582 + r += '<div onclick="notificationDelete(' + n.id + ')" class=unselectable title="' + "Clear this notification" + '" style=margin:5px;float:right;color:orange><b>X</b></div><div onclick="notificationSelected(' + n.id + ')" style=margin:5px>' + t + EscapeHtml(n.text) + '</div></div>';
4583 + }
4584 + }
4585 + var deleteall = '';
4586 + if (notifications.length > 1) { deleteall = '<div id="notifyRemoveAll" onclick="deleteAllNotifications()" style="cursor:pointer;border-top:1px solid orange;margin:5px;color:orange;text-align:right;padding-right:3px">' + "Clear all" + '</div>'; }
4587 + QH('notifiyBox', '<div class=customScroll style="max-height:170px;overflow-y:auto;margin:5px">' + r + '</div>' + deleteall);
4588 + }
4589 +
4590 + // A notification was selected
4591 + function notificationSelected(id, del) {
4592 + var j = -1;
4593 + for (var i in notifications) { if (notifications[i].id == id) { j = i; } }
4594 + if (j != -1) {
4595 + notificationSelectedEx(notifications[j], id);
4596 + if (del && notifications[j]) {
4597 + if (notifications[j].notification) { notifications[j].notification.close(); delete notifications[j].notification; }
4598 + notificationDelete(id);
4599 + }
4600 + }
4601 + }
4602 +
4603 + function notificationSelectedEx(n, id) {
4604 + if (n.nodeid != null) {
4605 + if (n.tag == 'desktop') gotoDevice(n.nodeid, 12); // Desktop
4606 + else if (n.tag == 'terminal') gotoDevice(n.nodeid, 11); // Terminal
4607 + else if (n.tag == 'files') gotoDevice(n.nodeid, 13); // Files
4608 + else if (n.tag == 'intelamt') gotoDevice(n.nodeid, 14); // Intel AMT
4609 + else if (n.tag == 'console') gotoDevice(n.nodeid, 15); // Files
4610 + else gotoDevice(n.nodeid, 10); // General
4611 + } else {
4612 + if ((n.tag != null) && n.tag.startsWith('meshmessenger/')) {
4613 + safeNewWindow('/messenger?id=' + n.tag + '&title=' + encodeURIComponentEx(n.username), n.tag.split('/')[2]);
4614 + notificationDelete(id);
4615 + }
4616 + }
4617 + }
4618 +
4619 + // Remove one notification
4620 + function notificationDelete(id) {
4621 + var j = -1, e = Q('notifyx' + id);
4622 + if (e != null) {
4623 + for (var i in notifications) { if (notifications[i].id == id) { j = i; } }
4624 + if (j != -1) {
4625 + meshserver.send({ action: 'intersession', subaction: 'removeNotify', id: id }); // Remove the notification in other sessions of the same user.
4626 + if (notifications[j].notification) { notifications[j].notification.close(); delete notifications[j].notification; }
4627 + notifications.splice(j, 1);
4628 + e.parentNode.removeChild(e);
4629 + setNotificationCount(notifications.length);
4630 + if (notifications.length == 0) { QV('notifiyBox', false); }
4631 + if (notifications.length == 1) { QV('notifyRemoveAll', false); }
4632 + if ((notifications.length > 0) && (j == 0)) {
4633 + var n = notifications[0];
4634 + QS('notifyx' + n.id)['border-top'] = '1px solid transparent';
4635 + }
4636 + }
4637 + }
4638 + }
4639 +
4640 + // Add a new notification and play the notification sound
4641 + function addNotification(n) {
4642 + // Perform message translation
4643 + var translatedTitles = [
4644 + null,
4645 + "New Account", // 1
4646 + "Server Limit",
4647 + "Security Warning",
4648 + "Account Settings",
4649 + "Device Group",
4650 + "Invite Codes"
4651 + ];
4652 + var translatedMessages = [
4653 + null,
4654 + "Permission denied", // 1
4655 + "Invalid username",
4656 + "Invalid password",
4657 + "Invalid email",
4658 + "Invalid domain",
4659 + "Invalid site permissions",
4660 + "User already exists",
4661 + "Unable to add user in this mode",
4662 + "Validation exception",
4663 + "Account limit reached.", // 10
4664 + "Chat Request, Click here to accept.",
4665 + "There has been {0} failed login attempts on this account since the last login.",
4666 + "Failed to change email address, another account already using: {0}.",
4667 + "Email sent.",
4668 + "User {0} not found.",
4669 + "Users {0} not found.",
4670 + "Error, unable to change to previously used password.",
4671 + "Error, unable to change to commonly used password.",
4672 + "Error, password not changed.",
4673 + "Password changed.", // 20
4674 + "Current password not correct.",
4675 + "Error, invite code \"{0}\" already in use.",
4676 + "SMS gateway not enabled",
4677 + "No user management rights",
4678 + "Invalid SMS message",
4679 + "No phone number for this user",
4680 + "SMS succesfuly sent.",
4681 + "SMS error",
4682 + "SMS error: {0}"
4683 + ];
4684 + if (typeof n.titleid == 'number') { try { n.title = translatedTitles[n.titleid]; } catch (ex) { } }
4685 + if (typeof n.msgid == 'number') { try { n.text = translatedMessages[n.msgid]; if (Array.isArray(n.args)) { format(n.text, ...n.args); } } catch (ex) { } }
4686 +
4687 + // Show notification within the web page.
4688 + if (n.time == null) { n.time = Date.now(); }
4689 + if (n.id == null) { n.id = Math.random(); }
4690 + notifications.unshift(n);
4691 + setNotificationCount(notifications.length);
4692 + clickNotificationIcon(true);
4693 + var notifySettings = getstore('notifications', 0);
4694 + if (notifySettings & 1) { Q('chimes').play(); }
4695 +
4696 + // If web notifications are granted, use it.
4697 + var notification = null;
4698 + if (Notification && (Notification.permission == 'granted')) {
4699 + var text = n.text.split('&reg;').join('').split('<b>').join('').split('</b>').join('').split('<br />').join('\r\n'); // Clean up any HTML codes
4700 + if (n.nodeid) {
4701 + var node = getNodeFromId(n.nodeid);
4702 + if (node) {
4703 + if (notifySettings & 16) { // Notify with group name
4704 + notification = new Notification(decodeURIComponent('{{{extitle}}}') + ' - ' + meshes[node.meshid].name + ' - ' + node.name, { tag: n.tag, body: text, icon: '/images/notify/icons128-' + node.icon + '.png' });
4705 + } else {
4706 + notification = new Notification(decodeURIComponent('{{{extitle}}}') + ' - ' + node.name, { tag: n.tag, body: text, icon: '/images/notify/icons128-' + node.icon + '.png' });
4707 + }
4708 + }
4709 + } else {
4710 + if (n.icon == null) { n.icon = 0; }
4711 + var title = n.title;
4712 + if (title == null) { title = ''; } else { title = ' - ' + n.title; }
4713 + notification = new Notification(decodeURIComponent('{{{extitle}}}') + title, { tag: n.tag, body: text, icon: '/images/notify/icons128-' + n.icon + '.png' });
4714 + }
4715 + notification.id = n.id;
4716 + notification.xtag = n.tag;
4717 + notification.nodeid = n.nodeid;
4718 + notification.username = n.username;
4719 + notification.onclick = function (e) { notificationSelected(e.target.id, true); }
4720 + n.notification = notification;
4721 + }
4722 +
4723 + // If the notification has a max time, setup the timer here.
4724 + if ((typeof n.maxtime == 'number') && (n.maxtime > 0)) { var trigger = function notifyRemoveTrigger() { notificationDelete(notifyRemoveTrigger.xid); }; trigger.xid = n.id; setTimeout(trigger, n.maxtime * 1000); }
4725 + }
4726 +
4727 + // Remove all notifications
4728 + function deleteAllNotifications() {
4729 + notifications = [];
4730 + setNotificationCount(0);
4731 + drawNotifications();
4732 + QV('notifiyBox', false);
4733 + }
4734 +
4735 //
4736 // PANELS
4737 //
@@ -4649,6 +4932,8 @@
4932 function getUserName(userid) { if (users && users[userid] != null) return users[userid].name; return userid.split('/')[2]; }
4933 function addDetailItem(title, value, state) { return '<table style=width:100%><td>' + nobreak(title) + '<td style=text-align:right>' + value + '</table>'; }
4934 function isPrivateIP(a) { return (a.startsWith('10.') || a.startsWith('172.16.') || a.startsWith('192.168.')); }
4935 + function encodeURIComponentEx(txt) { return encodeURIComponent(txt).replace(/'/g, '%27'); };
4936 + function safeNewWindow(url, target) { var newWindow = window.open(url, target, 'noopener,noreferrer'); if (newWindow) { newWindow.opener = null; } }
4937
4938 </script>
4939 </body>
views/default.handlebars
+11 -8
@@ -12425,8 +12425,8 @@
12425 var emailLink = '';
12426 if (user.email) { emailLink = ' <a href="mailto:' + EscapeHtml(user.email) + '" \'><img class=hoverButton src="images/link1.png" /></a>'; }
12427 if (((user.siteadmin != 0xFFFFFFFF) || (userinfo.siteadmin == 0xFFFFFFFF))) { // If we are not site admin, we can't change a admin email or real name
12428 - x += addDeviceAttribute("Email", everify + email + emailLink + ' <img class=hoverButton style=cursor:pointer src="images/link5.png" onclick=p30showUserEmailChangeDialog(event,"' + userid + '") />');
12429 - x += addDeviceAttribute("Real Name", realname + ' <img class=hoverButton style=cursor:pointer src="images/link5.png" onclick=p30showUserRealNameChangeDialog(event,"' + userid + '") />');
12428 + x += addDeviceAttribute("Email", everify + email + emailLink + ' <img class=hoverButton style=cursor:pointer src="images/link5.png" onclick=p30showUserEmailChangeDialog(event,"' + encodeURIComponentEx(user._id) + '") />');
12429 + x += addDeviceAttribute("Real Name", realname + ' <img class=hoverButton style=cursor:pointer src="images/link5.png" onclick=p30showUserRealNameChangeDialog(event,"' + encodeURIComponentEx(user._id) + '") />');
12430 } else {
12431 x += addDeviceAttribute("Email", everify + email + emailLink);
12432 x += addDeviceAttribute("Real Name", realname);
@@ -12436,7 +12436,7 @@
12436 x += addDeviceAttribute("Phone Number", (user.phone?user.phone:('<i>' + "None" + '</i>')) + ' <img class=hoverButton style=cursor:pointer src="images/link5.png" onclick=p30editPhone() />');
12437 }
12438
12439 - x += addDeviceAttribute("Server Rights", premsg + msg.join(', ') + ' <img style=cursor:pointer class=hoverButton onclick=\'return showUserAdminDialog(event,"' + userid + '")\' src="images/link5.png" />');
12439 + x += addDeviceAttribute("Server Rights", premsg + msg.join(', ') + ' <img style=cursor:pointer class=hoverButton onclick=\'return showUserAdminDialog(event,"' + encodeURIComponentEx(user._id) + '")\' src="images/link5.png" />');
12440 if (user.quota) x += addDeviceAttribute("Server Quota", EscapeHtml(parseInt(user.quota) / 1024) + ' k');
12441 x += addDeviceAttribute("Creation", printDateTime(new Date(user.creation * 1000)));
12442 if (user.login) x += addDeviceAttribute("Last Login", printDateTime(new Date(user.login * 1000)));
@@ -12455,7 +12455,7 @@
12455 if ((userinfo.siteadmin == 0xFFFFFFFF) || (userinfo.siteadmin & 2)) {
12456 var xuserGroups = '<i>' + "None" + '</i>';
12457 if (user.groups) { xuserGroups = ''; for (var i in user.groups) { xuserGroups += '<span class="tagSpan">' + EscapeHtml(user.groups[i]) + '</span>'; } }
12458 - x += addDeviceAttribute("Admin Realms", addLinkConditional(xuserGroups, 'showUserGroupDialog(event,"' + userid + '")', (userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.groups == null) && (userinfo._id != user._id) && (user.siteadmin != 0xFFFFFFFF))));
12458 + x += addDeviceAttribute("Admin Realms", addLinkConditional(xuserGroups, 'showUserGroupDialog(event,"' + encodeURIComponentEx(user._id) + '")', (userinfo.siteadmin == 0xFFFFFFFF) || ((userinfo.groups == null) && (userinfo._id != user._id) && (user.siteadmin != 0xFFFFFFFF))));
12459 }
12460
12461 // Display device user consent
@@ -12488,10 +12488,13 @@
12488 x += '</table></div><br />';
12489
12490 // Add action buttons
12491 - x += '<input type=button value="' + "Notes" + '" title="' + "View notes about this user" + '" onclick=showNotes(false,"' + encodeURIComponentEx(userid) + '") />';
12492 - if (user.phone && (features & 0x02000000)) { x += '<input type=button value="' + "SMS" + '" title="' + "Send a SMS message to this user" + '" onclick=showSendSMS("' + encodeURIComponentEx(userid) + '") />'; }
12493 - if ((typeof user.email == 'string') && (user.emailVerified === true) && (features & 0x00000040)) { x += '<input type=button value="' + "Email" + '" title="' + "Send a email message to this user" + '" onclick=showSendEmail("' + userid + '") />'; }
12494 - if (!self && (activeSessions > 0)) { x += '<input type=button value="' + "Notify" + '" title="' + "Send user notification" + '" onclick=showUserAlertDialog(event,"' + encodeURIComponentEx(userid) + '") />'; }
12491 + x += '<input type=button value="' + "Notes" + '" title="' + "View notes about this user" + '" onclick=showNotes(false,"' + encodeURIComponentEx(user._id) + '") />';
12492 + if (user.phone && (features & 0x02000000)) { x += '<input type=button value="' + "SMS" + '" title="' + "Send a SMS message to this user" + '" onclick=showSendSMS("' + encodeURIComponentEx(user._id) + '") />'; }
12493 + if ((typeof user.email == 'string') && (user.emailVerified === true) && (features & 0x00000040)) { x += '<input type=button value="' + "Email" + '" title="' + "Send a email message to this user" + '" onclick=showSendEmail("' + encodeURIComponentEx(user._id) + '") />'; }
12494 + if (!self && (activeSessions > 0)) {
12495 + x += '<input type=button value="' + "Notify" + '" title="' + "Send user notification" + '" onclick=showUserAlertDialog(event,"' + encodeURIComponentEx(user._id) + '") />';
12496 + x += '<input type=button value="' + "Chat" + '" title="' + "Chat" + '" onclick=userChat(event,"' + encodeURIComponentEx(user._id) + '","' + encodeURIComponentEx(user.name) + '") />';
12497 + }
12498
12499 // Setup the panel
12500 QH('p30html', x);
views/messenger.handlebars
+2 -1
@@ -4,6 +4,7 @@
4 <title>{{{title}}} - Messenger</title>
5 <meta http-equiv=X-UA-Compatible content="IE=edge">
6 <meta content="text/html;charset=utf-8" http-equiv=Content-Type>
7 + <meta name="viewport" content="user-scalable=1.0,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0" />
8 <meta name=format-detection content="telephone=no">
9 <meta name="robots" content="noindex,nofollow">
10 <link type="text/css" href="styles/style.css" media="screen" rel="stylesheet" title="CSS" />
@@ -24,7 +25,7 @@
25 </div>
26 <div style="padding-top:9px;padding-left:6px;font-size:20px;display:inline-block"><b><span id="xtitle">MeshMessenger</span></b></div>
27 </div>
27 - <div id="xmiddle" style="position:absolute;left:0;right:0;top:38px;bottom:30px">
28 + <div id="xmiddle" style="position:absolute;left:0;right:0;top:38px;bottom:30px;font-size:16px">
29 <div id="xmsgparent" style="position:absolute;left:0;right:0;top:0;bottom:0;overflow-y:scroll">
30 <div id="xmsg" style="padding:5px"></div>
31 </div>