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
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) {