feature: Thread improvements, round 1 #74

Open
Guzio wants to merge 7 commits from Guzio/out-of-your-element:mergable-fr-fr into main
First-time contributor

This PR contains the 1st round of improvements to thread UX

As promised some time ago in OOYE's Matrix space.

Some note about terminology: Over the course of this description, I'll refer to threads that appear on Discord as attached to a message as „attached” or „branched” (tho I tried to use only the 2nd term in the code itself), whereas threads that were created using the button as „standalone”, „detached” or „headless”.

To remind everyone, the conversation that resulted in this PR, went more-or-less like this:

Identified issues:

The discussion begun when someone pointed out a possible thread UX improvement: icon change.

My reply:

I can think of one more: Create „dummy” threads on Matrix side and handle in-Matrix thread creation.

In the next message, an explanation why and what would they do:

The current OOYE thread implementation has 2 issues:

  • It's non-trivial to backwards-map thread-rooms back to their original rooms/channels.
  • People may unknowingly create threads on Matrix, and then not have them bridged to Discord.

In the next message, an elaboration on point 1:

First issue, elaborated: When you look at all those thread-rooms in this space, you have no way of telling which one originated from what room/channel. Likewise, when you're in a room, you have no way of telling what threads does it have, unless you explicitly search for /thread or something. The first scenario (not knowing from where a thread came) may impact communication, when a given thread title may mean something different in different contexts. For example, I accidentally joined the "interaction loading" thread, thinking it originated from ooyelement, as a technical discussion about bridging interactions (like "Channel gating" originated from ooyelement, as a technical discussion about, well, channel gating), only to discover that it was someone's error from help-getting-started. Another (now hypothetical) situation I can think of is a person joining a "pics" thread, thinking it's about IRL photos, without knowing that it's a thread in the Minecraft channel/room, and being hit with in-game screenshots instead. This isn't a big deal, but it just looks somewhat awkward to join a thread, realise it's not your thing, and then leave without saying a word. Knowing threads' origins may help to somewhat limit this. The second scenario (not knowing that a given room has threads) is purely a parity thing. On Discord, you can tell it at a glance (you have a convenient button). So you should be able to see that on Matrix, too.

In the next message, an elaboration on point 2:

Second issue is self-explanatory. Just look at help-getting-stalrted. A lot of people create threads for their issues there. But these are Matrix-only threads, so Discord users can't help in them. And - crucially - the bot provides no feedback at all, that a given thread is not gonna be bridged to Discord, so unless you already know about /thread, you wouldn't have any idea that some people in the community have no way of seeing your messages.

Proposed solution (starting with my own message):

I think this could be implemented in stages, from simplest contributions to most complex.

  • Change the emoji to 🧵
  • Add, in thread-room descriptions, the channel/room from which it originated. Unfortunately, you can't always see the description of a Matrix room prior to joining, so this isn't the best solution to the "joined a wrong thread" problem - for that, read till the end.
  • Make it so that the bot warns the Matrix user, whenever they create a thread, that this thread cannot be bridged and that they should use /thread instead.
  • Elaborate on the previous mechanic, by - instead of directing the user to a command (which, really, introduces an extra stage of friction for no obvious reason) - it just auto-creates a thread-room for you, and instead links to it directly from the Matrix thread, along with a note that you should text there instead, as anything you send on a Matrix thread won't be bridged.
  • To fix the "joined the wrong room" problem once and for all, and to improve thread discoverability from within a Matrix room, the previous mechanic could also be expanded to also have the bot auto-create a Matrix "dummy" thread every time one on Discord is created, and inside it, put the same message as specified previously (that you shouldn't talk here and visit the room at the link instead).

Note that this message is also kinda a timeline for how I'd implement this. Because I tbh want to work on this right now, given how the exam session is finally over and I have some time.

Some time later, Cadence mentioned a UX concern:

Thread titles. [HINDSIGHT: I had no idea why would this be a problem („Just reuse the thread name from Matrix!”, I thought), but now I know that the problem is that Matrix DOESN'T HAVE thread names. Hence this was, indeed, a UX problem.]

Bonus round - what to do about /thread with no args:

The discussion begun when someone accidentally ran /thread without args.

Ellie noted:

miiiiiiight be a good feature to allow unnamed threads :p

My reply:

I'm already messing around with threads on my fork (for all its worth, the first series of patches is almost ready, I think - just haven't gotten around to PRing it yet [HINDSIGHT: Boy, could I be more wrong! 😅]), so I'll take care of it, if I may.

Some time later:

I can't really think of a good UX for auto-determining thread names when /thread is called without arguments, so I think I'll just make it return a help-usage message instead of an "ugly error".

Cadence disagreed:

thread names can be changed, maybe you could set a placeholder initially and then update the thread name after the next message is sent in it or something idk
either is fine

...And so did Ellie:

I'd just name it "untitled thread", yeah

Out of this, the following were implemented (or not and why):

From the „Proposed solution” message exchange:

  • Emojis were unchanged, as per Cadence's later request (a poll was supposed to be conducted, but I guess she forgot, or ended up using her ultimate authority to overrule against any emoji changes after further considerations, or maybe her expectation was that I'll run that poll while I was expecting her to, idk - at any rate, I don't mind that there was no poll, as I'm generally neutral on this and I think both are fine, only having included the change in my proposal because someone else asked for it).
  • No change to in-thread descriptions. Any changes to description-related logic would be entirely independent from whatever I did here, so it can go into the „second round” of patches (this IS called „round 1” after all).
  • The bot, indeed, does warn the user now. Except that this does not happen when the user creates a thread, as there apparently is no such concept on Matrix. There is no distinct „thread created” event, with title'n'stuff, like on Discord - instead, the thread just kinda comes to exist when you reply in it. So instead, I send the warning every time you send a message in the thread. I was unsure, at first, whether this is a good idea because that would possibly be annoying to people typing in threads. On the other hand - when you send a message in a thread, you are being annoying to people in Discord and to people using clients with no full support for threads (eg. ElementX, Commet or Cinny - they just render in-thread messages as replies directly on the main timeline, except they give you a little „this is a thread, btw” marker), so the message is effectively supposed to „annoy you back” and also hopefully annoy you into compliance.
  • After further consideration, the bot will not auto-create a Discord thread upon Matrix thread creation. This is mainly due to the issue described above, ie. the fact that there is no such thing as thread creation. The UX I was envisioning for this was basically „a more native way to call /thread”. Basically - instead of a command, you use a button in your client, and the bot responds to that action by doing exactly what you asked for. (Seeing it through that lenses, having the bot tell you „that's not how you do threads!!1!” would indeed be „an extra stage of friction for no obvious reason”: you just created a thread, why do you have to create it again, but differently?) But given that there is no such button, the equation is now somewhat different: you didn't create a thread, you already sent a message in it. You already did „the bad thing”, due to how threads look somewhat broken on various clients and on Discord (as stated above). Now, the role would be to educate you to not do it again - hence the message whose role is to „annoy you into compliance” is sent. Crucially, that message isn't just annoying, but also informative. It teaches you how to use /thread. The idea is that after 2 or 3 times, people will naturally resort to /thread right away, without the intermediate step of sending an in-thread message. If we were to auto-create a thread instead, then we'd teach the users to rely on that mechanism, which would lead to them always annoying Discord/Cinny/ElementX/Commet/etc. users with at least a single message (the one that they used for thread creation). This 1000% must be revisited if Matrix ever introduces a system for creating „standalone” threads (like you can have on Discord), but until then, I think that this annoy-but-also-educate approach is the better choice. I am, however, open for discussion.
  • The „dummy thread” mechanic is indeed there, tho in a somewhat different way than what was proposed originally - its scope is kinda both broader and narrower at the same time. I'll elaborate more later.

From the „Identified issues” message exchange:

  • as-stated-above - no changes to emoji; dummy-threads will be elaborated on later
  • The discoverability problem is now partially solved with dummy threads. As stated above, there are still no changes to thread descriptions, tho. However, as I said in the „Proposed solution” section, the ultimate solution to that problem isn't thread descriptions, but dummy threads. These are there already. Backlinks in thread descriptions still make sense, and I'd love to add support for them eventually, but they're no longer as essential as they would've been without dummy threads.
  • The „people create Matrix threads, while unaware of /thread” problem is fixed by the fact that now there's a message that tells you that you're doing threads wrong.

From the „Bonus round”:

I settled on a middle-ground solution:

  • If /thread is ran standalone, ie. in such a way that the thread would've branched from /thread itself (more on that later), it shows the help message.
  • ...otherwise, it auto-creates the name based on the message the thread would’ve branched from.

I decided to keep the „show help message” logic on standalone because:

  • Tho thread titles can be changed, there is no guarantee that they will be changed by the thread creator (either because they didn't know that this was an option, or they just didn't care). I'm worried this may eventually lead to a „20+ threads called "New thread" after the mods were asleep”-type situation.
  • There would otherwise be no place for a help message (maybe in the long term, an /ooyehelp command, or something like that, could be added - but for now, there are only 2 cmds, so it would be silly to make a whole help system for just that).

The „dummy thread” system and branching explained:

So... It's time to address the „more on that later” from paragraphs above. Basically - the previous UX was „you run /thread <title> (no matter where/how) -> the bot creates a new standalone thread on Discord and sends a notification on the main room timeline” or „someone creates any thread (headless or otherwise) -> the bot bridges a Matrix room to it and sends a notification on the main room timeline”. Now, the system is more sophisticated:

For threads that originated on Discord...

  • The bot will try (if possible) to send the notification as an in-thread reply on Matrix. This way, that „thread being attached to a message” UI piece is also reflected on Matrix, if your client supports it. If it doesn't, the notification will at least appear as a reply, which is still an improvement, as previously there was no way to „backtrack” threads to their original message at all (which may be a problem if eg. there were multiple convos happening at once, and everyone decided to branch off into their thread, but the thread titles were unclear). Same applies to Discord threads appearing under the thread list button. Now, the that will also happen on the Matrix side, if your client supports it - this improves your ability to discover threads some time later after they were created, while also letting you find what message they branched from. Overall - indexing improvements! Yay!
  • There is one caveat, tho (I did say „the bot wil try”, not „the bot will”): This will not happen if the Discord thread is standalone, simply due to the fact that there is no message to branch the thread from. And as I painfully discovered above, there is no way to have a headless thread on Matrix. Maybe OOYE could be sending an m.notice, to have that tiny notice-bar like there's on Discord, and then it could branch the thread from it with its standard m.emote? Maaaayyybeeee, but then clients that don't support threads well will be hit with 2 messages that basically say the same thing, just in 2 different styles. Not ideal. Once again, this is something that should be revisited if Matrix ever introduces a system for creating „standalone” threads.

For threads that originated on Matrix (as in: /thread was ran)...

  • If /thread [name] was a reply, then the new thread is branched (on both Discord and Matrix - this is the same for all other branching cases from now on, so I won't be repeating that) from the thread to which /thread was replying. You can - but not have to - specify a name. This ensures parity with
  • If /thread <name> was ran on the main room timeline, and not as a reply, then a thread with the specified name is created on Discord, and it will branch from the very message that was this command. I'm aware that branching from a command may look somewhat goofy, but by branching from anything, we get all the discoverability benefits that were specified above in „For threads that originated on Discord...” - and if we have to branch from something, then the command that created the thread is the least atrocious choice.
  • If /thread was ran on the main room timeline, and not as a reply, then (as established) help is shown.
  • If /thread [name] was ran in a thread - see below...

For threads that originated on Matrix (as in: were created there)...

Well, we already know what happens! Because you can just create a thread on Matrix - you must send a message - the bot will scream at you for sending messages in threads, which looks bad for both some Matrix users and on Discord. The devil is in the details - you're directed to either:

  • Run /thread in that thread. No arguments, no nothing (tho you can specify a thread name, it's just not needed). If you do that, the created thread will act as if you ran /thread as a reply to the message from which the Matrix thread branched from. It doesn't matter whether it's the 1st message in the thread or 1000th; doesn't matter if it's a reply or not. If ran in a Matrix thread - branch the created Discord thread from the message from which the Matrix thread already is branching. Also, the message tells you that there are other ways to run /thread and you can discover them by running /thread (without args; not as a reply) on the main room timeline.
  • If there already exists a thread-room and a Discord thread for the message from which /thread above would've branched - you're simply linked to it and directed to talk there.

It also handles errors!

  • If /thread would've created branched from a message that already has a thread-room branched to a thread on Discord, it directs you there instead of erroring out.
  • As already said at least 2 times, it doesn't error out if a title is too short (ie. missing), but rather infers it if possible, or otherwise gives the help message.
  • It handles too-long titles, too! If it was inferred from the message contents, it simply truncates the title to fit in the character limit. If you specified the title yourself, thread creation fails, but not with an „ugly error”, but a note that you talk too much.
  • It doesn't give you an „ugly error” if you create a thread-in-a-thread, but rather tells you why threads can't be created there.

Auxiliary changes / The structure of this PR

I tried to structure my changes in such a way that no commit squashing is needed when merging-in this PR, by using „standard” commit names (ie. in present tense and short) and also grouping multiple changes into „logical” squash-commits on my end. This is the result of all of that:

1st commits:

Changes to .gitignore, as per Cadence's request. Not much to document here, all of this was basically verbatim instruction-following from OOYE's space/server.

2nd and 3rd commits:

These were mostly focused on type changes. This was before I realised that squashing is a thing (I'm sorry, I'm not exactly all that proficient in Git; so far having only used it for mostly personal projects where a clean tree isn't all that necessary), so these are unfortunately spread across 2 commits. Anyway, these changes were fairly minor, hence the „Small” in the 2nd commit title - one of them was to simply add a couple of @ts-ignores in places where TS was a bit silly, and the other 2 were:

Support of the m.thread sub-type of m.relates_to:

Implemented entirely within the 3rd commit. Notably, said commit included the following blunders:

  • I accidentally referred to m.thread as m.replace in the commit message. This makes no sense - m.replace already was supported. The line that says „To support "rel_type":"m.replace" relation-events (added "m.replace" option to existing key "rel_type" and a new "is_falling_back" key)” should actually be „To support "rel_type":"m.thread" relation-events (added "m.thread" option to existing key "rel_type" and a new "is_falling_back" key)” but I must've been to sleepy-or-something to notice.
  • Somehow, I called a booleanbool” in a .d.ts file. And TS didn't flag it (.d.tses treat all type errors as implicit anys). This was later corrected in the next commit, but the damage's been done - there is now an embarrassing mistake in the commit log. Whoops! 😅

Overall, between that broken commit message, and the fact that type changes are spread over 2 commits for no good reason - it makes me think that the best course of action would be to squash those two particular commits (to prevent needless commit-spam and to override that wonked message on the 3rd commit) while preserving the 1st and last as a distinct entry. I'm not sure, however, if this is possible. If it is - great! If not - well, I'm worried that it may jeopardize this PR's ability to get merged without complete squashing, even tho I tried to organise the commits nicely (except this blunder). Tho I guess it's on me for writing so long commit descriptions that I get attached to them; no sane person puts so much stuff in there as my serial-yapping ass.

Narrowing-down of of guard()'s type definition.

I'll admit, I had no reason to touch this. But I did, anyway. It was in the early „messing around, getting a feel for the codebase” stage and I'll admit that I may have gone a bit off the rails. At any rate, type was restricted to a {string} and fn was restricted to a function that takes in 2 anys and returns an any. This is a huge improvement over „everything is an any, good luck”, but I nevertheless tried to restrict it further (ideally, limit what parameters that passed-in function has). However, I ran into some difficulties with that, so I left it as any.

In the last commit, I was finally able to get it to behave as I wanted (mark the event parameter as an event-type), but that came at a trade-off of making the type signature look pretty atrocious (I had to manually mark any type-error-inducing keys as any). Also, it makes the type signature be somewhat detached from reality: guard() doesn't really care about the exact type of event - all it needs is for it to have a room_id key and any other key usually associated with Matrix events isn't strictly required.

Basically, the point I'm getting at is: „What if restricting the type of event is a bad idea? Maybe I should've made it as broad as possible (make sure that it's an object with a room_id key, and don't care otherwise)?”. ...Or, you know, I shouldn't've touched a random type that's largely unrelated to my PR. That's also an option. At any rate, I'm open for feedback.

I then did some merges...

I'm not even counting those as real commits, as I'm pretty sure neither does GitDab.

Finally - commit 4:

This is where all the juicy stuff happened. That's the one that contains all the thread UX improvements that make up the core of this PR. Beyond that, it also includes:

  • Further type changes: those specified above (bool vs boolean fixes and more changes to guard()), as well as fixes to d2m thread-to-announcement-converter's const context.
  • Broad changes to matrix-command-handler (please refer to the commit message for a detailed explanation)
  • Extra documentation (as to why threads aren't just bridged as threads). It's currently a verbatim quote of Cadence's stance on threads - might be a good idea to reword/format it a little bit.
  • A new utility func (to convert thread message IDs to thread room IDs).
  • New tests, some changes to old ones, and 2 new rows of synthetic data. All tests that are in any way related to what I did do pass - tho I noticed some 4 sticker-related ones failing. I suspect 9b3707baa1 might be to blame (I merged all new commits into my branch as they appeared on origin, to prevent any potential merge conflicts in the future - so that's how I ended up testing their changes) - it seems like they did a pretty big overhaul to the sticker subsystem, but changes to any test files are notably missing.
# This PR contains the 1st round of improvements to thread UX As promised some time ago in OOYE's Matrix space. *Some note about terminology: Over the course of this description, I'll refer to threads that appear on Discord as attached to a message as „attached” or „branched” (tho I tried to use only the 2nd term in the code itself), whereas threads that were created using the button as „standalone”, „detached” or „headless”.* ## To remind everyone, the conversation that resulted in this PR, went more-or-less like this: ### Identified issues: The discussion begun when someone pointed out a possible thread UX improvement: icon change. #### My reply: I can think of one more: Create „dummy” threads on Matrix side and handle in-Matrix thread creation. #### In the next message, an explanation why and what would they do: The current OOYE thread implementation has 2 issues: * It's non-trivial to backwards-map thread-rooms back to their original rooms/channels. * People may unknowingly create threads on Matrix, and then not have them bridged to Discord. #### In the next message, an elaboration on point 1: First issue, elaborated: When you look at all those thread-rooms in this space, you have no way of telling which one originated from what room/channel. Likewise, when you're in a room, you have no way of telling what threads does it have, unless you explicitly search for /thread or something. The first scenario (not knowing from where a thread came) may impact communication, when a given thread title may mean something different in different contexts. For example, I accidentally joined the "interaction loading" thread, thinking it originated from ooyelement, as a technical discussion about bridging interactions (like "Channel gating" originated from ooyelement, as a technical discussion about, well, channel gating), only to discover that it was someone's error from help-getting-started. Another (now hypothetical) situation I can think of is a person joining a "pics" thread, thinking it's about IRL photos, without knowing that it's a thread in the Minecraft channel/room, and being hit with in-game screenshots instead. This isn't a big deal, but it just looks somewhat awkward to join a thread, realise it's not your thing, and then leave without saying a word. Knowing threads' origins may help to somewhat limit this. The second scenario (not knowing that a given room has threads) is purely a parity thing. On Discord, you can tell it at a glance (you have a convenient button). So you should be able to see that on Matrix, too. #### In the next message, an elaboration on point 2: Second issue is self-explanatory. Just look at help-getting-stalrted. A lot of people create threads for their issues there. But these are Matrix-only threads, so Discord users can't help in them. And - crucially - the bot provides no feedback at all, that a given thread is not gonna be bridged to Discord, so unless you already know about `/thread`, you wouldn't have any idea that some people in the community have no way of seeing your messages. ### Proposed solution *(starting with my own message)*: I think this could be implemented in stages, from simplest contributions to most complex. * Change the emoji to 🧵 * Add, in thread-room descriptions, the channel/room from which it originated. Unfortunately, you can't always see the description of a Matrix room prior to joining, so this isn't the best solution to the "joined a wrong thread" problem - for that, read till the end. * Make it so that the bot warns the Matrix user, whenever they create a thread, that this thread cannot be bridged and that they should use `/thread` instead. * Elaborate on the previous mechanic, by - instead of directing the user to a command (which, really, introduces an extra stage of friction for no obvious reason) - it just auto-creates a thread-room for you, and instead links to it directly from the Matrix thread, along with a note that you should text there instead, as anything you send on a Matrix thread won't be bridged. * To fix the "joined the wrong room" problem once and for all, and to improve thread discoverability from within a Matrix room, the previous mechanic could also be expanded to also have the bot auto-create a Matrix "dummy" thread every time one on Discord is created, and inside it, put the same message as specified previously (that you shouldn't talk here and visit the room at the link instead). Note that this message is also kinda a timeline for how I'd implement this. Because I tbh want to work on this right now, given how the exam session is finally over and I have some time. #### Some time later, Cadence mentioned a UX concern: Thread titles. *[HINDSIGHT: I had no idea why would this be a problem („Just reuse the thread name from Matrix!”, I thought), but now I know that the problem is that Matrix DOESN'T HAVE thread names. Hence this was, indeed, a UX problem.]* ### Bonus round - what to do about `/thread` with no args: The discussion begun when someone accidentally ran `/thread` without args. #### Ellie noted: miiiiiiight be a good feature to allow unnamed threads :p #### My reply: I'm already messing around with threads on my fork (for all its worth, the first series of patches is almost ready, I think - just haven't gotten around to PRing it yet *[HINDSIGHT: Boy, could I be more wrong! 😅]*), so I'll take care of it, if I may. #### Some time later: I can't really think of a good UX for auto-determining thread names when /thread is called without arguments, so I think I'll just make it return a help-usage message instead of an "ugly error". #### Cadence disagreed: thread names can be changed, maybe you could set a placeholder initially and then update the thread name after the next message is sent in it or something idk either is fine #### ...And so did Ellie: I'd just name it "untitled thread", yeah ## Out of this, the following were implemented (or not and why): ### From the „Proposed solution” message exchange: * Emojis were unchanged, as per Cadence's later request (a poll was supposed to be conducted, but I guess she forgot, or ended up using her ultimate authority to overrule against any emoji changes after further considerations, or maybe her expectation was that I'll run that poll while I was expecting her to, idk - at any rate, I don't mind that there was no poll, as I'm generally neutral on this and I think both are fine, only having included the change in my proposal because someone else asked for it). * No change to in-thread descriptions. Any changes to description-related logic would be entirely independent from whatever I did here, so it can go into the „second round” of patches (this IS called „round 1” after all). * The bot, indeed, does warn the user now. Except that this does not happen when the user creates a thread, as there apparently is no such concept on Matrix. There is no distinct „thread created” event, with title'n'stuff, like on Discord - instead, the thread just kinda *comes to exist* when you reply in it. So instead, I send the warning every time you send a message in the thread. I was unsure, at first, whether this is a good idea because that would possibly be annoying to people typing in threads. On the other hand - when you send a message in a thread, *you* are being annoying to people in Discord and to people using clients with no full support for threads (eg. ElementX, Commet or Cinny - they just render in-thread messages as replies directly on the main timeline, except they give you a little „this is a thread, btw” marker), so the message is effectively supposed to „annoy you back” and also hopefully annoy you into compliance. * After further consideration, the bot will not auto-create a Discord thread upon Matrix thread creation. This is mainly due to the issue described above, ie. the fact that *there is no such thing* as thread creation. The UX I was envisioning for this was basically „a more native way to call `/thread`”. Basically - instead of a command, you use a button in your client, and the bot responds to that action by doing exactly what you asked for. (Seeing it through that lenses, having the bot tell you „that's not how you do threads!!1!” would indeed be „an extra stage of friction for no obvious reason”: you just created a thread, why do you have to create it again, but differently?) But given that there is no such button, the equation is now somewhat different: you didn't create a thread, you already sent a message in it. You already did „the bad thing”, due to how threads look somewhat broken on various clients and on Discord (as stated above). Now, the role would be to educate you to not do it again - hence the message whose role is to „annoy you into compliance” is sent. Crucially, that message isn't just annoying, but also informative. It teaches you how to use `/thread`. The idea is that after 2 or 3 times, people will naturally resort to `/thread` right away, without the intermediate step of sending an in-thread message. If we were to auto-create a thread instead, then we'd teach the users to rely on that mechanism, which would lead to them always annoying Discord/Cinny/ElementX/Commet/etc. users with at least a single message (the one that they used for thread creation). This 1000% must be revisited if Matrix ever introduces a system for creating „standalone” threads (like you can have on Discord), but until then, I think that this annoy-but-also-educate approach is the better choice. I am, however, open for discussion. * The „dummy thread” mechanic is indeed there, tho in a somewhat different way than what was proposed originally - its scope is kinda *both broader and narrower at the same time*. I'll elaborate more later. ### From the „Identified issues” message exchange: * as-stated-above - no changes to emoji; dummy-threads will be elaborated on later * The discoverability problem is now partially solved with dummy threads. As stated above, there are still no changes to thread descriptions, tho. However, as I said in the „Proposed solution” section, the ultimate solution to that problem isn't thread descriptions, but dummy threads. These are there already. Backlinks in thread descriptions still make sense, and I'd love to add support for them eventually, but they're no longer as essential as they would've been without dummy threads. * The „people create Matrix threads, while unaware of `/thread`” problem is fixed by the fact that now there's a message that tells you that you're doing threads wrong. ### From the „Bonus round”: I settled on a middle-ground solution: * If `/thread` is ran standalone, ie. in such a way that the thread would've branched from `/thread` itself (more on that later), it shows the help message. * ...otherwise, it auto-creates the name based on the message the thread would’ve branched from. I decided to keep the „show help message” logic on standalone because: * Tho thread titles can be changed, there is no guarantee that they will be changed by the thread creator (either because they didn't know that this was an option, or they just didn't care). I'm worried this may eventually lead to a „20+ threads called "New thread" after the mods were asleep”-type situation. * There would otherwise be no place for a help message (maybe in the long term, an `/ooyehelp` command, or something like that, could be added - but for now, there are only 2 cmds, so it would be silly to make a whole help system for just that). ## The „dummy thread” system and branching explained: So... It's time to address the „more on that later” from paragraphs above. Basically - the previous UX was „you run `/thread <title>` (no matter where/how) -> the bot creates a new standalone thread on Discord and sends a notification on the main room timeline” or „someone creates any thread (headless or otherwise) -> the bot bridges a Matrix room to it and sends a notification on the main room timeline”. Now, the system is more sophisticated: ### For threads that originated on Discord... * The bot will *try* (if possible) to send the notification as an in-thread reply on Matrix. This way, that „thread being attached to a message” UI piece is also reflected on Matrix, if your client supports it. If it doesn't, the notification will at least appear as a reply, which is still an improvement, as previously there was no way to „backtrack” threads to their original message at all (which may be a problem if eg. there were multiple convos happening at once, and everyone decided to branch off into their thread, but the thread titles were unclear). Same applies to Discord threads appearing under the thread list button. Now, the that will also happen on the Matrix side, if your client supports it - this improves your ability to discover threads some time later after they were created, while also letting you find what message they branched from. Overall - indexing improvements! Yay! * There is one caveat, tho (I *did* say „the bot wil try”, not „the bot will”): This will *not* happen if the Discord thread is standalone, simply due to the fact that there is no message to branch the thread from. And as I painfully discovered above, there is no way to have a headless thread on Matrix. Maybe OOYE could be sending an `m.notice`, to have that tiny notice-bar like there's on Discord, and then it could branch the thread from it with its standard `m.emote`? Maaaayyybeeee, but then clients that don't support threads well will be hit with 2 messages that basically say the same thing, just in 2 different styles. Not ideal. Once again, this is something that should be revisited if Matrix ever introduces a system for creating „standalone” threads. ### For threads that originated on Matrix (as in: `/thread` was ran)... * If `/thread [name]` was a reply, then the new thread is branched (on both Discord and Matrix - this is the same for all other branching cases from now on, so I won't be repeating that) from the thread to which `/thread` was replying. You can - but not have to - specify a name. This ensures parity with * If `/thread <name>` was ran on the main room timeline, and not as a reply, then a thread with the specified name is created on Discord, and it will branch from the very message that was this command. I'm aware that branching from a command may look somewhat goofy, but by branching from *anything*, we get all the discoverability benefits that were specified above in „For threads that originated on Discord...” - and if we have to branch from *something*, then the command that created the thread is the least atrocious choice. * If `/thread` was ran on the main room timeline, and not as a reply, then (as established) help is shown. * If `/thread [name]` was ran in a thread - see below... ### For threads that originated on Matrix (as in: were created there)... Well, we already know what happens! Because you can just create a thread on Matrix - you must send a message - the bot will scream at you for sending messages in threads, which looks bad for both some Matrix users and on Discord. The devil is in the details - you're directed to either: * Run `/thread` in that thread. No arguments, no nothing (tho you can specify a thread name, it's just not needed). If you do that, the created thread will act as if you ran `/thread` as a reply to the message from which the Matrix thread branched from. It doesn't matter whether it's the 1st message in the thread or 1000th; doesn't matter if it's a reply or not. If ran in a Matrix thread - branch the created Discord thread from the message from which the Matrix thread already is branching. Also, the message tells you that there are other ways to run `/thread` and you can discover them by running `/thread` (without args; not as a reply) on the main room timeline. * If there already exists a thread-room and a Discord thread for the message from which `/thread` above would've branched - you're simply linked to it and directed to talk there. ### It also handles errors! * If `/thread` would've created branched from a message that already has a thread-room branched to a thread on Discord, it directs you there instead of erroring out. * As already said at least 2 times, it doesn't error out if a title is too short (ie. missing), but rather infers it if possible, or otherwise gives the help message. * It handles too-long titles, too! If it was inferred from the message contents, it simply truncates the title to fit in the character limit. If you specified the title yourself, thread creation fails, but not with an „ugly error”, but a note that you talk too much. * It doesn't give you an „ugly error” if you create a thread-in-a-thread, but rather tells you why threads can't be created there. ## Auxiliary changes / The structure of this PR I tried to structure my changes in such a way that no commit squashing is needed when merging-in this PR, by using „standard” commit names (ie. in present tense and short) and also grouping multiple changes into „logical” squash-commits on my end. This is the result of all of that: ### 1st commits: Changes to `.gitignore`, as per Cadence's request. Not much to document here, all of this was basically verbatim instruction-following from OOYE's space/server. ### 2nd and 3rd commits: These were mostly focused on type changes. This was before I realised that squashing is a thing (I'm sorry, I'm not exactly all that proficient in Git; so far having only used it for mostly personal projects where a clean tree isn't all that necessary), so these are unfortunately spread across 2 commits. Anyway, these changes were fairly minor, hence the „Small” in the 2nd commit title - one of them was to simply add a couple of `@ts-ignore`s in places where TS was a bit silly, and the other 2 were: #### Support of the `m.thread` sub-type of `m.relates_to`: Implemented entirely within the 3rd commit. Notably, said commit included the following blunders: * I accidentally referred to `m.thread` as `m.replace` in the commit message. This makes no sense - `m.replace` already was supported. The line that says „To support "rel_type":"m.replace" relation-events (added "m.replace" option to existing key "rel_type" and a new "is_falling_back" key)” should actually be „To support "rel_type":"m.thread" relation-events (added "m.thread" option to existing key "rel_type" and a new "is_falling_back" key)” but I must've been to sleepy-or-something to notice. * Somehow, I called a `boolean` „`bool`” in a `.d.ts` file. And TS didn't flag it (`.d.ts`es treat all type errors as implicit `any`s). This was later corrected in the next commit, but the damage's been done - there is now an embarrassing mistake in the commit log. *Whoops! 😅* Overall, between that broken commit message, and the fact that type changes are spread over 2 commits for no good reason - it makes me think that the best course of action would be to squash those two particular commits (to prevent needless commit-spam and to override that *wonked* message on the 3rd commit) while preserving the 1st and last as a distinct entry. I'm not sure, however, if this is possible. If it is - great! If not - well, I'm worried that it may jeopardize this PR's ability to get merged without complete squashing, even tho I tried to organise the commits nicely (except this blunder). Tho I guess it's on me for writing so long commit descriptions that I get attached to them; no sane person puts so much stuff in there as my serial-yapping ass. #### Narrowing-down of of `guard()`'s type definition. I'll admit, I had no reason to touch this. But I did, anyway. It was in the early „messing around, getting a feel for the codebase” stage and I'll admit that I may have gone a bit off the rails. At any rate, `type` was restricted to a `{string}` and `fn` was restricted to a function that takes in 2 `any`s and returns an `any`. This is a huge improvement over „everything is an `any`, good luck”, but I nevertheless tried to restrict it further (ideally, limit what parameters that passed-in function has). However, I ran into some difficulties with that, so I left it as `any`. In the last commit, I was finally able to get it to behave as I wanted (mark the `event` parameter as an event-type), but that came at a trade-off of making the type signature look pretty atrocious (I had to manually mark any type-error-inducing keys as `any`). Also, it makes the type signature be somewhat detached from reality: `guard()` doesn't really care about the exact type of `event` - all it needs is for it to have a `room_id` key and any other key usually associated with Matrix events isn't strictly required. Basically, the point I'm getting at is: „What if *restricting* the type of `event` is a bad idea? Maybe I should've made it as broad as possible (make sure that it's an object with a `room_id` key, and don't care otherwise)?”. ...Or, you know, I shouldn't've touched a random type that's largely unrelated to my PR. That's also an option. At any rate, I'm open for feedback. ### I then did some merges... I'm not even counting those as real commits, as I'm pretty sure neither does GitDab. ### Finally - commit 4: This is where all the juicy stuff happened. That's the one that contains all the thread UX improvements that make up the core of this PR. Beyond that, it also includes: * Further type changes: those specified above (bool vs boolean fixes and more changes to `guard()`), as well as fixes to `d2m thread-to-announcement`-converter's `const context`. * Broad changes to `matrix-command-handler` (please refer to the commit message for a detailed explanation) * Extra documentation (as to why threads aren't just bridged as threads). It's currently a verbatim quote of Cadence's stance on threads - might be a good idea to reword/format it a little bit. * A new utility func (to convert thread message IDs to thread room IDs). * New tests, some changes to old ones, and 2 new rows of synthetic data. All tests that are in any way related to what I did *do* pass - tho I noticed some 4 sticker-related ones failing. I suspect https://gitdab.com/cadence/out-of-your-element/commit/9b3707baa1dc7f9308f3152068d69f0678d16d30 might be to blame (I merged all new commits into my branch as they appeared on origin, to prevent any potential merge conflicts in the future - so that's how I ended up testing their changes) - it seems like they did a pretty big overhaul to the sticker subsystem, but changes to any test files are notably missing.
Guzio added 7 commits 2026-03-02 15:43:29 +00:00
* The guard() function in m2d/event-dispatcher.js no longer takes (any, any), but a string and a function.

* m2d/send-event.js no longer complains that res.body has some missing fields. It would appear as though those missing fields weren't revelant to the fromWeb() function (into which res.body is passed), given that this code worked before and still contunes to work, so I just @ts-ignore'd res.body

This commit's developer's off-topic personal comment, related to this commit: This has nothing to do with improving thread UX, even tho this is what I was supposed to work on. However, in my attempts to discover in what file should I start, I stumbled upon those errors from m2d/send-event.js, so I fixed them. And after establishing that m2d/event-dispatcher.js is the file that I'm looking for, I also noticed that guard()'s @parm definitions could be improved, so I did that. Now - back to thread work...
* To better reflect reality ("m.in_reply_to" will not always be present - it's not (always?) found on "rel_type":"m.replace" relation-events)

* To support "rel_type":"m.replace" relation-events (added "m.replace" option to existing key "rel_type" and a new "is_falling_back" key)

AFFECTED TYPES: M_Room_Message, M_Room_Message_File, M_Room_Message_Encrypted_File

BREAKS: Nothing, as .d.ts files don't affect buisness logic. In terms of lint errors: Marking "m.in_reply_to" as optional is indeed technically a "breaking change" (TypeScript may complain about „is probably undefined” in places where it didn't before), but from early "testing" (ie. looking at VSCode's errors tab), it doesn't seem like anything broke, as no file that imports any of those 3 types (Or their Outer_ counterparts) has „lit up” with errors (unless I missed something). There was one type error found in m2d/converters/event-to-message.js, at line 1009, but that seemed unrelated to types.d.ts - nevertheless, that error was also corrected in this commit, by adding proper type annotations somewhere else in the affected file.
/This is a squash-commit - the following is a rough summary of all sub-commits, written in style of commit messages (not necessarily those commits themselves), ie. short and in present tense./

* Document design choice to not bridge Discord threads as Matrix threads [by directly quoting Cadence]
* Alter thread-to-announcement, so that it replies in-thread [with this, Matrix users will get a list of almost all (exl. those that don't branch from anything) open threads on a given channel, whereas before it wasn't possible. Also features slight alterations to the text]
* Notify the user, whenever an in-thread message on Matrix is sent, that this isn't how they're supposed to do threads on OOYE
* Detect /thread being ran as a reply or in-thread to branch the thread from the relevant message
* Handle various /thread errors [notably being ran without args (infer the title if ran in the context above, simply show help if not)]
*  Whenever possible, direct the user to an already-existing thread-room [if /thread was ran as a reply or in-thread, or as part of the notification mentioned in point 3 (feat. a new utility method)]

AUXILIARY TYPE CHANGES (not always relevant to UX-improvement-related changes):
* Fix „boolean” being referred to as „bool” in types.d.ts
* Rename execute(event) in matrix-command-handler is now parseAndExecute(event) [and it is no longer of type CommandExecute, but has its own custom definition, because a) it has a different return now (returns what command was ran (needed for point 3 in section above) instead of always undefined and b) other params from CommandExecute (like ctx or words) weren't being used - quite the contrary, their values were only being created at that stage (as part of command parsing - hence the rename, too), so telling that they're values you pass into execute() was at least somewhat confusing]
* Further narrow-down the type of guard() in m2d event-dispatcher

TEST CHANGES:
* Create 7 new tests, all pass
* Update 4 existing threads, all pass
* Pass all other relevant tests [and almost all other tests, too - there are some issues with event2message for stickers, but given the fact that this commit does not touch the stickers subsystem in any way at all,  it does not seem like they are any way related to my changes and they must've been failing before]
* Do extensive manual testing and debugging
Co-authored-by: Guzio <guzekk@protonmail.com>
Co-committed-by: Guzio <guzekk@protonmail.com>
First-time contributor

@Guzio you can add the WIP: prefix to the PR which auto-flags it as work in progress :)

@Guzio you can add the `WIP: ` prefix to the PR which auto-flags it as work in progress :)
Guzio changed title from feat.: Thread improvements, round 1 to WIP: feat.: Thread improvements, round 1 2026-03-03 00:12:55 +00:00
Author
First-time contributor

@beanie wrote in #74 (comment):

@Guzio you can add the WIP: prefix to the PR which auto-flags it as work in progress :)

I know - GitDab told me. I just decided it wouldn't be necessary because „surely, I'll finish that message in a few minutes; noone will ever even notice that this was WIP for a short while”.

...Cue-in me returning from uni, taking multiple-hour-long nap, and also taking much longer then anticipated to write the message. 😅

Marked this as WIP for now, will hopefully un-mark it over the course of the next 15 minutes.

@beanie wrote in https://gitdab.com/cadence/out-of-your-element/pulls/74#issuecomment-9082: > @Guzio you can add the `WIP: ` prefix to the PR which auto-flags it as work in progress :) I know - GitDab told me. I just decided it wouldn't be necessary because „surely, I'll finish that message in a few minutes; noone will ever even notice that this was WIP for a short while”. ...Cue-in me returning from uni, taking multiple-hour-long nap, and also taking much longer then anticipated to write the message. 😅 Marked this as WIP for now, will *hopefully* un-mark it over the course of the next 15 minutes.
Guzio changed title from WIP: feat.: Thread improvements, round 1 to feature: Thread improvements, round 1 2026-03-03 12:12:11 +00:00
Author
First-time contributor

over the course of the next 15 minutes

took a tiny-bit longer than anticipated lol

But I'm done here, yaaayyy! Now awaiting feedback...

> over the course of the next 15 minutes took a tiny-bit longer than anticipated lol But I'm done here, yaaayyy! Now awaiting feedback...
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u mergable-fr-fr:Guzio-mergable-fr-fr
git checkout Guzio-mergable-fr-fr

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git checkout main
git merge --no-ff Guzio-mergable-fr-fr
git checkout Guzio-mergable-fr-fr
git rebase main
git checkout main
git merge --ff-only Guzio-mergable-fr-fr
git checkout Guzio-mergable-fr-fr
git rebase main
git checkout main
git merge --no-ff Guzio-mergable-fr-fr
git checkout main
git merge --squash Guzio-mergable-fr-fr
git checkout main
git merge --ff-only Guzio-mergable-fr-fr
git checkout main
git merge Guzio-mergable-fr-fr
git push origin main
Sign in to join this conversation.
No description provided.