diff --git a/apps/messagegui/ChangeLog b/apps/messagegui/ChangeLog index 455f963438..d341295cbe 100644 --- a/apps/messagegui/ChangeLog +++ b/apps/messagegui/ChangeLog @@ -114,4 +114,5 @@ 0.83: Add option to not open the first unread message 0.84: Fix: Assign show message entry to the settings menu and not the message itself. 0.85: Use new Rebble fonts if available - Remove workaround for 2v10 (>3 years ago) - assume everyone is on never firmware now \ No newline at end of file +0.86: Refactor to display a scroller with all messages loaded. + Initial scroll position is at the chosen message. diff --git a/apps/messagegui/app.js b/apps/messagegui/app.js index 31b7c95dcb..b5c06a902f 100644 --- a/apps/messagegui/app.js +++ b/apps/messagegui/app.js @@ -46,9 +46,10 @@ if (Graphics.prototype.setFontIntl) { fontVLarge = noScale?"Intl":"Intl:3"; } -var active; // active screen (undefined/"list"/"music"/"map"/"message"/"scroller"/"settings") +var active; // active screen (undefined/"list"/"music"/"map"/"overview"/"scroller"/"settings") var openMusic = false; // go back to music screen after we handle something else? var replying = false; // If we're replying to a message, don't interrupt +var persist = "messagegui.app.js" === global.__FILE__; /** this is a timeout if the app has started and is showing a single message but the user hasn't seen it (eg no user input) - in which case @@ -78,7 +79,7 @@ var onMessagesModified = function(type,msg) { } if (msg && msg.id=="nav" && msg.t=="modify" && active!="map") return; // don't show an updated nav message if we're just in the menu - showMessage(msg&&msg.id, false); + showMessageRouter(msg, persist, "dependsOnActive"); }; Bangle.on("message", onMessagesModified); @@ -87,6 +88,40 @@ function saveMessages() { } E.on("kill", saveMessages); +function showMessageRouter(msg, persist, explicitDestnation) { + //explicitDestnation (undefined/"scroller"/"overview"/"dependsOnActive") + + ////var active; // active screen (undefined/"list"/"music"/"map"/"overview"/"scroller"/"settings") + //if (active==undefined) { } else if (active=="list") ... //and so on. + + if (persist) {cancelReloadTimeout();} else if (Bangle.isLocked()) {resetReloadTimeout();} + + if (msg.id=="music") { + cancelReloadTimeout(); // don't auto-reload to clock now + return showMusicMessage(msg); + } + if (msg.id=="nav") { + cancelReloadTimeout(); // don't auto-reload to clock now + return showMapMessage(msg); + } + if (msg.id=="call") { + return showMessageOverview(msg.id); + } + if ("scroller"===explicitDestnation) { + return showMessagesScroller(msg); + } + if ("overview"===explicitDestnation) { + return showMessageOverview(msg.id); + } + if ("dependsOnActive"===explicitDestnation) { + if ("scroller"===active) {return showMessagesScroller(msg);} // reinit scroller with updated messages list. + if ("list"===active) {return returnToMain();} + if ("settings"===active || "overview"===active) {return;} + } + return showMessagesScroller(msg); + //if (false) {showMessageSettings(msg);} +} + function showMapMessage(msg) { active = "map"; require("messages").stopBuzz(); // stop repeated buzzing while the map is showing @@ -234,44 +269,253 @@ function showMusicMessage(msg) { }, 400); } -function showMessageScroller(msg) { - cancelReloadTimeout(); +function showMessagesScroller(msg) { + const MSG_IDX = msg ? MESSAGES.findIndex((m)=>m.id==msg.id) : undefined; + + if (replying) { return; } active = "scroller"; + + const WU = 1!==process.env.HWVERSION ? require("widget_utils") : undefined; + WU&&WU.hide(); + const APP_RECT = Bangle.appRect; + var bodyFont = fontBig; g.setFont(bodyFont); - var lines = []; - if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10); - var titleCnt = lines.length; - if (titleCnt) lines.push(""); // add blank line after title - lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]); + const FONT_HEIGHT = g.getFontHeight(); + let initScrollIdx; + var titleLines = []; + let allLines = [" "]; + let firstTitleLinePerMsg = []; + let footerImgNeg, footerImgPos; + for (let i=0 ; i"; + footerImgPos = atob("QRABAAAAAAAH//+AAAAABgP//8AAAAADgf//4AAAAAHg4ABwAAAAAPh8APgAAAAAfj+B////////geHv///////hf+f///////GPw///////8cGBwAAAAAPx/gDgAAAAAfD/gHAAAAAA8DngOAAAAABwDHP8AAAAADACGf4AAAAAAAAM/w=="); + } else if (msg.positive) { + //footer[0] += ((!msg.negative)?" ".repeat(6):"") + " " + "-".repeat(4) + ">"; + footerImgPos = atob("QRABAAAAAAAAAAOAAAAABgAAA8AAAAADgAAD4AAAAAHgAAPgAAAAAPgAA+AAAAAAfgAD4///////gAPh///////gA+D///////AD4H//////8cPgAAAAAAPw8+AAAAAAAfB/4AAAAAAA8B/gAAAAAABwB+AAAAAAADAB4AAAAAAAAABgAA=="); + } + if (!footer) { + footer = ["-".repeat(12)]; + } + lines = lines.concat(g.wrapString(msgIter.body, APP_RECT.w-10), + footer); + allLines = allLines.concat(lines); + } + + if (allLines.length == 0) { + cancelReloadTimeout(); + returnToClockIfEmpty(); + } + + let shownScrollIdxFirst = allLines.length; + let shownScrollIdxLast = 0; + E.showScroller({ - h : g.getFontHeight(), // height of each menu item in pixels - c : lines.length, // number of menu items + scroll : initScrollIdx*FONT_HEIGHT, + h : FONT_HEIGHT, // height of each menu item in pixels + c : allLines.length, // number of menu items // a function to draw a menu item - draw : function(idx, r) { - // FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12 - g.setBgColor(idx=lines.length-2) - showMessage(msg.id, true); + draw : function(scrollIdx, r) {"ram"; + //print(scrollIdx) + g.setBgColor(titleLines.find(e=>e==scrollIdx)!==undefined ? g.theme.bg2 : g.theme.bg). + setColor(titleLines.find(e=>e==scrollIdx)!==undefined ? g.theme.fg2 : g.theme.fg). + clearRect(r); + g.setFont(bodyFont).setFontAlign(0,-1).drawString(allLines[scrollIdx], r.x+r.w/2, r.y); + if (allLines[scrollIdx]==="") { + g. + setColor("#f00").drawImage(footerImgNeg,r.x+5+3,r.y). + setColor("#0f0").drawImage(footerImgPos,r.w-64-5,r.y); + } + if (allLines[scrollIdx]===" ") { + g. + setColor("#f00").drawImage(atob("GBiBAAAYAAH/gAf/4A//8B//+D///D///H/P/n+H/n8P/n4f/vwAP/wAP34f/n8P/n+H/n/P/j///D///B//+A//8Af/4AH/gAAYAA=="), r.x, r.y-1). + setColor(g.theme.fg2).drawImage(atob("GBgBABgAAf+AB//gD//wH//4P//8P//8fAA+fAA+f//+f//+/AA//AA/f//+f//+fAA+fAA+P//8P//8H//4D//wB//gAf+AABgA"),r.w-24,r.y-1); + } + if (scrollIdxshownScrollIdxLast) {shownScrollIdxLast = scrollIdx;} + }, + select : function(scrollIdx, touch) { + if (touch && touch.type===2) {return;} + const MSG_SELECTED_IDX = findSelectedMsgIdx(scrollIdx); + const MSG_SELECTED = MESSAGES[MSG_SELECTED_IDX]; + WU&&WU.show(); + + print(process.memory().free); + E.showScroller(); + Bangle.removeListener("swipe", Bangle.swipeHandler); + Bangle.removeListener("touch", touchHandler); + print(process.memory().free); + updateReadMessages(); + delete titleLines, allLines; + if (!touch || touch.type===0) { + setTimeout(()=>{ + showMessageSettings(MSG_SELECTED) + },0); + } + //print(touch) + if (touch && touch.type.swipeLR!=0) { + //print("select swipe") + updateReadMessages(); + if (touch.type.swipeLR>0 && posHandler) {posHandler(MSG_SELECTED);} + if (touch.type.swipeLR<0 && negHandler) {negHandler(MSG_SELECTED);} + } }, - back : () => showMessage(msg.id, true) + back: function () { + if (!persist) { return load(); } + Bangle.removeListener("touch", touchHandler); + updateReadMessages(); + WU&&WU.show(); + returnToMain(); + } }); + + //// Additional input handling to extend that of E.showScroller //// + + // Input handling for positive and negative response via swipes. + // Emits a touch event that in turn triggers the select function of E.showScroller above. + Bangle.swipeHandler = (lr) => { + if (lr) {Bangle.emit("touch", 1, {x:Math.floor(APP_RECT.x2/2), y:Math.floor(APP_RECT.y2/2), type:{swipeLR:lr}});} + }; + Bangle.on("swipe", Bangle.swipeHandler); + + // Add an external back touch handler. + // Emits a new touch event that in turn triggers the select function of E.showScroller above. + let touchHandler = (button, xy)=>{ + // if ((left side of Banlge 1 screen) || (top left corner of Bangle 2 screen)) + if ((!xy && 1===button) || (xy && xy.type===0 && xy.x<30 && xy.y<30)) { + if (!persist) {return load();} + WU&&WU.show(); + returnToMain(); + E.stopEventPropagation(); + Bangle.removeListener("touch", touchHandler); + } else if (xy && xy.type===0 && xy.x>175-30 && xy.y<30) { + Bangle.emit("touch", 2, {x:Math.floor(APP_RECT.x2/2), y:Math.floor(APP_RECT.y2/2), type:0}); + E.stopEventPropagation(); + Bangle.removeListener("touch", touchHandler); + } + }; + Bangle.prependListener("touch", touchHandler); + + //// Helper functions //// + + // helper function for message selection + let findSelectedMsgIdx = function(scrollIdx) { + for (let i = firstTitleLinePerMsg.length - 1; i >= 0; i--) { + if (scrollIdx >= firstTitleLinePerMsg[i]) { + return i; + } + } + } + + // Helper function to update new status of messages + function updateReadMessages() { + let shownMsgIdxFirst, shownMsgIdxLast; + const LINES_PER_SCREEN = APP_RECT.h/FONT_HEIGHT; + //print(firstTitleLinePerMsg) + //print(shownIdxFirst, shownIdxLast) + + for (let i=0; i{ + //print("negHandler") + msg.new = false; + cancelReloadTimeout(); // don't auto-reload to clock now + Bangle.messageResponse(msg,false); + returnToCheckMessages(); + }; + //footer.push({type:"img",src:atob("PhAB4A8AAAAAAAPAfAMAAAAAD4PwHAAAAAA/H4DwAAAAAH78B8AAAAAA/+A/AAAAAAH/Af//////w/gP//////8P4D///////H/Af//////z/4D8AAAAAB+/AfAAAAAA/H4DwAAAAAPg/AcAAAAADwHwDAAAAAA4A8AAAAAAAA=="),col:"#f00",cb:negHandler}); + } + if (msg.reply && reply) { + //print("posHandler reply") + posHandler = (msg)=>{ + replying = true; + msg.new = false; + cancelReloadTimeout(); // don't auto-reload to clock now + reply.reply({msg: msg}) + .then(result => { + Bluetooth.println(JSON.stringify(result)); + replying = false; + returnToCheckMessages(); + }) + .catch(() => { + replying = false; + showMessagesScroller(msg); + }); + }; + //footer.push({type:"img",src:atob("QRABAAAAAAAH//+AAAAABgP//8AAAAADgf//4AAAAAHg4ABwAAAAAPh8APgAAAAAfj+B////////geHv///////hf+f///////GPw///////8cGBwAAAAAPx/gDgAAAAAfD/gHAAAAAA8DngOAAAAABwDHP8AAAAADACGf4AAAAAAAAM/w=="),col:"#0f0", cb:posHandler}); + } + else if (msg.positive) { + posHandler = (msg)=>{ + //print("posHandler") + msg.new = false; + cancelReloadTimeout(); // don't auto-reload to clock now + Bangle.messageResponse(msg,true); + returnToCheckMessages(); + }; + //footer.push({type:"img",src:atob("QRABAAAAAAAAAAOAAAAABgAAA8AAAAADgAAD4AAAAAHgAAPgAAAAAPgAA+AAAAAAfgAD4///////gAPh///////gA+D///////AD4H//////8cPgAAAAAAPw8+AAAAAAAfB/4AAAAAAA8B/gAAAAAABwB+AAAAAAADAB4AAAAAAAAABgAA=="),col:"#0f0",cb:posHandler}); + } } function showMessageSettings(msg) { active = "settings"; var menu = {"":{ "title":/*LANG*/"Message", - back:() => showMessage(msg.id, true) + back:() => showMessagesScroller(msg) }, }; if (msg.id!="music") - menu[/*LANG*/"View Message"] = () => showMessageScroller(msg); + menu[/*LANG*/"View Message"] = () => showMessagesScroller(msg); if (msg.reply && reply) { menu[/*LANG*/"Reply"] = () => { @@ -280,11 +524,11 @@ function showMessageSettings(msg) { .then(result => { Bluetooth.println(JSON.stringify(result)); replying = false; - showMessage(msg.id); + showMessagesScroller(msg); }) .catch(() => { replying = false; - showMessage(msg.id); + showMessagesScroller(msg); }); }; } @@ -328,9 +572,8 @@ function showMessageSettings(msg) { E.showMenu(menu); } -function showMessage(msgid, persist) { +function showMessageOverview(msgid) { if (replying) { return; } - if(!persist) resetReloadTimeout(); let idx = MESSAGES.findIndex(m=>m.id==msgid); var msg = MESSAGES[idx]; if (updateLabelsInterval) { @@ -338,15 +581,7 @@ function showMessage(msgid, persist) { updateLabelsInterval=undefined; } if (!msg) return returnToClockIfEmpty(); // go home if no message found - if (msg.id=="music") { - cancelReloadTimeout(); // don't auto-reload to clock now - return showMusicMessage(msg); - } - if (msg.id=="nav") { - cancelReloadTimeout(); // don't auto-reload to clock now - return showMapMessage(msg); - } - active = "message"; + active = "overview"; // Normal text message display var title=msg.title, titleFont = fontLarge, lines; var body=msg.body, bodyFont = fontLarge; @@ -414,7 +649,7 @@ function showMessage(msgid, persist) { .catch(() => { replying = false; layout.render(); - showMessage(msg.id); + showMessagesScroller(msg); }); }; footer.push({type:"img",src:atob("QRABAAAAAAAH//+AAAAABgP//8AAAAADgf//4AAAAAHg4ABwAAAAAPh8APgAAAAAfj+B////////geHv///////hf+f///////GPw///////8cGBwAAAAAPx/gDgAAAAAfD/gHAAAAAA8DngOAAAAABwDHP8AAAAADACGf4AAAAAAAAM/w=="),col:"#0f0", cb:posHandler}); } @@ -444,7 +679,7 @@ function showMessage(msgid, persist) { ]}, {type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2, cb:()=>{ // allow tapping to show a larger version - showMessageScroller(msg); + showMessagesScroller(msg); } }, {type:"h",fillx:1, c: footer} ]},{back:goBack}); @@ -452,8 +687,8 @@ function showMessage(msgid, persist) { Bangle.swipeHandler = (lr,ud) => { if (lr>0 && posHandler) posHandler(); if (lr<0 && negHandler) negHandler(); - if (ud>0 && idx0) showMessage(MESSAGES[idx-1].id, true); + if (ud>0 && idx0) showMessageOverview(MESSAGES[idx-1].id); }; Bangle.on("swipe", Bangle.swipeHandler); g.reset().clearRect(Bangle.appRect); @@ -490,8 +725,8 @@ function checkMessages(options) { // If we have a new message, show it if (!options.ignoreUnread && newMessages.length) { delete newMessages[0].show; // stop us getting stuck here if we're called a second time - showMessage(newMessages[0].id, false); - // buzz after showMessage, so being busy during layout doesn't affect the buzz pattern + showMessageRouter(newMessages[0], persist); + // buzz after showMessagesScroller, so being busy during scroller setup doesn't affect the buzz pattern if (global.BUZZ_ON_NEW_MESSAGE) { // this is set if we entered the messages app by loading `messagegui.new.js` // ... but only buzz the first time we view a new message @@ -501,9 +736,6 @@ function checkMessages(options) { } return; } - // no new messages: show playing music? Only if we have playing music, or state=="show" (set by messagesmusic) - if (options.openMusic && MESSAGES.some(m=>m.id=="music" && ((m.track && m.state=="play") || m.state=="show"))) - return showMessage('music', true); // no new messages - go to clock? if (options.clockIfAllRead && newMessages.length==0) return load(); @@ -512,7 +744,7 @@ function checkMessages(options) { E.showScroller({ h : 50, c : Math.max(MESSAGES.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) - draw : function(idx, r) {"ram" + draw : function(idx, r) {"ram"; var msg = MESSAGES[idx]; if (msg && msg.new) g.setBgColor(g.theme.bgH).setColor(g.theme.fgH); else g.setBgColor(g.theme.bg).setColor(g.theme.fg); @@ -552,13 +784,13 @@ function checkMessages(options) { }, select : idx => { if (idx < MESSAGES.length) - showMessage(MESSAGES[idx].id, true); + showMessageRouter(MESSAGES[idx], true, "scroller"); }, back : () => load() }); } -function returnToCheckMessages(clock) { +function returnToCheckMessages() { checkMessages({clockIfNoMsg:1,clockIfAllRead:1,ignoreUnread:settings.ignoreUnread,openMusic}); } @@ -599,8 +831,14 @@ setTimeout(() => { }, 10); // if checkMessages wants to 'load', do that /* If the Bangle is unlocked by the user, treat that -as a queue to stop repeated buzzing */ +as a queue to stop repeated buzzing. +Also suspend the reload timeout while the watch is unlocked. */ Bangle.on('lock',locked => { - if (!locked) + if (!locked) { require("messages").stopBuzz(); + cancelReloadTimeout(); + } + if (locked) { + if (!persist) {resetReloadTimeout();} + } }); diff --git a/apps/messagegui/metadata.json b/apps/messagegui/metadata.json index e0f47c6bc3..1f804ad56f 100644 --- a/apps/messagegui/metadata.json +++ b/apps/messagegui/metadata.json @@ -2,7 +2,7 @@ "id": "messagegui", "name": "Message UI", "shortName": "Messages", - "version": "0.85", + "version": "0.86", "description": "Default app to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app",