~emersion/public-inbox

gamja: Don't mark messages as read when window is not in focus v3 SUPERSEDED

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, 182 insertions(+), 32 deletions(-)
#1021729 .build.yml failed
gamja/patches/.build.yml: FAILED in 19s

[Don't mark messages as read when window is not in focus][0] v3 from [sitting33][1]

[0]: https://lists.sr.ht/~emersion/public-inbox/patches/42524
[1]: mailto:me@sit.sh

✗ #1021729 FAILED gamja/patches/.build.yml https://builds.sr.ht/~emersion/job/1021729
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/42524/mbox | git am -3
Learn more about email & git

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

---
 components/app.js | 72 +++++++++++++++++++++++++++++++----------------
 1 file changed, 48 insertions(+), 24 deletions(-)

diff --git a/components/app.js b/components/app.js
index 3987749..89f3ee6 100644
--- a/components/app.js
+++ b/components/app.js
@@ -228,6 +228,7 @@ export default class App extends Component {
		this.handleSettingsChange = this.handleSettingsChange.bind(this);
		this.handleSettingsDisconnect = this.handleSettingsDisconnect.bind(this);
		this.handleSwitchSubmit = this.handleSwitchSubmit.bind(this);
		this.handleDocumentFocus = this.handleDocumentFocus.bind(this);

		this.state.settings = {
			...this.state.settings,
@@ -528,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);
			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 };
		}, () => {
	markBufferAsRead(id) {
		this.setState(state => {
			const buf = State.getBuffer(state, 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) {
@@ -576,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);
@@ -590,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, true);
		});
	}

@@ -717,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);
@@ -1912,13 +1928,21 @@ export default class App extends Component {
		}
	}

	handleDocumentFocus() {
		this.markBufferAsRead(this.state.activeBuffer, false);
	}

	componentDidMount() {
		this.baseTitle = document.title;
		setupKeybindings(this);

		window.addEventListener("focus", this.handleDocumentFocus);
	}

	componentWillUnmount() {
		document.title = this.baseTitle;

		window.removeEventListener("focus", this.handleDocumentFocus);
	}

	render() {
-- 
2.41.0

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

---
 components/app.js           | 99 ++++++++++++++++++++++++++++++++++---
 components/settings-form.js | 42 ++++++++++++++++
 state.js                    |  1 +
 3 files changed, 134 insertions(+), 8 deletions(-)

diff --git a/components/app.js b/components/app.js
index 89f3ee6..bab59ec 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();
		});
	}

@@ -609,6 +611,65 @@ export default class App extends Component {
		});
	}

	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 +815,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 +1219,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 +1296,8 @@ export default class App extends Component {
					closed,
					receipts: { [ReceiptType.READ]: readReceipt },
				});

				this.updateDocumentTitle();
			});
			break;
		default:
@@ -1913,7 +1994,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: FAILED in 19s

[Don't mark messages as read when window is not in focus][0] v3 from [sitting33][1]

[0]: https://lists.sr.ht/~emersion/public-inbox/patches/42524
[1]: mailto:me@sit.sh

✗ #1021729 FAILED gamja/patches/.build.yml https://builds.sr.ht/~emersion/job/1021729