The release workflow and the PKGBUILD already did; the two build scripts
behind npm run daemon, pack and deploy did not, so a Cargo.toml that
disagreed with Cargo.lock was quietly re-resolved and the lockfile
rewritten instead of the build failing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vitest, with 70 tests across the pure lib modules - formatting (IRC codes,
BBCode, spoilers, emoji and emotes, media and YouTube detection), media URL
resolution including the file-version fragment, the rail and its folders,
mentions, search filters, nick completion, Kick watching, emote and
interruption state - and the media scheme's boundary in the main process,
with Electron stubbed.
scripts/ui-check.mjs runs the built app against a scratch home and a fake
IRC server, opens the emoji picker, the channel, message and member menus
(right-clicked in the window's far corner) and the mentions inbox at two
window sizes, and fails if any of them leaves the window at 80 ms, 400 ms,
1.5 s or 3.5 s. Checked that it fails: with the context menu's clamping
removed it reports every corner-opened menu outside the window.
npm test and the layout check (under xvfb) run in the Linux release job.
#212
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The missing half of #189. A call placed as a video call already opened a
camera; a call placed as a voice call could never grow one, and the
button was hidden unless the call was on a media server - so on the two
shapes of Matrix call that do not use one, there was no camera at all.
Added and removed rather than enabled and disabled. A disabled track
keeps sending - a black rectangle at whatever frame rate the encoder
feels like - so the far end sees a dark square where a face was and
cannot tell that from a camera that has frozen. Removing the track ends
it properly, and every other client then draws the person's name. The
loopback harness below measures exactly this difference: with the old
enabled=false the far end still counts one live video track after the
camera is switched off, and with the track removed it counts none.
One gesture for all three shapes of call - a media server, a mesh
between several people, a single connection between two - because the
button is one button and the person pressing it is asking one thing.
Which shape this is belongs to the call, not to the control. On a mesh
the camera is still opened once and lent to every leg, since five
connections asking the machine for the same camera get one capture and
four refusals.
Renegotiations are now serialised. Turning a camera on and sharing a
screen a second later are two changes to one connection, and starting
the second offer while the first is unanswered leaves a peer connection
neither end can recover from. A remote track that is taken away is
dropped from the stream it arrived on, so a tile stops drawing the last
frame it received - Chromium reports that as a mute before it reports it
as an end, and on a removed track the mute is often all there is.
cameraOn is asked of the call rather than assumed, in both branches:
that block runs on every phase change, and a fixed answer put the button
back to "off" the moment a call connected.
The call UI is unchanged in shape and deliberately not Matrix-shaped -
Discord's camera (#180) and screen share (#14) get the same row of
buttons when they arrive.
Co-Authored-By: Claude Opus 5 <[email protected]>
Naming builds after their commit gave dist/ more than one AppImage for the
first time, and the install script took whichever `ls` listed first. That was
correct for exactly as long as builds overwrote each other: moho-76c7c45 sorts
before moho-ede11ca, so `npm run deploy` built the new one and then installed
the old one over the top of it, reporting success either way.
Caught because the launcher kept starting a build that did not have the change
in it.
It asks for the current commit's own file now, and falls back to the newest by
time where there is no git or no matching name - so a build that did not
happen fails loudly rather than quietly launching yesterday.
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]>
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]>
Clicking irc://irc.coreirc.net/elitewarez anywhere on the machine now
brings moho up on that channel.
What happens depends on whether it is a network moho is already on. If
it is, there is nothing to set up: the channel is joined on that
connection and opened, which is what clicking a link to a channel meant.
If it is not, the accounts page opens with the host, port and channel
already filled in - the nick is the only thing such a link cannot
supply, which is why there is a form at all rather than an account
simply appearing. Being on the network is not string equality either: a
link to irc.libera.chat is somewhere you already are if you connected
through irc.eu.libera.chat.
The parser is written against what people actually write rather than
against the drafts, which expired twenty years ago without agreeing. A
bare # starts a fragment, so browsers split the channel off and it has
to be put back; it also arrives percent-encoded, or doubled for a
network-wide channel. A comma is usually another channel and sometimes
one of a small fixed set of flags. A key is never kept - a password out
of a link anybody could have posted is not a thing to write to disk.
Getting there needs three separate paths because the desktops disagree:
an argument on a cold start, a second-instance event once one is already
running, and open-url on macOS. A link arriving before there is a window
is held rather than dropped, since a click that launches the app is
exactly when there is not one yet. The scheme is checked again on the
way in: it arrives as a command-line argument, and an argument is not a
promise about its own contents.
The same links in a message body are now clickable and go the same way,
rather than being handed to the desktop and bounced straight back here.
Parser checked against fifteen link shapes, including the reported one.
Two of them found real faults: several channels being cut to one, and a
key being read where the drafts put a flag.
Points at nobilis 6aa2a1e, without which a new account would have joined
nothing.
Co-Authored-By: Claude Opus 5 <[email protected]>
The AppImage went to ~/.local/bin, which is already on PATH here, with
a launcher entry and the icon at every size a panel or window switcher
might ask for - scaling one 512px icon down at display time is blurrier
than shipping the sizes.
Paired with packaging as `npm run deploy` rather than left as a manual
step. Building and installing are different acts, but the gap between
them is where a stale copy lives: a fresh AppImage sitting in dist/
while an older one runs from ~/.local/bin is exactly the confusion that
already cost an evening once.
Co-Authored-By: Claude Opus 5 <[email protected]>