Clicking one used to swallow the link: the chat opened here and the
stream went nowhere, so somebody clicking through to watch ended up with
half of what they asked for. It does both now - the stream in the
browser, where video is watched, and the chat here, which is what this
client is for.
With no Kick account the stream still opens and the toast says only the
chat is missing, rather than reporting a failure for something that
half-worked.
And a Kick channel's own row has "Open in browser", next to the way to
open its chat in a window - a channel is a stream as well as a chat, and
the stream is the part moho does not show.
The toolbar had a magnifying-glass button doing what the context menu's
Profile entry does, so a message carried two ways to ask the same
question - and the toolbar is the row of things you do *to* a message:
react, reply, more. Who somebody is is not one of those.
Profile stays where it belongs, at the top of both context menus - the
message's and the member list's.
Fifteen mentions of `chatd`, a daemon that has been called nobilis since
it moved to its own repository; a config directory two names out of date;
and no mention of Kick at all, which is a protocol this client speaks.
While correcting those, the parts that had quietly drifted: IRC does more
than SASL PLAIN now, Sneedchat's rooms are read from the site rather than
edited in Settings, Matrix has threads and receipts and a directory
search, and the build is `npm run daemon` rather than a bare cargo
invocation. The wire section says requests are answered concurrently, and
points at methods.rs for the full method list rather than listing ninety
of them somewhere they can go stale again.
Two things it now admits that it did not: Matrix verification is between
your own sessions only, and there is no cross-signing.
The mute in a conversation's menu was a preference belonging to this
window. On Matrix the account itself can hold the answer - it is a push
rule, which is what Element writes and what a phone reads - so muting a
room here now quietens it there too. Every other service keeps the local
behaviour, since none of them has anywhere to put it.
Typing "@" opens a list of who and what can be tagged here: the people in
the conversation, the roles this service lets anybody ping, and its own
whole-room words - "@everyone" and "@here" on Discord, "@room" on Matrix,
"@everyone" on Sneedchat, nothing on IRC, which has no such idea. Arrows
move, Enter or Tab takes, Escape dismisses, and the mouse does the same
without stealing the caret.
Matching is fuzzy rather than by prefix. The names here are long and full
of things nobody types in order - "***GAYMER WORD USER***" - and a prefix
match makes somebody type the punctuation. A prefix still wins, then a
word start, then a run of adjacent letters, then anything in order.
What gets inserted is what the service wants: "@name" everywhere except
IRC, where a name at the start of a line becomes "name: " and the "@"
would be a character that means nothing to anybody reading it.
The composer's hooks were the first thing to go wrong here - two useMemos
landed below an early return, which React reports as error #310 and which
takes the whole window with it. Every hook is above it now.
Issue #12: the networks now carry their own marks, taken from each
network's own site and bundled rather than fetched - an IRC network is
not a CDN, a rail of tiles should not be a rail of requests, and half of
these sites are plain HTTP. Libera, OFTC, Rizon, QuakeNet, Undernet,
hackint, DALnet and CoreIRC have one; EFnet, Snoonet and Abjects publish
none and keep their initials, which is a better answer than a
wrong-looking copy of somebody's artwork. The marks belong to the
networks that own them and identify those networks here and nothing else.
CoreIRC and Abjects are in the list now - the two this client's own users
are on. A network being small is exactly why nobody else's client offers
it ready-made, and typing "irc.coreirc.net" correctly is the step the
list exists to remove.
The form itself: the network is picked from those marks rather than from
a dropdown of names, and the server and port fields appear only for
"Other" - a chosen network has its address already, and a field showing
it invites editing something that is right. Sign-in is asked here now,
as SASL or NickServ, because a registered nick that never identifies is
a nick lost to the ghost of the last session, and that happens on the
first connection rather than after anybody visits a settings pane.
Autojoin left the form entirely. Which channels an account rejoins is a
property of the channels, so it is set on one: right-click a channel,
"Join automatically". It was a comma-separated field typed once and never
seen again.
And Sneedchat's rooms moved out of the account pane into the join panel,
now that the list comes from the site. That page is for joining things; a
room being ticked is exactly that. The account pane is for what an
account is.
Above Mention in the member list and above Reply on a message. It is what
a right click on a person is usually for, and everything under it acts on
them - which is a thing to decide after seeing who they are rather than
before.
The WHOIS card became a profile: right-click somebody in the member list,
right-click their message, or use the button in the hover toolbar, and
the service is asked who they are. Which service decides what comes back -
IRC has an idle time and no account age, Discord has the day the account
was made and the day they joined this server, Matrix has a power level
and a last-seen - so the card draws the facts it was given rather than a
form with holes in it.
The answer arrives as an event because it is a request over a network,
and the card opens on what is already known so a menu item never appears
to do nothing.
Tab completes a name from the roster, cycles on repeated presses, and
follows a name at the start of a line with ": ". There was no completion
at all, on a protocol where people are called `[Fish]tank_`.
And IRC's own formatting codes are rendered rather than shown as control
characters: bold, italic, underline, reverse, and both the numbered and
hex colour forms, in mIRC's own palette. A setting strips them instead,
which is the switch HexChat has for the same reason. The sanitiser learnt
four more style properties to allow this, each against its own pattern -
a style attribute on somebody else's message is a large surface.
The room list was six entries in this client's source, so #lolcows - a
room the site has had all along - could not be joined without editing and
rebuilding. The daemon reads the real list off the chat page now, and the
account's settings ask for it when they are opened, filling in when the
answer arrives a few seconds later.
The built-in list is the fallback rather than the source. The site is
behind a proof-of-work gate over Tor and the read can fail; rooms
somebody can still tick beat an empty list, and when it does fail the
pane says so rather than quietly showing a short list as if it were
complete. #lolcows is in that fallback now too.
Joining is two round trips before the daemon can name a room - the join
itself, then the room's state - and until they finished the click
produced nothing anywhere on screen. The room now appears immediately,
named by whatever was in hand: the directory's name, or the address that
was typed. Measured at 150ms from the click, against roughly half a
second before.
The same optimistic echo an outgoing message gets, and for the same
reason: the wait is real, and it is the silence that makes it feel
broken. The stand-in is replaced by the real buffer when it arrives,
matched on the room id rather than on the buffer id - which is derived
from a name nothing knew when the join was asked for.
It goes on failure, so a refused join does not leave a row claiming a
room is still on its way, and it gives up after thirty seconds for a
server that accepts a join and then says nothing. Until the real room
arrives the row is there to be seen rather than opened: there is nothing
behind it yet.
The dialog said "Searching 3 homeservers", which named none of them and
sat on a line with a text field that capped it at about four before the
rest were pushed out of sight. The servers now have a row of their own,
each named, with the number of rooms it contributed - or why it gave
none: refused, rate limited, timed out, no directory.
Each one is a switch. Searching everywhere and searching two particular
servers are both things somebody wants, and the difference is the whole
reason the list is merged in the first place. Switching one off filters
what is already in hand rather than searching again: the rooms are here,
and a round trip to hide rows somebody is looking at would make an
instant thing slow. They default to on, so a server that turns up later
is included without having to be found and enabled.
Scrolling to the end loads the next page. A button there was a second
decision about the thing somebody is already doing, which is looking
further down the list.
Appended pages are deduplicated by room id. The daemon can only
deduplicate within one answer - it does not know what this window is
already showing - so a page from one server could repeat rooms another
server had listed earlier.
A room that has been joined but not yet heard from now draws a turning
ring beside its name, and opening it says "Synchronising with the
server…" rather than "No messages here yet" - which was a claim about
what the room contains that nothing had established.
A ring rather than a bar because nothing here knows a proportion: the
server decides how long it takes. It takes its colour from whatever it
sits beside, so it is dim in a buffer row and reads as text in the
notice. Under prefers-reduced-motion it pulses in place instead, which
still says "working" with nothing travelling across the screen.
Avatar drew whatever URL it was given and had no answer for one that
failed, so a picture that could not be fetched became a broken-image
glyph. Now it falls back to the same coloured initial it already shows
for somebody with no picture at all.
Not hypothetical: a room directory lists icons hosted on servers that may
refuse to hand them over, and a column of broken images is worse than a
column of letters. It guards every avatar in the client, though - a
member whose picture has been deleted, a cached file that has been
cleaned up underneath us.
The directory search was two fields in the join page, which made finding
a room a thing you did inside a form. It is a dialog now: it opens over
the window, takes the focus, searches as you type once typing stops, and
closes when you arrive somewhere. Element's spotlight is the same shape
and for the same reason.
One list rather than a section per homeserver. The honest structure would
be per server, since that is what a directory is - but the whole point is
finding a room whose server you do not know, and a per-server view asks
you to know it first. Where a room lives is printed beside it instead,
along with its member count and topic.
There is still a field for adding a homeserver by hand, because knowing
the server is the one case the merged search cannot improve on. A server
that does not answer is named in the line above the results.
A search in flight is tagged, so a slow server answering a query somebody
has already typed past cannot replace what they are looking at now.
A thread gets a side panel, not a buffer. It is not a place you go - it
is an aside inside a conversation you are already reading, and the buffer
list would fill with fragments that appear and go quiet. Element makes
the same call, and somebody moving between the two should not have to
learn two shapes. It stands in the member list's column and closes it
while open: two columns beside a conversation leaves the conversation too
narrow to read.
Two ways in. A message that started a thread carries the count, drawn
from what is loaded - which undercounts a thread older than the window,
and says "at least this many" rather than "none". A message that is in
one says so where the reply preview would be, and opens it.
The panel has its own box. Typing there answers the thread while the
room's composer still answers the room, which is the whole reason to open
one.
And the room directory: a search field with an optional homeserver
beside it, because a directory is per server and the ordinary way to find
a community is to have been told which server it is on. Explicit search
rather than as-you-type - each keystroke would be a request to somebody
else's server, and directories are rate-limited.
Faces in the right gutter against the last message each person reached -
which is the question people actually ask of receipts, not "did they see
this one" but "where has everybody got to". Capped at three with a count
after it, and the names in the tooltip: a room of thirty would otherwise
draw thirty avatars against one line.
Two settings, because they are two choices. "Let others see what I have
read" turns off broadcasting - which asks the daemon for a private
receipt, not for silence, so your own phone still agrees about what is
unread. "Show who has read each message" hides the gutter without
changing what anyone else is told.
The send preference is read at the moment of acknowledging rather than
mirrored into state: main deliberately does not notify a window of its
own preference change, so a mirror would be stale in exactly the window
somebody just used the toggle in.
One form for a room or a space, since a space is a room with a different
creation type and nothing else - two forms would invent a distinction the
protocol has not got.
Encryption defaults to off and says why: it can be turned on later and
never off, so the reversible choice is the honest default. It is disabled
for a public room, where it would be a promise the room cannot keep.
The new room is not selected on creation. It arrives through the next sync
with whatever name and kind the server settled on, and jumping at a buffer
that does not exist yet would land on nothing.
Matrix ranks are a number, so the member menu offers the three the
protocol's own defaults are built around - 0 everybody, 50 what every
other client calls moderator, 100 the room's owner - rather than a
free-text box for a value nobody has a feel for.
Co-Authored-By: Claude Opus 5 <[email protected]>
A field rather than a picker, because a Matrix address names a person
globally - there is no roster to choose from and nobody to look up first.
Offered on every Matrix room rather than gated on permission: the server
decides and says so plainly, and guessing here would only hide the action
from somebody who could have used it.
And the sessions panel now says what verifying actually does. moho does no
cross-signing, so a verification is recorded between these two sessions
and never published - Element goes on showing the device as unverified.
Somebody not told that would reasonably conclude one of the two clients is
broken, which is a worse outcome than the missing feature.
Co-Authored-By: Claude Opus 5 <[email protected]>
Both ways this goes wrong look like a wrong password, so the form says
which one it is before somebody hits it: the domain from your Matrix
address is usually right because the daemon asks the server where its
client API lives, and :8448 never is because that is the port servers use
to talk to each other.
Co-Authored-By: Claude Opus 5 <[email protected]>
Closes the client half of #83.
Six toggles for the things nobody said - redemptions, subscriptions,
raids, polls, going live, moderation - each separable, because on a busy
channel they can outnumber the conversation and they are not all wanted at
once. Same reasoning as IRC's join and part filters, and now possible
because the daemon says which kind each line is.
A kick.com link in a message opens the channel here rather than the
browser. Only the bare `kick.com/<handle>` form: a link to a clip, a video
or a category is a page moho cannot show, and opening the channel instead
would quietly take somebody somewhere they did not ask to go.
The scheme registration list is deliberately left alone. A kick.com link
is an ordinary web address, and registering for https would be
volunteering to be the system browser.
Co-Authored-By: Claude Opus 5 <[email protected]>
The menu asks Kick what it may do the way it asks IRC: from what is
already on screen. A moderator's own row says they are one, so that is the
test - which means a moderator who has not spoken yet is offered nothing,
and that is the honest reading rather than offering actions that come back
refused.
Actions are named for what they do on the service. "Kick" is a timeout on
Kick, because there is nobody to remove from a livestream chat.
The mode chip now passes words through instead of reading them a letter at
a time - "followers only" is not f, o, l, l, o, w.
Co-Authored-By: Claude Opus 5 <[email protected]>
IRC's rank is one character and sits in a fixed eight-pixel slot before
the name. Kick's is a word - "moderator", "subscriber" - which overflowed
that slot and painted straight over the nick beside it, leaving a member
list where every row was two strings on top of each other.
A word is drawn as a badge after the name instead, with the same
abbreviations the message byline uses, so the same person reads the same
way in both places. It is pushed right and never shrunk: a long name
ellipsises rather than the badge being squeezed away, since the badge is
the shorter and the more scannable of the two.
Co-Authored-By: Claude Opus 5 <[email protected]>
They were siblings of the name, and the name is a block in the comfy and
bubble layouts - so the badges dropped to the next line and sat in with the
message text, reading as part of what was said.
Name and badges are one element now, so a layout that gives the name its
own line takes the badges with it. The compact log keeps the byline inline,
where the name has always run into the text.
The member panel is also named for what it actually is on Kick: there is no
viewer list to ask for, so it lists who has spoken recently and says so.
Kick's badges are words rather than sigils, mapped onto the same ranks the
list already groups by - one grouping for every protocol rather than each
growing its own.
Co-Authored-By: Claude Opus 5 <[email protected]>
Kick gives everybody a colour and it is half of how a busy chat is read.
It overrides the colour derived from the nick rather than replacing that
mechanism, since IRC has no such thing and still needs it.
Badges sit beside the name, abbreviated: the full word is the tooltip,
because a row of them spelled out would be wider than the message above.
A badge with no short form keeps its own first letters rather than being
dropped - an unfamiliar badge still says the person has one.
Only broadcaster and moderator get colour. Colouring every badge would
make a name carrying four of them the loudest thing on screen, which is
the opposite of what a badge is for.
Co-Authored-By: Claude Opus 5 <[email protected]>
Closes#66. Kick, ban and mute routed unconditionally to the Matrix RPCs,
so on IRC the menu simply did not appear - and the commands that do exist
were reachable only by typing them into the composer, which is the thing
this menu exists to save.
Matrix answers "may I" from the server, where its power levels live. IRC's
answer was already on screen: the member list carries every nick's prefix,
including the local user's, so what we may do is whatever our own prefix
says. Both are advisory - the server re-checks and refuses if we were
wrong, exactly as it would any other client.
Generous at the halfop boundary on purpose. Networks disagree about what a
halfop may do, and offering an action the server then refuses is a better
failure than hiding one it would have allowed.
Giving and taking rank are separate entries rather than one toggle, chosen
by the member's own prefix - a menu that guessed wrong would take op from
somebody by accident. "Mute" resolves to devoice on IRC, which is what
muting means in a moderated channel; there is no separate mute to give.
Co-Authored-By: Claude Opus 5 <[email protected]>
The mode letters sit beside the channel's name, titled with what they
mean - the letters are only obvious to somebody who already knew, and the
ones worth explaining are the handful that decide whether you can speak.
An unknown letter is shown as itself rather than guessed at.
The channel browser is a button rather than something that happens on
opening the page: a LIST on a real network answers with tens of thousands
of channels and some servers throttle or refuse it outright. Filtering is
local, because the daemon holds the whole answer and typing narrows it
instantly, where a server-side filter would mean a fresh LIST per
keystroke against something that just rate-limited you. Two hundred rows
drawn at most, since a filter matching nothing in particular still matches
forty thousand.
The server buffer it sends the command through is found rather than
assembled from the account id: the daemon names that buffer after the host
it actually connected to, which is not always the host that was typed.
Co-Authored-By: Claude Opus 5 <[email protected]>
A mechanism picker in the SASL block, defaulted rather than chosen: the
default is right for nearly everyone, and each alternative needs something
the network has to support first.
The certificate field appears only for EXTERNAL, since every other
mechanism ignores it and a path box beside PLAIN reads as something that
does nothing. The path is never reported back - a frontend has no use for
it, and it names a file on whichever machine the daemon runs on rather
than this one - so the field shows only whether one is set.
SCRAM says plainly that almost no IRC network offers it. Better than
letting somebody pick it, watch the connection fail, and conclude the
client is broken.
Co-Authored-By: Claude Opus 5 <[email protected]>
The whisper goes out with the buffer it was sent from, so it lands in that
conversation rather than in every room the account is in.
Co-Authored-By: Claude Opus 5 <[email protected]>
Sending one used to go and find the Whispers buffer and open it, which
took you out of the room you were talking in every time you answered.
There is no such conversation on Sneedchat anyway - the whisper now
appears in the room, marked as one, which is where the site puts it and
where the person who sent it is still looking.
Purple, and its own tint rather than the mention blue. Every inbound
whisper is highlighted, so without this the two said the same thing in the
same colour and a private message was indistinguishable from the room
saying your name. Styled on the kind rather than the highlight, so a
whisper written into a second room - deliberately not highlighted, to keep
one whisper to one alert - still reads as one.
The tag said "whisper to {from}", which on our own whisper is us. Who it
went to is in the line itself now, written the way the site writes it.
Co-Authored-By: Claude Opus 5 <[email protected]>
The field floated at a fixed offset from the bottom of the list, so
renaming a heading at the top meant crossing the window to type into it -
around eight hundred pixels from the heading you had just right-clicked.
It now takes the heading's place, in the flow. Renaming happens where the
cursor already is, and the field cannot be mispositioned because it is not
positioned at all.
A new heading is made at the top of the list rather than appended, since
the command is given from the group's title directly above - a heading
that appears twenty rows below where it was asked for is one somebody has
to go and find. It is named in the saved order too, or the ordering rule
would sort a heading nobody has placed to the bottom, which is exactly
where this one must not go. Drag it anywhere afterwards.
The name opens selected, because it is either the old one being replaced
or a placeholder nobody meant to keep; the first keystroke should replace
it. Clicking away keeps what was typed, since a field sitting in the list
reads as part of it - Escape is how you call it off.
Co-Authored-By: Claude Opus 5 <[email protected]>
It is positioned absolutely with a small inset on each side, which is
right - but nothing above it was a containing block, so it resolved
against the viewport. "Left and right, inset a little" then meant the
width of the whole window, and renaming a heading put a text field across
the message list and the member list.
The list is now that containing block, which is what the box was always
being positioned against in intent. Nothing else inside it is positioned
absolutely, and the context menus are fixed, so nothing else moves. The
vertical offset is unchanged because the list already ends where the
window does.
A max-width on the input as well, so this cannot spill again if something
above it stops being a containing block: the sidebar is as wide as this
should ever be, whatever it happens to be measured against.
Measured after: 204px wide inside a 220px sidebar, rather than 1894.
Co-Authored-By: Claude Opus 5 <[email protected]>
"File under X" was an entry per heading, and that list had no ceiling. On
a server with twenty categories the menu was taller than the screen, which
made every other entry on it unreachable - so the feature that grew was
also the one that broke everything beside it.
Dragging a channel onto a heading says the same thing in the place where
the answer already is, and costs nothing as the headings multiply.
Only a heading somebody made will take one. A service's heading is that
server's own grouping, and moving a channel into a Discord category is a
change to the server rather than to this list - so those decline the drop
rather than accepting and ignoring it, which is what makes the cursor say
no while the drag is still in the air. Rows are only draggable at all
where there is a heading of your own to drop them on.
"Remove from category" stays, and is now shown only when the channel is
under one. It is the single bounded entry the growing list never was, and
taking a channel back out has no gesture of its own: the channels under no
heading are drawn without a heading to drop onto.
Co-Authored-By: Claude Opus 5 <[email protected]>
The same gesture the server rail already had, on the headings in the
channel list, saved per rail entry.
Every heading moves, the service's included. A Discord category's position
is that server's opinion about its own categories, which is a good default
and no reason to be stuck with it in your own sidebar - so custom headings
and the service's are dragged the same way, which also means the rule that
custom ones always sit above the service's is now a starting point rather
than a fact.
The unheaded section is exempt: it has no heading to take hold of, and
"the ones under nothing" only means anything at the top.
Insertion is the rail's rule, so dropping onto the last heading means "put
it last" - the gesture people try first, and the one the rail needed a
separate drop zone to answer because a merge target sat in the way. There
is no merging here, so the plain rule is enough.
The saved order is a list of keys and is deliberately allowed to be
partial: a heading nobody has placed - a Discord category added since, or
one just created - sorts after the ones they have, rather than somewhere
arbitrary among them. A key naming a heading that is gone is skipped.
Co-Authored-By: Claude Opus 5 <[email protected]>
In the Kick account's own card, because the trade behind it is worth
stating where it is felt: the follows are read once, when the account is
first connected, since a list re-read on every connect would put back
every channel you had closed. The cost is that anyone followed later never
turns up on its own, so this is the way to ask - and it is you asking.
The blurb says it will also reopen a followed channel you closed earlier,
because it will. An earlier draft claimed the opposite, which was a
comfortable thing to write and not true: only the automatic sync must
never resurrect a closed channel, since nobody asked it to run.
Names the channels rather than counting them where there are few enough to
read. "3 added" sends somebody looking for which three.
Co-Authored-By: Claude Opus 5 <[email protected]>
The hover menu's Reply only ever names what is being answered; what that
becomes is the daemon's business, and Kick now has a native reply rather
than the "@name," mention Sneedchat needs. The comment said otherwise and
listed the protocols, so it was already the kind of comment that goes
stale by being right at the time.
Co-Authored-By: Claude Opus 5 <[email protected]>
Joining is the "+" and a streamer's handle, in whatever form it was
pasted - a kick.com link, an @handle, or the bare name all name the same
person, and the daemon takes the handle out of it.
Emotes render with no lookup: the id is in the `[emote:id:name]` token and
the picture is public, so every emote in a channel draws for every reader
including the subscriber-only ones. That is the point - gating what
somebody can see on what they have paid for would leave a newcomer reading
a chat that is half raw tokens. The tier gates sending, so the picker
shows locked emotes greyed rather than hiding them: knowing what
subscribing would buy is most of the reason the tier exists.
The picker learned two things it did not know. Sets, so a channel's own
emotes sit above Kick's global ones under their own heading rather than
being mixed in nameless; and that the text an emoji stands for is the
service's, not one format - Kick writes the id first so a client that
cannot draw the picture still shows something readable.
Redemptions, subscriptions, gifted subs and raids get a plate rather than
the italic grey a join gets. In a chat moving fast enough that these are
what people are watching for, an event styled like system noise is an
event nobody sees; the plate survives being scrolled past because the eye
catches the shape before it reads the words. Deliberately not the mention
highlight, which means "this is about you" and would be a lie here.
Signing in is Kick's own page in a browser window, one more entry in the
login table beside Discord's. Adding an account offers watching first,
because that ordering is the honest one: asking for a password before
showing anything would be asking for one that buys nothing they came for.
Co-Authored-By: Claude Opus 5 <[email protected]>
Several channels can be watched at once now: pop one out from the header,
beside the member-list toggle, or from its row in the list.
Deliberately not a second chat view. The log, the composer, the member
list and dropping a file on it all read whichever buffer the store has
open, so a popout is that same store with the open buffer nailed down -
which is the only new idea here. What is left out follows from it: no
rail and no settings, because a window that can navigate anywhere is
another main window; and the prompts that float over everything stay in
the main window, since the same question asked in four places is four
chances to answer it differently.
Being watched in its own window counts as being read - no badge, no
desktop alert - because that is what popping it out was for. Minimising
one takes it back, which is why every show and hide is answered.
Two things had to become per-window that were not. The title bar's own
buttons acted on the main window whoever pressed them, so a popout's
minimise minimised the window behind it; and settings are cached per
window, so muting a conversation or switching to compact would have
applied to whichever window happened to be asked.
And windows are now revealed by whichever of "painted" and "loaded"
arrives first. ready-to-show is the first frame the compositor accepts,
and a GPU process that restarts underneath it leaves that frame - and the
event - never arriving: the window exists, holds its buffer and answers
IPC, and is simply invisible. That is why the button appeared to need two
clicks, the second finding a window already open and raising it. It also
explains moho occasionally starting to nothing at all.
Co-Authored-By: Claude Opus 5 <[email protected]>
Releasing the pin was a matter of distance, and taking it back still was.
That is the same mistake twice, and the second one is worse: while the pin
is off it is holding the place in history somebody went looking for.
Scrolling up now releases immediately rather than after 250px. A channel
busy enough to keep firing the pin's own scroll-to-bottom would undo an
upward scroll before it ever reached the threshold, so the distance was
not a threshold at all - it was a fight the reader could not win.
Taking it back needs a scroll made downwards that lands near the bottom.
Proximity alone is not consent to be dragged there: a busy buffer drops
rows off its top as it trims, which carries a stationary reader closer to
the bottom on its own.
Which leaves telling a reader's scroll from the page moving under one, and
height is the tell - losing content clamps the position down by what was
lost, which looks exactly like scrolling up and is not.
Co-Authored-By: Claude Opus 5 <[email protected]>
npm run deploy stopped building the daemon, with "rustup could not
choose a version of cargo to run" - which reads like rust is missing and
is not.
rustup installs a shim, and the shim finds the real toolchain through
CARGO_HOME and RUSTUP_HOME. On the XDG layout those point under
~/.local/share rather than ~/.cargo, so they are named by environment
variables a login shell sets and an npm script never sees. The shim then
has nowhere to look.
The distribution's own /usr/bin/cargo does run, which makes it look like
the answer, and for a native build it is. It is also the wrong one: it
carries no cross-compilation targets, so a Windows build through it
fails with "can't find crate for `core`" - which in turn reads like a
broken checkout. Each wrong answer points at the other, which is why
this has been worked around by hand all week instead of fixed.
So the XDG locations are filled in when nothing else has, rustup's shim
is preferred because it is the toolchain carrying the targets, and every
candidate is asked `--version` rather than assumed - that question being
the only thing that tells a working toolchain from a shim that cannot
find one. The Linux build goes through the same script as the Windows
one now, since it had the same problem and no reason to solve it
differently.
Both were verified through npm rather than from a shell that already had
the right environment, which is the only way this bug is visible at all.
Co-Authored-By: Claude Opus 5 <[email protected]>
Three things about moving around the window, all of them the same shape:
something that reads a position without asking what moved it.
The log stopped following a busy channel. Whether to stay pinned was
decided by how far the bottom was, and the bottom gets further away for
two different reasons - reading upwards, and a message arriving. Those
are indistinguishable in that one number: a message taller than the
threshold moves the bottom exactly as far as scrolling up by its height
would. In a channel busy enough to grow by that much between frames the
pin came off, the log stopped following, and Jump to present appeared
offering to undo something the reader never did. It now asks whether the
position moved up, which is what reading away actually is, and the rule
is a named function with the failing case written down as a test rather
than three lines inside a callback.
The browser's own scroll anchoring is turned off for that list. It
adjusts the position when content above the view changes size, which is
right for a document and wrong here - two things moving one position
produces a scroll nobody asked for, which the above would then have to
interpret.
Folders could be dropped into but never picked up: the tile had every
drag handler except the one that starts a drag. A folder has no place of
its own in the saved order, which holds servers and draws a folder
wherever the first of its members falls, so moving one moves its members
as a block - which is also what stops it being torn in half on the way.
Dropping a folder onto another folder places it rather than filing it,
since folders do not nest and filing one inside another would lose it.
And the end of the column was unreachable. The only way to get there was
the bottom third of the last tile with the merge zone directly above it,
so aiming for the end and making a folder instead was easier than
getting it right. The empty space below the column now means "put it
last" - and says so while something is held over it, since empty space
that quietly does something is empty space nobody uses.
Co-Authored-By: Claude Opus 5 <[email protected]>
Points at nobilis 7d67fad. The inbox now fills from what was actually
said rather than only from what arrived while this window was open, and
the downloads list keeps its history instead of emptying itself every
time the daemon stops.
Co-Authored-By: Claude Opus 5 <[email protected]>
Send a file sits beside Whisper in the two places you already are when
you decide to: a name in the member list, and a message somebody wrote -
which is usually where the thought comes from. The file is chosen in the
system's own picker, so it looks and behaves like every other open
dialog on that machine, and picking one is the consent to send it.
Both panels now speak about sending as well as receiving. An offer we
made is not a question for us to answer, so it shows as a transfer
waiting on them rather than as a prompt with buttons that would do
nothing, and its bar stays hidden until something is actually moving -
at zero it only looks like a stall. Cancelling it withdraws the offer
and says so.
There is a setting for the address to give out, which matters behind a
router: the machine only knows its own side of one, and the address
worth publishing is the one the router answers on.
Points at nobilis 10d75ac.
Co-Authored-By: Claude Opus 5 <[email protected]>
pack:win bundled nobilis/target/release/nobilis.exe, and nothing on a
Linux machine ever writes there - a cross-build goes to
target/x86_64-pc-windows-gnu/release/. So the packager took whichever
.exe happened to be at the first path, which on a machine that has been
cross-building for days was three days old. Building a Windows package
today would have shipped a daemon from before XDCC, whispers and
deeplinks existed, and it would only have shown up as "unknown method"
on somebody else's computer.
npm run daemon built the Linux binary regardless of the target being
packaged, so pack:win never refreshed the exe it was about to ship.
There is a daemon:win now that does, and pack:win runs it.
Its own script rather than a line in package.json because two things
have to be right. rustup's toolchain has to be on PATH, not merely named
- cargo runs whichever rustc it finds, and pointing at rustup's cargo
while a distribution's rustc is first still fails with "can't find crate
for `core`", which reads like a broken checkout. And the copy is
conditional on the executable actually existing, since a build that
fails after having produced one once leaves the old one in place, which
is precisely how the stale copy got there.
Caught by the guard on its first run: the cross-build failed and the
Aug 28 file was left untouched rather than copied forward.
Co-Authored-By: Claude Opus 5 <[email protected]>
Points at nobilis d7370ed: a replayed message is no longer announced a
second time, so a mention caught in the history Sneedchat resends after
an outage notifies once rather than on every reconnect.
Co-Authored-By: Claude Opus 5 <[email protected]>