sitting33: 2 Don't mark messages as read when window is not in focus Show number of unread buffers or messages in title 4 files changed, 175 insertions(+), 38 deletions(-)
gamja/patches/.build.yml: SUCCESS in 52s [Don't mark messages as read when window is not in focus][0] v4 from [sitting33][1] [0]: https://lists.sr.ht/~emersion/public-inbox/patches/42525 [1]: mailto:me@sit.sh ✓ #1021731 SUCCESS gamja/patches/.build.yml https://builds.sr.ht/~emersion/job/1021731
I've split this into multiple commits, and left out the config option for now (hardcoded to only show number of unread highlights). Would prefer to keep it simple if possible. Thanks! Do you know if there's a better way to detect a change in the "unread" field of any buffer? It feels wrong to just call updateDocumentTitle() in each call-site which might change an "unread" field.
The only way I see is to refactor markBufferAsRead() to use setBufferState(), and then call updateDocumentTitle() there. I believe it should be possible. But this will result in updating the title every time a buffer's state is changed, and also feels very janky, since setBufferState() seems more of a data-layer method than a UI one.
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~emersion/public-inbox/patches/42525/mbox | git am -3Learn more about email & git
--- components/app.js | 65 ++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/components/app.js b/components/app.js index 69098d9..59c6c44 100644 --- a/components/app.js +++ b/components/app.js @@ -529,33 +529,13 @@ export default class App extends Component { client.setReadMarker(storedBuffer.name, readReceipt.time); } - switchBuffer(id) { - let buf; - this.setState((state) => { - buf = State.getBuffer(state, id); + markBufferAsRead(id) { + this.setState(state => { + const buf = State.getBuffer(state, id); if (!buf) { return; } - let client = this.clients.get(buf.server); - let stored = this.bufferStore.get({ name: buf.name, server: client.params }); - let prevReadReceipt = getReceipt(stored, ReceiptType.READ); - // TODO: only mark as read if user scrolled at the bottom - let update = State.updateBuffer(state, buf.id, { - unread: Unread.NONE, - prevReadReceipt, - }); - - return { ...update, activeBuffer: buf.id }; - }, () => { - if (!buf) { - return; - } - - if (this.buffer.current) { - this.buffer.current.focus(); - } - let client = this.clients.get(buf.server); for (let notif of this.messageNotifications) { @@ -577,6 +557,38 @@ export default class App extends Component { } } + return State.updateBuffer(state, buf.id, { + unread: Unread.NONE, + }); + }); + } + + switchBuffer(id) { + let buf; + this.setState((state) => { + buf = State.getBuffer(state, id); + if (!buf) { + return; + } + + let client = this.clients.get(buf.server); + let stored = this.bufferStore.get({ name: buf.name, server: client.params }); + let prevReadReceipt = getReceipt(stored, ReceiptType.READ); + + let update = State.updateBuffer(state, buf.id, { + prevReadReceipt, + }); + + return { ...update, activeBuffer: buf.id }; + }, () => { + if (!buf) { + return; + } + + if (this.buffer.current) { + this.buffer.current.focus(); + } + let server = this.state.servers.get(buf.server); if (buf.type === BufferType.NICK && !server.users.has(buf.name)) { this.whoUserBuffer(buf.name, buf.server); @@ -591,6 +603,9 @@ export default class App extends Component { } else { document.title = this.baseTitle; } + + // TODO: only mark as read if user scrolled at the bottom + this.markBufferAsRead(buf.id); }); } @@ -718,7 +733,7 @@ export default class App extends Component { let prevReadReceipt = buf.prevReadReceipt; let receipts = { [ReceiptType.DELIVERED]: receiptFromMessage(msg) }; - if (this.state.activeBuffer !== buf.id) { + if (this.state.activeBuffer !== buf.id || !document.hasFocus()) { unread = Unread.union(unread, msgUnread); } else { receipts[ReceiptType.READ] = receiptFromMessage(msg); @@ -1919,6 +1934,8 @@ export default class App extends Component { for (let client of this.clients.values()) { client.send({ command: "PING", params: ["gamja"] }); } + + this.markBufferAsRead(this.state.activeBuffer); } componentDidMount() { -- 2.41.0
Sorry for the massive delay. I've split this into 2 patches and done some minor edits (to avoid doing a nested setState() call from a setState() completion callback for instance). Thanks!
--- components/app.js | 105 +++++++++++++++++++++++++++++++----- components/settings-form.js | 42 +++++++++++++++ state.js | 1 + 3 files changed, 134 insertions(+), 14 deletions(-) diff --git a/components/app.js b/components/app.js index 59c6c44..2838c5a 100644 --- a/components/app.js +++ b/components/app.js @@ -560,6 +560,8 @@ export default class App extends Component { return State.updateBuffer(state, buf.id, { unread: Unread.NONE, }); + }, () => { + this.updateDocumentTitle(); }); } @@ -598,17 +600,70 @@ export default class App extends Component { this.whoChannelBuffer(buf.name, buf.server); } - if (buf.type !== BufferType.SERVER) { - document.title = buf.name + ' · ' + this.baseTitle; - } else { - document.title = this.baseTitle; - } - // TODO: only mark as read if user scrolled at the bottom this.markBufferAsRead(buf.id); }); } + updateDocumentTitle() { + const currentBuffer = State.getBuffer(this.state, this.state.activeBuffer); + const currentServerName = getServerName(this.state.servers.get(currentBuffer.server)); + + let resultingTitle = ""; + + // add unread counter + let titleCounterSetting = this.state.settings.titleCounter; + let unreadCounter = 0; + if (titleCounterSetting === "buffers" || titleCounterSetting === "highlights") { + let minUnread = titleCounterSetting === "highlights" ? Unread.HIGHLIGHT : Unread.MESSAGE; + for (const buffer of this.state.buffers.values()) { + if (Unread.compare(buffer.unread, minUnread) >= 0) { + unreadCounter++; + } + } + } else if (titleCounterSetting === "messages") { + for (const buffer of this.state.buffers.values()) { + if (buffer.unread === Unread.NONE) { + continue; + } + + const client = this.clients.get(buffer.server); + const stored = this.bufferStore.get({name: buffer.name, server: client.params}); + const lastReadReceipt = getReceipt(stored, ReceiptType.READ); + + // go through every message starting at the bottom until we reach + // a message that's already been read + let bufferUnreadCounter = 0; + let msgIndex = buffer.messages.length - 1; + let msg = buffer.messages[msgIndex]; + while (msg && !isMessageBeforeReceipt(msg, lastReadReceipt)) { + // these are messages that can trigger a buffer to be marked as unread + if (["PRIVMSG", "NOTICE", "INVITE"].includes(msg.command)) { + bufferUnreadCounter++; + } + msg = buffer.messages[--msgIndex]; + } + + unreadCounter += Math.max(bufferUnreadCounter, 1); + } + } + if (unreadCounter > 0) { + resultingTitle += `(${unreadCounter}) `; + } + + // add current buffer name + if (currentBuffer.type !== BufferType.SERVER) { + resultingTitle += currentBuffer.name + ' · '; + } + + resultingTitle += currentServerName + ' · '; + + // add the base title (document.title) + resultingTitle += this.baseTitle; + + document.title = resultingTitle; + } + prepareChatMessage(serverID, msg) { // Treat server-wide broadcasts as highlights. They're sent by server // operators and can contain important information. @@ -754,6 +809,11 @@ export default class App extends Component { this.sendReadReceipt(client, stored); } return { unread, prevReadReceipt }; + }, () => { + // don't update the title if we're getting the history + if (!irc.findBatchByType(msg, "chathistory")) { + this.updateDocumentTitle(); + } }); } @@ -1153,17 +1213,30 @@ export default class App extends Component { } let name = msg.params[0].slice(1); let batch = client.batches.get(name); - if (!batch || batch.type !== "soju.im/bouncer-networks") { + if (!batch) { break; } - // We've received a BOUNCER NETWORK batch. If we have a URL to - // auto-open and no existing network matches it, ask the user to - // create a new network. - if (this.autoOpenURL && this.autoOpenURL.host && !this.findBouncerNetIDByHost(this.autoOpenURL.host)) { - this.openURL(this.autoOpenURL); - this.autoOpenURL = null; + switch (batch.type) { + case "chathistory": + // Since the component hasn't updated because of the new + // messages yet, we need to update the title *after* the + // new state has been applied. + this.setState({}, () => { + this.updateDocumentTitle(); + }) + break; + case "soju.im/bouncer-networks": + // We've received a BOUNCER NETWORK batch. If we have a URL to + // auto-open and no existing network matches it, ask the user to + // create a new network. + if (this.autoOpenURL && this.autoOpenURL.host && !this.findBouncerNetIDByHost(this.autoOpenURL.host)) { + this.openURL(this.autoOpenURL); + this.autoOpenURL = null; + } + break; } + break; case "MARKREAD": target = msg.params[0]; @@ -1217,6 +1290,8 @@ export default class App extends Component { closed, receipts: { [ReceiptType.READ]: readReceipt }, }); + + this.updateDocumentTitle(); }); break; default: @@ -1913,7 +1988,9 @@ export default class App extends Component { handleSettingsChange(settings) { store.settings.put(settings); - this.setState({ settings }); + this.setState({ settings }, () => { + this.updateDocumentTitle(); + }); } handleSettingsDisconnect() { diff --git a/components/settings-form.js b/components/settings-form.js index 31e045e..3044eb1 100644 --- a/components/settings-form.js +++ b/components/settings-form.js @@ -8,6 +8,7 @@ export default class SettingsForm extends Component { this.state.secondsInTimestamps = props.settings.secondsInTimestamps; this.state.bufferEvents = props.settings.bufferEvents; + this.state.titleCounter = props.settings.titleCounter; this.handleInput = this.handleInput.bind(this); this.handleSubmit = this.handleSubmit.bind(this); @@ -67,6 +68,47 @@ export default class SettingsForm extends Component { </label> <br/><br/> + <label> + <input + type="radio" + name="titleCounter" + value="none" + checked=${this.state.titleCounter === "none"} + /> + Don't show unread counter in page title + </label> + <br/> + <label> + <input + type="radio" + name="titleCounter" + value="highlights" + checked=${this.state.titleCounter === "highlights"} + /> + Show number of highlights in page title + </label> + <br/> + <label> + <input + type="radio" + name="titleCounter" + value="buffers" + checked=${this.state.titleCounter === "buffers"} + /> + Show number of all unread buffers in page title + </label> + <br/> + <label> + <input + type="radio" + name="titleCounter" + value="messages" + checked=${this.state.titleCounter === "messages"} + /> + Show number of all unread messages in page title + </label> + <br/><br/> + <label> <input type="radio" diff --git a/state.js b/state.js index 1c1bc5a..ff25d47 100644 --- a/state.js +++ b/state.js @@ -213,6 +213,7 @@ export const State = { settings: { secondsInTimestamps: true, bufferEvents: BufferEventsDisplayMode.FOLD, + titleCounter: "highlights", }, }; }, -- 2.41.0
builds.sr.ht <builds@sr.ht>gamja/patches/.build.yml: SUCCESS in 52s [Don't mark messages as read when window is not in focus][0] v4 from [sitting33][1] [0]: https://lists.sr.ht/~emersion/public-inbox/patches/42525 [1]: mailto:me@sit.sh ✓ #1021731 SUCCESS gamja/patches/.build.yml https://builds.sr.ht/~emersion/job/1021731
I've split this into multiple commits, and left out the config option for now (hardcoded to only show number of unread highlights). Would prefer to keep it simple if possible. Thanks! Do you know if there's a better way to detect a change in the "unread" field of any buffer? It feels wrong to just call updateDocumentTitle() in each call-site which might change an "unread" field.