~emersion/public-inbox

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH] Store and recall last 5 messages per buffer with arrow keys

Details
Message ID
<20220216111231.517099-1-senan@senan.xyz>
DKIM signature
missing
Download raw message
Patch: +67 -1
Hi! This patch let's you use the arrow keys up/down to recall the last
5 messages sent on a per-buffer basis. The lines are kept in a new localStorage
key. (I can also reuse one of the existing localStorage keys if you like)

A side-effect of this change (and actually lead to simpler implementation)
is that unsent message drafts are stored as well. Eg. you can start typing
a message a message to a user, then switch to a different buffer or even
reload gamja, then go back to the buffer and sent the message.

(this required that a `key` prop is passed to <Composer /> so that we get a
unique render per buffer, giving it a chance to fetch from localStorage again)

Thanks!
---
 components/app.js      |  6 ++++++
 components/composer.js | 27 ++++++++++++++++++++++++++-
 store.js               | 35 +++++++++++++++++++++++++++++++++++
 3 files changed, 67 insertions(+), 1 deletion(-)

diff --git a/components/app.js b/components/app.js
index cde062a..46a5deb 100644
--- a/components/app.js
+++ b/components/app.js
@@ -222,6 +222,7 @@ export default class App extends Component {
		this.handleVerifySubmit = this.handleVerifySubmit.bind(this);

		this.bufferStore = new store.Buffer();
		this.sentStore = new store.SentLines();

		configPromise.then((config) => {
			this.handleConfig(config);
@@ -1888,11 +1889,16 @@ export default class App extends Component {
			</>
			${memberList}
			<${Composer}
				key=${activeBuffer.id}
				ref=${this.composer}
				readOnly=${composerReadOnly}
				onSubmit=${this.handleComposerSubmit}
				autocomplete=${this.autocomplete}
				commandOnly=${commandOnly}

				sentGet=${() => this.sentStore.get(String(activeBuffer.id))}
				sentPush=${(text) => this.sentStore.push(String(activeBuffer.id), text)}
				sentUpdate=${(text) => this.sentStore.update(String(activeBuffer.id), text)}
			/>
			${dialog}
			${error}
diff --git a/components/composer.js b/components/composer.js
index f029381..9c1162c 100644
--- a/components/composer.js
+++ b/components/composer.js
@@ -3,6 +3,7 @@ import { html, Component, createRef } from "../lib/index.js";
export default class Composer extends Component {
	state = {
		text: "",
		sentLinesIndex: 0,
	};
	textInput = createRef();
	lastAutocomplete = null;
@@ -15,11 +16,18 @@ export default class Composer extends Component {
		this.handleInputKeyDown = this.handleInputKeyDown.bind(this);
		this.handleWindowKeyDown = this.handleWindowKeyDown.bind(this);
		this.handleWindowPaste = this.handleWindowPaste.bind(this);

		// populate the initial text input from the store if we have it
		this.state.text = props.sentGet()?.[this.state.sentLinesIndex] || "";
	}

	handleInput(event) {
		this.setState({ [event.target.name]: event.target.value });

		if (event.target.name === "text") {
			this.props.sentUpdate(event.target.value);
		}

		if (this.props.readOnly && event.target.name === "text" && !event.target.value) {
			event.target.blur();
		}
@@ -28,10 +36,26 @@ export default class Composer extends Component {
	handleSubmit(event) {
		event.preventDefault();
		this.props.onSubmit(this.state.text);
		this.setState({ text: "" });
		this.props.sentPush("");
		this.setState({ text: "", sentLinesIndex: 0 });
	}

	handleLoadSentLine(diff) {
		const i = this.state.sentLinesIndex + diff;
		const lines = this.props.sentGet();
		if (i >= lines.length || i < 0) return;

		this.setState({ text: lines[i], sentLinesIndex: i });
	}

	handleInputKeyDown(event) {
		switch (event.key) {
			case "ArrowUp":
				return this.handleLoadSentLine(+1);
			case "ArrowDown":
				return this.handleLoadSentLine(-1);
		}

		let input = event.target;

		if (!this.props.autocomplete || event.key !== "Tab") {
@@ -114,6 +138,7 @@ export default class Composer extends Component {
		this.lastAutocomplete = autocomplete;

		this.setState({ text: autocomplete.text });
		this.props.sentUpdate(autocomplete.text);
	}

	handleWindowKeyDown(event) {
diff --git a/store.js b/store.js
index 6bf31aa..d288e56 100644
--- a/store.js
+++ b/store.js
@@ -146,3 +146,38 @@ export class Buffer {
		this.save();
	}
}

export class SentLines {
	raw = new Item("sent_lines");
	m = null;

	constructor() {
		const obj = this.raw.load();
		this.m = new Map(Object.entries(obj || {}));
	}

	save() {
		this.raw.put(Object.fromEntries(this.m));
	}

	get(buf) {
		return this.m.get(buf) || [];
	}

	push(buf, text) {
		if (!this.m.has(buf)) {
			this.m.set(buf, []);
		}
		this.m.get(buf).unshift(text);
		this.m.get(buf).splice(5);
		this.save();
	}

	update(buf, text) {
		if (!this.m.has(buf)) {
			this.m.set(buf, []);
		}
		this.m.get(buf)[0] = text;
		this.save();
	}
}
-- 
2.35.1
Details
Message ID
<_7n_rMfI9hnhYEAwngHSD4R5bCnTM6SH6kkObS0eW1m7AdjinWtqcwQKcGnsVT5z6LUZBhI2sUtzJv5xTRX_C-1NDiR3WwkNhkF2APATgE0=@emersion.fr>
In-Reply-To
<20220216111231.517099-1-senan@senan.xyz> (view parent)
DKIM signature
missing
Download raw message
On Wednesday, February 16th, 2022 at 12:12, sentriz <senan@senan.xyz> wrote:

> Hi! This patch let's you use the arrow keys up/down to recall the last
> 5 messages sent on a per-buffer basis. The lines are kept in a new localStorage
> key. (I can also reuse one of the existing localStorage keys if you like)

Hm, I don't think this approach will work correctly. The buffer IDs are
ephemeral, they're only valid for a single session and will become invalid as
soon as the page is unloaded. If the user joins a new channel, leaves a
channel, or messages another user with another client, the IDs will all get
mixed up.

Thankfully we already have a solution for this: store.Buffer. I think we could
just add a composerHistory field in there.

Btw, I'm perfectly fine with not using localStorage at first, if that
simplifies things.

> A side-effect of this change (and actually lead to simpler implementation)
> is that unsent message drafts are stored as well. Eg. you can start typing
> a message a message to a user, then switch to a different buffer or even
> reload gamja, then go back to the buffer and sent the message.

That's pretty nice!

> (this required that a `key` prop is passed to <Composer /> so that we get a
> unique render per buffer, giving it a chance to fetch from localStorage again)
> ---
>  components/app.js      |  6 ++++++
>  components/composer.js | 27 ++++++++++++++++++++++++++-
>  store.js               | 35 +++++++++++++++++++++++++++++++++++
>  3 files changed, 67 insertions(+), 1 deletion(-)
>
> diff --git a/components/app.js b/components/app.js
> index cde062a..46a5deb 100644
> --- a/components/app.js
> +++ b/components/app.js
> @@ -222,6 +222,7 @@ export default class App extends Component {
>  		this.handleVerifySubmit = this.handleVerifySubmit.bind(this);
>
>  		this.bufferStore = new store.Buffer();
> +		this.sentStore = new store.SentLines();
>
>  		configPromise.then((config) => {
>  			this.handleConfig(config);
> @@ -1888,11 +1889,16 @@ export default class App extends Component {
>  			</>
>  			${memberList}
>  			<${Composer}
> +				key=${activeBuffer.id}
>  				ref=${this.composer}
>  				readOnly=${composerReadOnly}
>  				onSubmit=${this.handleComposerSubmit}
>  				autocomplete=${this.autocomplete}
>  				commandOnly=${commandOnly}
> +
> +				sentGet=${() => this.sentStore.get(String(activeBuffer.id))}
> +				sentPush=${(text) => this.sentStore.push(String(activeBuffer.id), text)}
> +				sentUpdate=${(text) => this.sentStore.update(String(activeBuffer.id), text)}

Hm, this doesn't feel very react-ish... Not sure how to improve that...

>  			/>
>  			${dialog}
>  			${error}
> diff --git a/components/composer.js b/components/composer.js
> index f029381..9c1162c 100644
> --- a/components/composer.js
> +++ b/components/composer.js
> @@ -3,6 +3,7 @@ import { html, Component, createRef } from "../lib/index.js";
>  export default class Composer extends Component {
>  	state = {
>  		text: "",
> +		sentLinesIndex: 0,
>  	};
>  	textInput = createRef();
>  	lastAutocomplete = null;
> @@ -15,11 +16,18 @@ export default class Composer extends Component {
>  		this.handleInputKeyDown = this.handleInputKeyDown.bind(this);
>  		this.handleWindowKeyDown = this.handleWindowKeyDown.bind(this);
>  		this.handleWindowPaste = this.handleWindowPaste.bind(this);
> +
> +		// populate the initial text input from the store if we have it
> +		this.state.text = props.sentGet()?.[this.state.sentLinesIndex] || "";
>  	}
>
>  	handleInput(event) {
>  		this.setState({ [event.target.name]: event.target.value });
>
> +		if (event.target.name === "text") {
> +			this.props.sentUpdate(event.target.value);
> +		}
> +
>  		if (this.props.readOnly && event.target.name === "text" && !event.target.value) {
>  			event.target.blur();
>  		}
> @@ -28,10 +36,26 @@ export default class Composer extends Component {
>  	handleSubmit(event) {
>  		event.preventDefault();
>  		this.props.onSubmit(this.state.text);
> -		this.setState({ text: "" });
> +		this.props.sentPush("");
> +		this.setState({ text: "", sentLinesIndex: 0 });
> +	}
> +
> +	handleLoadSentLine(diff) {
> +		const i = this.state.sentLinesIndex + diff;
> +		const lines = this.props.sentGet();
> +		if (i >= lines.length || i < 0) return;
> +
> +		this.setState({ text: lines[i], sentLinesIndex: i });
>  	}
>
>  	handleInputKeyDown(event) {
> +		switch (event.key) {
> +			case "ArrowUp":
> +				return this.handleLoadSentLine(+1);
> +			case "ArrowDown":
> +				return this.handleLoadSentLine(-1);

Nit: handle* functions are named like this because they handle events. Here
handleLoadSentLine doesn't handle any event, so something like "loadSentLine"
would be a better name. Also switch cases are indented at the same level as the
switch statement.

> +		}
> +
>  		let input = event.target;
>
>  		if (!this.props.autocomplete || event.key !== "Tab") {
> @@ -114,6 +138,7 @@ export default class Composer extends Component {
>  		this.lastAutocomplete = autocomplete;
>
>  		this.setState({ text: autocomplete.text });
> +		this.props.sentUpdate(autocomplete.text);
>  	}
>
>  	handleWindowKeyDown(event) {
Reply to thread Export thread (mbox)