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

[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