Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

messagegui: refactor the messages scroller and use it more (WIP) #3685

Draft
wants to merge 24 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
79d16e6
messagegui: refactor to a scroller handling many messages simultaneously
Jan 3, 2025
43e343a
messagegui: small refactor
Jan 3, 2025
6884b5c
messagegui: small refactor
Jan 4, 2025
3ba1ccc
messagegui: store some func calls to vars for reuse
Jan 4, 2025
ca3e41a
messagegui: fix lint warn and some logic
Jan 4, 2025
a6eb336
messagegui: refactoring...
Jan 4, 2025
a8133ad
messagegui: refactoring more
Jan 4, 2025
a3c9841
messagegui: more refinement, some refactoring
Jan 12, 2025
3445f7f
messagegui: tweaks and refactor
Jan 22, 2025
155cf87
messagegui: fix clear hw btn handler, etc
Jan 24, 2025
6c939fa
messagegui: reset unread timeout when scrolling
Jan 24, 2025
9807d60
messagegui: retain persist bool info on scroller reinit
Jan 25, 2025
31a6c55
messagegui: fix some logic.
Jan 25, 2025
bb92ddd
messagegui: persist on msgs modified depends on __FILE__
Jan 25, 2025
f1c74ec
messagegui: make sure to cancel unread timeout if persist
Jan 25, 2025
d6e9c9c
messagegui: fix open music interface on select from list
Jan 25, 2025
d2d0bb5
messagegui: tweaks
Jan 25, 2025
457c260
messagegui: suspend unread timeout when unlocked
Jan 29, 2025
6e07024
messagegui: remove unused parameter
Jan 29, 2025
53ef0c3
messagegui: simpler logic for the scroller
Jan 31, 2025
41934ba
messagegui: refactor more to make scroller logic simpler
Feb 1, 2025
6b6f79f
messagegui: small change
Feb 1, 2025
e6bf0d7
messagegui: add FIXME comment
Feb 2, 2025
447f150
messagegui: improve logic re what messages to be marked `new=false`
Feb 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/messagegui/ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,4 @@
0.82: Stop buzzing when a message is removed (e.g. call answered)
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: (WIP) refactor to display a scroller with three messages loaded. When scrolling to the top or end new/older messages are loaded in.
218 changes: 171 additions & 47 deletions apps/messagegui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ 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
// hack for 2v10 firmware's lack of ':size' font handling
Expand Down Expand Up @@ -90,7 +90,8 @@ 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);
let persist = "messagegui.new.js"===global.__FILE__?false:true;
showMessageRouter(msg, persist, "dependsOnActive");
};
Bangle.on("message", onMessagesModified);

Expand All @@ -99,6 +100,39 @@ 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()}

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, persist);
}
if ("scroller"===explicitDestnation) {
return showMessagesScroller(msg, persist);
}
if ("overview"===explicitDestnation) {
return showMessageOverview(msg.id, persist);
}
if ("dependsOnActive"===explicitDestnation) {
if ("scroller"===active) {return showMessagesScroller(msg, persist);} // reinit scroller with updated messages list.
if ("list"===active) {return returnToMain();}
if ("settings"===active || "overview"===active) {return;}
}
//if (false) {showMessageSettings(msg);}
}

function showMapMessage(msg) {
active = "map";
require("messages").stopBuzz(); // stop repeated buzzing while the map is showing
Expand Down Expand Up @@ -246,44 +280,136 @@ function showMusicMessage(msg) {
}, 400);
}

function showMessageScroller(msg) {
cancelReloadTimeout();
function showMessagesScroller(msg, persist) {
if (persist===undefined) {persist = true;}
const MSG_IDX = msg ? MESSAGES.findIndex((m)=>m.id==msg.id) : undefined;

if (replying) { return; }
active = "scroller";
if (persist) {cancelReloadTimeout();} else {resetReloadTimeout();}

const WU = require("widget_utils");
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 initScroll;
var titleLines = [];
let allLines = [];
let firstTitleLinePerMsg = [];
for (let i=0 ; i<MESSAGES.length ; i++) {
if (MSG_IDX === i) {initScroll = allLines.length*FONT_HEIGHT}
let msgIter = MESSAGES[i];

var lines = [];
const TITLE_STRING = msgIter.title||msgIter.sender||msgIter.subject||msgIter.src||/*LANG*/"No Title";
lines = g.wrapString(TITLE_STRING, APP_RECT.w-10);
for (let i=0; i<lines.length; i++) {
titleLines.push(i + allLines.length);
if (i==0) firstTitleLinePerMsg.push(i + allLines.length);
}
lines = lines.concat(g.wrapString(msgIter.body, APP_RECT.w-10),
["-".repeat(12)]);
allLines = allLines.concat(lines);
}

if (allLines.length == 0) {
cancelReloadTimeout();
returnToClockIfEmpty();
}

let shownIdxFirst = allLines.length
let shownIdxLast = 0;

E.showScroller({
h : g.getFontHeight(), // height of each menu item in pixels
c : lines.length, // number of menu items
scroll : initScroll,
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<titleCnt ? g.theme.bg2 : g.theme.bg).
setColor(idx<titleCnt ? g.theme.fg2 : g.theme.fg).
clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
g.setFont(bodyFont).setFontAlign(0,-1).drawString(lines[idx], r.x+r.w/2, r.y);
}, select : function(idx) {
if (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 (scrollIdx<shownIdxFirst) {shownIdxFirst = scrollIdx;}
if (scrollIdx>shownIdxLast) {shownIdxLast = scrollIdx;}
if (!persist) {resetReloadTimeout();}
},
back : () => showMessage(msg.id, true)
select : function(scrollIdx, touch) {
WU.show();
for (let i=firstTitleLinePerMsg.length-1; i>=0 ; i--) {
if (scrollIdx>=firstTitleLinePerMsg[i]) {
if (!touch || touch.type===0) {
showMessageRouter(MESSAGES[i], true, "overview")
} else if (touch.type == 2) {
showMessageSettings(MESSAGES[i]);
}
break;
}
}
clearBtnHandler();
updateReadMessages();
}
});


// If Bangle.js 2 add an external select hw button handler.
let btnHandler = ((2===process.env.HWVERSION) && (setWatch(()=>{
Bangle.emit("drag", {dy:0}); // Compatibility with `kineticscroll`, stopping the scroller so it doesn't continue scrolling when the `showMessageOverview` screen is loaded.
// Zero ms timeout as to not move on before the scroller has registered the emitted drag event.
setTimeout(()=>{
if ("messagegui.new.js"===global.__FILE__) {return load();}
Bangle.emit("touch", 1, {x:APP_RECT.x2/2, y:APP_RECT.y2/2, type:0});
},0);
}, BTN, {edge:'rising', repeat:true})));

function clearBtnHandler() {
if (btnHandler) {clearWatch(btnHandler); btnHandler=undefined;}
}

function updateReadMessages() {
let shownMsgIdxFirst, shownMsgIdxLast;
const LINES_PER_SCREEN = APP_RECT.h/FONT_HEIGHT;
//print(firstTitleLinePerMsg)
//print(shownIdxFirst, shownIdxLast)

for (let i=0; i<firstTitleLinePerMsg.length-1 ; i++) {

if (shownIdxFirst<=firstTitleLinePerMsg[i] && shownIdxFirst+LINES_PER_SCREEN>firstTitleLinePerMsg[i]) {
shownMsgIdxFirst = i;
}

if (shownIdxLast>=firstTitleLinePerMsg[i+1] && shownIdxLast-LINES_PER_SCREEN<firstTitleLinePerMsg[i+1]) {
shownMsgIdxLast = i;
//print(i)
}

}
if (shownIdxLast===allLines.length-1) {shownMsgIdxLast = MESSAGES.length-1;}

//print(shownIdxFirst, shownIdxLast)
//print(shownMsgIdxFirst, shownMsgIdxLast)
//print(MESSAGES)
for (let i=shownMsgIdxFirst; i<shownMsgIdxLast+1; i++) {
MESSAGES[i].new = false;
}
//print(MESSAGES)
}
}

function showMessageSettings(msg) {
active = "settings";
var menu = {"":{
"title":/*LANG*/"Message",
back:() => showMessage(msg.id, true)
back:() => showMessageOverview(msg.id, true)
},
};

if (msg.id!="music")
menu[/*LANG*/"View Message"] = () => showMessageScroller(msg);
menu[/*LANG*/"View Message"] = () => showMessagesScroller(msg);

if (msg.reply && reply) {
menu[/*LANG*/"Reply"] = () => {
Expand All @@ -292,11 +418,11 @@ function showMessageSettings(msg) {
.then(result => {
Bluetooth.println(JSON.stringify(result));
replying = false;
showMessage(msg.id);
showMessageOverview(msg.id);
})
.catch(() => {
replying = false;
showMessage(msg.id);
showMessageOverview(msg.id);
});
};
}
Expand Down Expand Up @@ -340,7 +466,7 @@ function showMessageSettings(msg) {
E.showMenu(menu);
}

function showMessage(msgid, persist) {
function showMessageOverview(msgid, persist) {
if (replying) { return; }
if(!persist) resetReloadTimeout();
let idx = MESSAGES.findIndex(m=>m.id==msgid);
Expand All @@ -350,15 +476,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;
Expand Down Expand Up @@ -426,7 +544,7 @@ function showMessage(msgid, persist) {
.catch(() => {
replying = false;
layout.render();
showMessage(msg.id);
showMessageOverview(msg.id);
});
}; footer.push({type:"img",src:atob("QRABAAAAAAAH//+AAAAABgP//8AAAAADgf//4AAAAAHg4ABwAAAAAPh8APgAAAAAfj+B////////geHv///////hf+f///////GPw///////8cGBwAAAAAPx/gDgAAAAAfD/gHAAAAAA8DngOAAAAABwDHP8AAAAADACGf4AAAAAAAAM/w=="),col:"#0f0", cb:posHandler});
}
Expand Down Expand Up @@ -456,16 +574,16 @@ 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});

Bangle.swipeHandler = (lr,ud) => {
if (lr>0 && posHandler) posHandler();
if (lr<0 && negHandler) negHandler();
if (ud>0 && idx<MESSAGES.length-1) showMessage(MESSAGES[idx+1].id, true);
if (ud<0 && idx>0) showMessage(MESSAGES[idx-1].id, true);
if (ud>0 && idx<MESSAGES.length-1) showMessageOverview(MESSAGES[idx+1].id, true);
if (ud<0 && idx>0) showMessageOverview(MESSAGES[idx-1].id, true);
};
Bangle.on("swipe", Bangle.swipeHandler);
g.reset().clearRect(Bangle.appRect);
Expand Down Expand Up @@ -502,8 +620,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
showMessagesScroller(newMessages[0], false);
// 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
Expand All @@ -515,7 +633,7 @@ function checkMessages(options) {
}
// 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);
return showMessageOverview('music', true);
// no new messages - go to clock?
if (options.clockIfAllRead && newMessages.length==0)
return load();
Expand All @@ -524,7 +642,7 @@ function checkMessages(options) {
E.showScroller({
h : 48,
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);
Expand Down Expand Up @@ -564,13 +682,13 @@ function checkMessages(options) {
},
select : idx => {
if (idx < MESSAGES.length)
showMessage(MESSAGES[idx].id, true);
showMessageRouter(MESSAGES[idx], true, "overview");
},
back : () => load()
});
}

function returnToCheckMessages(clock) {
function returnToCheckMessages() {
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,ignoreUnread:settings.ignoreUnread,openMusic});
}

Expand Down Expand Up @@ -611,8 +729,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 ("messagegui.new.js"===global.__FILE__) {resetReloadTimeout();}
}
});
2 changes: 1 addition & 1 deletion apps/messagegui/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "messagegui",
"name": "Message UI",
"shortName": "Messages",
"version": "0.84",
"version": "0.85",
"description": "Default app to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",
Expand Down