~emersion/public-inbox

gamja: Don't mark messages as read when window is not in focus v4 PROPOSED

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(-)
#1021731 .build.yml success
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.



          
          
          
        
      

      
      
      
      
      
      
      
      

      

      
      
      
      
    
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH gamja v4 1/2] Don't mark messages as read when window is not in focus Export this patch

---
 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!

[PATCH gamja v4 2/2] Show number of unread buffers or messages in title Export this patch

---
 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
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.