Compare commits
17
Commits
@@ -1,23 +0,0 @@
|
||||
# On a merge, .gitmodules keeps the answer this branch already has.
|
||||
#
|
||||
# master names the daemon at github.com because master is what a stranger
|
||||
# clones, and development names it at the Gitea instance where the work
|
||||
# happens. Without this, a merge that touched .gitmodules on both sides would
|
||||
# stop and ask which host is right - and the answer is always "the one the
|
||||
# branch being merged into already said".
|
||||
#
|
||||
# `merge=ours` needs a driver by that name, which is one line per clone and
|
||||
# not something a repository can carry for you:
|
||||
#
|
||||
# git config merge.ours.driver true
|
||||
#
|
||||
# Without it git falls back to an ordinary merge, which conflicts rather than
|
||||
# doing the wrong thing quietly - so a clone that has not run it is no worse
|
||||
# off than before this file existed.
|
||||
#
|
||||
# The edge, because it is a real one: this keeps the *whole file*, not just the
|
||||
# URL. If development ever adds a second submodule, master takes none of that
|
||||
# entry while still taking the gitlink the merge brings with it - a submodule
|
||||
# with nothing in .gitmodules to say where it comes from. Adding a submodule is
|
||||
# therefore a thing to re-do by hand on master, and the only such thing.
|
||||
/.gitmodules merge=ours
|
||||
@@ -99,9 +99,8 @@ jobs:
|
||||
build: ${{ steps.stamp.outputs.build }}
|
||||
|
||||
steps:
|
||||
# nobilis is a submodule, and on master its URL is the public GitHub
|
||||
# one - on development it points at Gitea. That is the other reason this
|
||||
# workflow only ever runs on master.
|
||||
# nobilis is a submodule at a commit its own master holds, which is why
|
||||
# this workflow only ever runs on master.
|
||||
#
|
||||
# Depth 1 is enough: nobilis/build.rs only asks git for a short sha and
|
||||
# a dirty flag, and falls back to "unknown" rather than failing.
|
||||
@@ -290,6 +289,12 @@ jobs:
|
||||
- name: scroll check
|
||||
run: xvfb-run -a npm run test:scroll
|
||||
|
||||
# Kick event cards: where they sit in each layout, measured against a
|
||||
# chat line beside them - the picture's column, the name's place, and the
|
||||
# air above.
|
||||
- name: kick card check
|
||||
run: xvfb-run -a npm run test:kick
|
||||
|
||||
# MOHO_BUILD is computed inside the npm script - a shell assignment on
|
||||
# the electron-builder line - so it cannot be injected from out here.
|
||||
# The only way to control what the artifact is called is to guarantee
|
||||
@@ -334,7 +339,7 @@ jobs:
|
||||
retention-days: 7
|
||||
|
||||
# The daemon on its own, for nobilis's release - attached to that by
|
||||
# scripts/mirror-release.sh, since this job's token can only release
|
||||
# scripts/release-nobilis.sh, since this job's token can only release
|
||||
# here. Not an asset of moho's release: the AppImage already carries it.
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: github.ref_type == 'tag'
|
||||
|
||||
+32
-12
@@ -6,9 +6,10 @@ How the two halves fit together, what the daemon speaks, and where it puts thing
|
||||
## Why two parts
|
||||
|
||||
- **`nobilis`** (`nobilis/`, Rust) speaks each protocol directly and translates it into a small
|
||||
unified JSON model (accounts, buffers, messages) over a Unix socket. It never exposes
|
||||
protocol-specific concepts to the client; it speaks the unified model, and each backend module
|
||||
happens to be how that model gets populated for a given service.
|
||||
unified JSON model (accounts, buffers, messages) over a Unix socket. What every service has in
|
||||
common - sending, editing, reacting, backlog, search - is one set of calls and events. What
|
||||
only one service has - a Matrix room's power levels, a Discord poll, a Kick prediction - is a
|
||||
call of its own, so a client never has to pretend the services are the same where they are not.
|
||||
- **The Electron client** (`src/`) spawns and supervises `nobilis` and renders whatever it reports.
|
||||
|
||||
Keeping the protocol work in a separate long-lived daemon means connections, Tor circuits, and
|
||||
@@ -42,14 +43,29 @@ Three panes: an account-grouped buffer sidebar, the message log, and a member li
|
||||
sidebar and member list independently foldable. Closing the window hides it to the tray; nobilis
|
||||
keeps running, so connections and unread tracking survive.
|
||||
|
||||
- **Message rendering** - an IRC-style log by default, with nick colouring, BBCode and
|
||||
markdown, spoilers, code and quote blocks, inline image/video/YouTube embeds, custom emoji,
|
||||
emotes and stickers (Discord's animated Lottie ones included), reactions, replies, inline
|
||||
edit, and Sneedchat's bundled smilies. "Comfy" groups consecutive messages under one avatar,
|
||||
and "Bubbles" draws them as chat bubbles.
|
||||
- **Settings** - a category rail covering General, IRC, Sneedchat, Tor, Discord, Matrix, Kick
|
||||
- **Message rendering** - an IRC-style log by default ("Classic"), with nick colouring, BBCode and
|
||||
markdown, spoilers, code and quote blocks, inline image/video embeds, custom emoji, emotes and
|
||||
stickers (Discord's animated Lottie ones included), reactions, replies, inline edit, and
|
||||
Sneedchat's bundled smilies. "Comfy" groups consecutive messages under one avatar, and "Bubbles"
|
||||
draws them as chat bubbles. A link is described by the daemon for every service, not only the
|
||||
ones that do it themselves: a YouTube link becomes a titled card, a Tenor or Giphy link is drawn
|
||||
as the GIF, and a GIF picker sends one from the composer. What somebody did on a Kick stream -
|
||||
KICKs sent, subscriptions gifted and to whom, a raid, a redeemed reward - is drawn as a card.
|
||||
- **Settings** - a searchable rail of categories: Accounts and Downloads; General, Appearance,
|
||||
Notifications and Keybinds; a page for each network (IRC, Sneedchat, Tor, Discord, Matrix, Kick);
|
||||
and About. Message-kind filters (joins, parts, mode changes) are applied client-side, so toggling
|
||||
one takes effect immediately without a reconnect.
|
||||
- **Moving around** - Ctrl+K opens a quick switcher over every conversation on every account.
|
||||
A conversation opens at its first unread message when the unread run is short, and at its end,
|
||||
with a bar to go back by, when it is long - as Discord does it. Escape marks the conversation
|
||||
read and Shift+Escape the whole server; a muted conversation stays quiet but still counts the
|
||||
times your name was used in it.
|
||||
- **Presence and the tray** - the status menu offers only the statuses each service has (Discord
|
||||
all four, IRC away and back, Matrix online and offline) and sends nothing to a service that
|
||||
has no such thing. Do not disturb silences desktop notifications everywhere and is Discord's
|
||||
real do-not-disturb on Discord. When nothing has been used for a while an account is put on
|
||||
Idle and back, from the system's idle time where it will say and moho's own windows where it
|
||||
will not. The tray draws a number counted by the window by the rule the server rail uses.
|
||||
- **Per-protocol join pages** - each service's mechanism is genuinely different (IRC joins by
|
||||
channel name, Discord offers the friends list and sends joining, adding a friend and making
|
||||
a server to discord.com - see below - Matrix takes a room address
|
||||
@@ -165,7 +181,8 @@ called `subscribe` for that buffer.
|
||||
gateway. Messages with edit/delete/reactions/replies/forwarding, threads and forum posts, slash
|
||||
commands with buttons, menus and modal forms, polls that can be voted in, stickers sent and
|
||||
drawn, pinned messages, Discord's own search, invites, scheduled events, AutoMod's verdicts,
|
||||
and server and channel mutes read from the account itself.
|
||||
and server and channel mutes - read from the account itself, and set on it when a channel is muted
|
||||
here, so a phone is quiet too.
|
||||
|
||||
Voice and video are moho's own connections rather than a library's: voice, the camera and Go
|
||||
Live streams - hosted and watched, with sound - over Discord's RTP with DAVE end-to-end
|
||||
@@ -197,8 +214,9 @@ called `subscribe` for that buffer.
|
||||
is a renegotiation rather than a new call.
|
||||
- **Kick** - the streaming site's chat, over its Pusher socket. Joins by streamer handle, imports
|
||||
the account's follows, and carries the three emote tiers with subscriber gating (and 7TV and
|
||||
BTTV beside them), redemptions, subscriptions, gifted subs and raids, moderation, chat modes,
|
||||
and polls and predictions that can be answered rather than only watched. The stream itself
|
||||
BTTV beside them), KICKs sent to a streamer, redemptions, subscriptions, gifted subs with who
|
||||
received them, raids, moderation, chat modes, and polls and predictions that can be answered
|
||||
rather than only watched. The stream itself
|
||||
plays in the window over HLS, with its VODs and clips.
|
||||
|
||||
## On-disk state
|
||||
@@ -209,6 +227,8 @@ called `subscribe` for that buffer.
|
||||
| `~/.config/nobilis/scrollback.db` | SQLite scrollback |
|
||||
| `~/.config/nobilis/matrix-crypto/` | Matrix E2EE device keys and Olm sessions |
|
||||
| `~/.config/nobilis/voice.toml` | microphone, speakers, volumes and voice processing |
|
||||
| `~/.config/nobilis/highlights.toml`, `ignores.toml` | the words that count as a mention, and who is ignored |
|
||||
| `~/.config/nobilis/dcc.toml`, `irc-sts.toml` | DCC settings, and the IRC networks that have pinned TLS (STS) |
|
||||
| `~/.config/nobilis/tor-state/`, `tor-cache/` | the embedded Tor client's state |
|
||||
| `~/.cache/nobilis/` | re-derivable media caches (Matrix media, Discord thumbnails, stickers and sounds, Kick emotes, Sneedchat avatars and attachments), each held to a size and to thirty days, and fetched again when a missing file is looked at - see `nobilis/src/media_cache.rs` |
|
||||
| `~/.cache/moho/moho.log` | the app's log, the daemon's included |
|
||||
|
||||
+190
-92
@@ -17,6 +17,196 @@ Each section has two parts:
|
||||
that made it, including dependency updates
|
||||
(`node scripts/changelog-deps.mjs <previous tag>` lists those).
|
||||
|
||||
## [1.0.0]
|
||||
|
||||
### Major features
|
||||
|
||||
- **Five protocols in one window:** IRC, Discord, Matrix with end-to-end encryption, Sneedchat, and Kick.
|
||||
- **Discord voice and video:** calls, cameras, screen sharing and Go Live, stage channels, the soundboard and per-person volume, all with Discord's DAVE end-to-end encryption.
|
||||
- **Matrix:** encrypted rooms with device verification and key backup, Element Call, sliding sync, spaces, and signing in by QR code from a phone.
|
||||
- **Per-account Tor and SOCKS5 routing:** Clearnet, Service tunnel or Strict - Strict sends every picture, link check and embed through the account's route too. Or send everything through Tor at once.
|
||||
- **One mentions inbox across every service,** link previews, YouTube cards that play in place, voice messages, polls, and exporting a conversation to disk.
|
||||
- **A background daemon** (nobilis) that keeps every connection alive with the window closed.
|
||||
- **Discord forums:** forum and media channels appear in the channel list and open as a pane of posts - title, tags, the first message's words, replies, age, picture and top reaction - with search, sorting, older posts, and a New Post form
|
||||
- **A redesigned interface:** Discord-shaped messages with date separators and an unread bar, red mention badges, a Ctrl+K quick switcher, keyboard navigation, a rail with mention counts and folders, a member list with faces, and motion throughout
|
||||
- **Settings rebuilt:** one click from the gear, a grouped rail with a search over every setting, and new Appearance (layout, spacing, zoom, clock, reduce motion), Notifications and Keybinds pages with a hotkey recorder
|
||||
- **A one-box message composer:** a + menu, Send only when there is something to send, Shift+Enter for a new line on every service, an attachment tray with spoilers, and dropped files going into the tray
|
||||
- **Discord notices and permissions:** Discord's own safety notices drawn as the cards they are, and a banner in place of the message box wherever nothing can be written
|
||||
- **A rebuilt tray:** an icon drawn for the system's colour scheme with the number of conversations waiting on it, a tooltip that says what is waiting, and a menu of what is new, Show/Hide, Status, Settings, Restart moho and Quit - Restart moho brings back both the window and the daemon
|
||||
- **Status by service:** the plaque and the tray offer only the statuses each service has - Discord all four, IRC away and back, Matrix online and offline, Kick and Sneedchat none - and Do not disturb silences desktop notifications on every service, and is Discord's real mode there
|
||||
- **GIFs on every service:** a linked GIF is drawn as the picture - silent, looping, without the player - whichever service it came from, and a GIF picker with trending, categories and search sends one from the composer. Klipy, Tenor and Giphy pages are never probed for what they are.
|
||||
- **Idle detection:** moho notices when nothing has been used for a while, from the system where it can say and from its own windows where it cannot, and puts an account that has an idle on Idle and back. It has a setting for it and for how long, and the plaque says Idle (away) while keeping what you chose checked.
|
||||
- **Link cards on every service:** a YouTube link gets a titled card with its channel on IRC, Matrix, Kick and Sneedchat as it does on Discord - including through Tor, which YouTube refuses the usual way - and one added in an edit, read back from history, or posted before cards existed gets its card too.
|
||||
- **Kick events as cards:** KICKs sent to a streamer, gifted subscriptions with who received them, subscriptions, raids, redeemed rewards, bans and timeouts, and the stream going live or ending are each a card in moho's own theme, sitting where a person's message does in every layout.
|
||||
- **Conversations open where you stopped reading,** as Discord's do: at the first unread when the run is short, otherwise at the end with a bar to go back by, and a return to a conversation no longer lands hundreds of messages up.
|
||||
|
||||
### Fixes, patches and changes
|
||||
|
||||
- Fixed Kick's KICKs never being heard - they arrive on a subscription that was never opened - and drew KICKs, gifted subscriptions with their recipients, raids and redeemed rewards as cards ([moho `7bae0ea`](https://github.com/Moho-Chat/moho/commit/7bae0ea), [nobilis `95ee3b1`](https://github.com/Moho-Chat/nobilis/commit/95ee3b1))
|
||||
- Every other Kick event is a card too: subscriptions, bans, timeouts, unbans, a cleared chat, and the stream going live or ending ([moho `f118395`](https://github.com/Moho-Chat/moho/commit/f118395), [nobilis `23489f8`](https://github.com/Moho-Chat/nobilis/commit/23489f8))
|
||||
- Kick cards start at the picture's edge with the name where a name is in the log, in comfy and bubbles, with the same air above as a message ([moho `64ade92`](https://github.com/Moho-Chat/moho/commit/64ade92))
|
||||
- A conversation opens as Discord's does, and the line from a visit is dropped when the conversation is left ([moho `39dcba5`](https://github.com/Moho-Chat/moho/commit/39dcba5))
|
||||
- Fixed an unpinned Kick message never leaving the screen ([nobilis `81a33a1`](https://github.com/Moho-Chat/nobilis/commit/81a33a1))
|
||||
- Fixed cancelling an outgoing IRC file offer that nobody had accepted doing nothing for the three minutes the offer lasts ([nobilis `b3e92b5`](https://github.com/Moho-Chat/nobilis/commit/b3e92b5))
|
||||
- Fixed the IRC capability list being cut short when a server sends it over several lines with the continuation mark in the other field ([nobilis `b3e92b5`](https://github.com/Moho-Chat/nobilis/commit/b3e92b5))
|
||||
- Fixed a removed account leaving six lists behind in memory, among them its silenced channels and what its network said about MONITOR ([nobilis `b3e92b5`](https://github.com/Moho-Chat/nobilis/commit/b3e92b5))
|
||||
- Fixed a raid of one viewer reading "1 viewers" ([nobilis `b3e92b5`](https://github.com/Moho-Chat/nobilis/commit/b3e92b5))
|
||||
- Every test was read; those that could not fail now can, tests that checked a copy of the code now check the code, and the untested cases mutation testing found are covered ([moho `8a7e3e7`](https://github.com/Moho-Chat/moho/commit/8a7e3e7), [moho `b0e6775`](https://github.com/Moho-Chat/moho/commit/b0e6775), [moho `47653c5`](https://github.com/Moho-Chat/moho/commit/47653c5), [moho `09c060d`](https://github.com/Moho-Chat/moho/commit/09c060d), [nobilis `2477514`](https://github.com/Moho-Chat/nobilis/commit/2477514), [nobilis `b522fe6`](https://github.com/Moho-Chat/nobilis/commit/b522fe6), [nobilis `b3e92b5`](https://github.com/Moho-Chat/nobilis/commit/b3e92b5))
|
||||
- Nothing in the code or documentation refers to the maintainer's own server any more ([moho `97611c8`](https://github.com/Moho-Chat/moho/commit/97611c8), [nobilis `1c70d32`](https://github.com/Moho-Chat/nobilis/commit/1c70d32))
|
||||
- Documentation brought up to date with the settings pages, how a conversation opens, presence, link cards, Kick events and the daemon as it is ([moho `d99ad2b`](https://github.com/Moho-Chat/moho/commit/d99ad2b), [nobilis `6a1bb8e`](https://github.com/Moho-Chat/nobilis/commit/6a1bb8e))
|
||||
- Added idle detection with a setting for it and for how long ([moho `23c87dd`](https://github.com/Moho-Chat/moho/commit/23c87dd), [nobilis `35ea0b7`](https://github.com/Moho-Chat/nobilis/commit/35ea0b7))
|
||||
- Added GIFs drawn as pictures on every service, and a GIF picker ([moho `55248eb`](https://github.com/Moho-Chat/moho/commit/55248eb), [nobilis `046fbb4`](https://github.com/Moho-Chat/nobilis/commit/046fbb4))
|
||||
- Fixed a Sneedchat picture from the forum's uploads host - an AVIF, or an MP4 - not drawing: its domain is taken off and it is fetched through the account's route, clearnet or Tor ([moho `07f4fc2`](https://github.com/Moho-Chat/moho/commit/07f4fc2), [nobilis `ca29a63`](https://github.com/Moho-Chat/nobilis/commit/ca29a63))
|
||||
- Fixed the tray's number drifting from the client's: it is now counted by the window, by the rail's rule, and told to the tray whenever it changes ([moho `1e314f8`](https://github.com/Moho-Chat/moho/commit/1e314f8))
|
||||
- Fixed YouTube cards never appearing for an account on Tor: YouTube answers every Tor exit with a 403 on its card lookup, so the video's own page is read when that happens ([moho `8064e79`](https://github.com/Moho-Chat/moho/commit/8064e79), [nobilis `340342b`](https://github.com/Moho-Chat/nobilis/commit/340342b))
|
||||
- Fixed link cards being lost when a message was edited, and missing from Matrix history, Discord catch-up and older messages ([moho `638b02e`](https://github.com/Moho-Chat/moho/commit/638b02e), [nobilis `dab394f`](https://github.com/Moho-Chat/nobilis/commit/dab394f))
|
||||
- Fixed scrolling through a long run of unread messages snapping back to where the "new messages" bar appeared ([moho `336335a`](https://github.com/Moho-Chat/moho/commit/336335a))
|
||||
- The number beside a conversation is what is addressed to you - mentions in a channel, each unread message in a direct message - and the bold name is the sign of activity ([moho `987f31c`](https://github.com/Moho-Chat/moho/commit/987f31c))
|
||||
- A muted channel keeps its mention count in the list, the rail, the switcher and the tray, and stops the popups; muting a Discord channel or direct message is now set on the account, so a phone is quiet too ([moho `7803cc3`](https://github.com/Moho-Chat/moho/commit/7803cc3), [nobilis `b0fa250`](https://github.com/Moho-Chat/nobilis/commit/b0fa250))
|
||||
- Reworked the tray icon and menu, and dropped Restart daemon and Stop daemon from it ([moho `24106e5`](https://github.com/Moho-Chat/moho/commit/24106e5))
|
||||
- Do not disturb now silences an account's desktop notifications and the taskbar flash; the tray no longer has a switch of its own for it ([moho `53fb0f3`](https://github.com/Moho-Chat/moho/commit/53fb0f3))
|
||||
- Idle from the tray reaches Discord and IRC and leaves Matrix, Kick and Sneedchat as they were; a service is no longer asked for a status it does not have ([moho `c1bcb72`](https://github.com/Moho-Chat/moho/commit/c1bcb72), [nobilis `a8441ab`](https://github.com/Moho-Chat/nobilis/commit/a8441ab))
|
||||
- Fixed Matrix's rate limit on changing status showing as M_LIMIT_EXCEEDED: a limited request now waits the time the server names, and a longer wait is applied when it is over ([moho `0dfff0d`](https://github.com/Moho-Chat/moho/commit/0dfff0d), [nobilis `43135c1`](https://github.com/Moho-Chat/nobilis/commit/43135c1))
|
||||
- Fixed the window being pulled back to the open conversation's server whenever you chose another: the daemon link coming up is acted on once, a refresh no longer follows the open conversation, and the tray is told only what changed ([moho `745641a`](https://github.com/Moho-Chat/moho/commit/745641a))
|
||||
- Matrix's invisible is called Offline in the plaque, its popout and the tray, which is what it sets ([moho `745641a`](https://github.com/Moho-Chat/moho/commit/745641a))
|
||||
- A room muted on a Matrix account can now be unmuted from the menu, including a mute set from Element ([moho `745641a`](https://github.com/Moho-Chat/moho/commit/745641a), [nobilis `59db82c`](https://github.com/Moho-Chat/nobilis/commit/59db82c))
|
||||
- Server rail tiles now count the mentions waiting in them rather than the messages, and a muted room counts for nothing, mentions included ([moho `0dc048c`](https://github.com/Moho-Chat/moho/commit/0dc048c))
|
||||
- Fixed a room unmuted on a Matrix account still being drawn as muted; a mute or unmute the server did not take is now reported with its reason, and Matrix requests to a server that does not answer give up ([moho `95f1784`](https://github.com/Moho-Chat/moho/commit/95f1784), [nobilis `d1bbcaf`](https://github.com/Moho-Chat/nobilis/commit/d1bbcaf))
|
||||
- Discord requests now give up on a connection that has gone quiet, a history catch-up always releases its place, and the gateway logs what it receives and why a message is dropped ([moho `b151ef8`](https://github.com/Moho-Chat/moho/commit/b151ef8), [nobilis `a34821e`](https://github.com/Moho-Chat/nobilis/commit/a34821e))
|
||||
- Fixed Discord reconnecting every few seconds after being told it is rate limited: it now waits out the minute Discord asks for ([nobilis `59db82c`](https://github.com/Moho-Chat/nobilis/commit/59db82c))
|
||||
- Fixed Sneedchat pictures posted back to back by a script merging into one unusable link ([moho `6edae7a`](https://github.com/Moho-Chat/moho/commit/6edae7a))
|
||||
- Discord files staged together now go as one message, with an upload ring that fills as it uploads ([moho `9888c80`](https://github.com/Moho-Chat/moho/commit/9888c80), [nobilis `9690905`](https://github.com/Moho-Chat/nobilis/commit/9690905))
|
||||
- Fixed the release mirror script printing a traceback while the release did not exist yet ([moho `a8d9f41`](https://github.com/Moho-Chat/moho/commit/a8d9f41))
|
||||
- Defined the design tokens the stylesheet used but never had, and applied one set across it ([moho `9db25c8`](https://github.com/Moho-Chat/moho/commit/9db25c8), [moho `3cdfe37`](https://github.com/Moho-Chat/moho/commit/3cdfe37))
|
||||
- Added a screenshot harness for before-and-after pictures of the interface ([moho `88375ba`](https://github.com/Moho-Chat/moho/commit/88375ba))
|
||||
- Unread channels now read as unread, mentions are red, and every badge is one style ([moho `4ca37dd`](https://github.com/Moho-Chat/moho/commit/4ca37dd))
|
||||
- Added date separators to the message list ([moho `b2464e4`](https://github.com/Moho-Chat/moho/commit/b2464e4))
|
||||
- Added Mark as read on channels, categories and servers, and Escape to read what you are in ([moho `2745a2f`](https://github.com/Moho-Chat/moho/commit/2745a2f))
|
||||
- Added a Ctrl+K quick switcher and keyboard navigation between channels ([moho `1eef37e`](https://github.com/Moho-Chat/moho/commit/1eef37e))
|
||||
- Voice panel: a horizontal microphone bar, the connection's real latency, and a share button ([moho `ad0f0c0`](https://github.com/Moho-Chat/moho/commit/ad0f0c0), [nobilis `e0f6169`](https://github.com/Moho-Chat/nobilis/commit/e0f6169))
|
||||
- Replies jump to the message they answer, and names and pictures open the profile ([moho `bf45147`](https://github.com/Moho-Chat/moho/commit/bf45147))
|
||||
- Added Copy and Copy text on every message, and Copy message link on Discord ([moho `0033955`](https://github.com/Moho-Chat/moho/commit/0033955))
|
||||
- The hover toolbar fades in, can be reached by keyboard, and offers Edit, Copy and your recent reactions ([moho `57e1856`](https://github.com/Moho-Chat/moho/commit/57e1856))
|
||||
- Fixed six interface bugs: a nested voice button, folded categories hiding unread, muted channels hiding mentions, overlapping corners, the typing strip moving the log, and pinned rows missing their service mark ([moho `6598043`](https://github.com/Moho-Chat/moho/commit/6598043))
|
||||
- Pending sends dim only their text, failed ones can be deleted, and Up in an empty box edits your last message in a multi-line editor ([moho `6a82c32`](https://github.com/Moho-Chat/moho/commit/6a82c32))
|
||||
- Server rail: mention counts, folders that say what is waiting and stay open, an instant name flyout and a red close button ([moho `8072b24`](https://github.com/Moho-Chat/moho/commit/8072b24))
|
||||
- Member list: faces, presence dots, and a click for the profile; Discord's member pictures now come with the list ([moho `c912ed3`](https://github.com/Moho-Chat/moho/commit/c912ed3), [nobilis `4099305`](https://github.com/Moho-Chat/nobilis/commit/4099305))
|
||||
- Voice channel list: faces, muted and deafened marks, a speaking ring and a collapsible heading ([moho `843288d`](https://github.com/Moho-Chat/moho/commit/843288d))
|
||||
- Comfy messages in Discord's shape, amber mention rows, one code and quote style, and marked system lines ([moho `3a7677e`](https://github.com/Moho-Chat/moho/commit/3a7677e))
|
||||
- Buttons are primary, subtle or the one danger style, and the restarts ask first ([moho `2c8609c`](https://github.com/Moho-Chat/moho/commit/2c8609c))
|
||||
- Escape closes only the top layer, and full-page panels close on Escape ([moho `c5e7619`](https://github.com/Moho-Chat/moho/commit/c5e7619))
|
||||
- Thread panel reads the display settings, groups replies and follows the newest ([moho `33129f9`](https://github.com/Moho-Chat/moho/commit/33129f9))
|
||||
- Toasts slide in and out, come in three kinds, pause on hover and fold repeats ([moho `2d1b8be`](https://github.com/Moho-Chat/moho/commit/2d1b8be))
|
||||
- Context menu: keyboard navigation, checked items, shortcut hints, and placement that holds while it animates ([moho `4065963`](https://github.com/Moho-Chat/moho/commit/4065963), [moho `fcb7df5`](https://github.com/Moho-Chat/moho/commit/fcb7df5))
|
||||
- A conversation opens at its first unread, with a bar saying how many are above ([moho `55fd192`](https://github.com/Moho-Chat/moho/commit/55fd192))
|
||||
- Added hover fades, turning fold arrows, arriving messages, a reaction pop and growing overlays ([moho `6d8a050`](https://github.com/Moho-Chat/moho/commit/6d8a050))
|
||||
- Every dialog is built from one shared frame with a close button, kept focus and a scrim below the title bar ([moho `5cb30ce`](https://github.com/Moho-Chat/moho/commit/5cb30ce), [moho `420a2f7`](https://github.com/Moho-Chat/moho/commit/420a2f7), [moho `6a329c6`](https://github.com/Moho-Chat/moho/commit/6a329c6))
|
||||
- Shift+Enter makes a new line on every service and pasted line breaks stay; IRC sends them as a batch or separate lines, Sneedchat as [br], Kick as one line ([moho `9ba630c`](https://github.com/Moho-Chat/moho/commit/9ba630c), [nobilis `ccc8de6`](https://github.com/Moho-Chat/nobilis/commit/ccc8de6))
|
||||
- Reply bar: its own surface, the author's face, Esc to cancel, and an @ switch to answer without pinging on Discord ([moho `f48185e`](https://github.com/Moho-Chat/moho/commit/f48185e), [nobilis `918fcfa`](https://github.com/Moho-Chat/nobilis/commit/918fcfa))
|
||||
- One style for the name, command and emoji lists, with member pictures and :shortcode emoji completion ([moho `28d8b3c`](https://github.com/Moho-Chat/moho/commit/28d8b3c))
|
||||
- Lightbox: step through a conversation's pictures, wheel and pinch zoom with panning, and Copy picture ([moho `817cb1e`](https://github.com/Moho-Chat/moho/commit/817cb1e))
|
||||
- Emoji picker: walk it with the arrow keys, Enter picks the top result, and skin tones are remembered ([moho `99fd0d3`](https://github.com/Moho-Chat/moho/commit/99fd0d3))
|
||||
- Attachment tray cards with name, size and a Discord spoiler toggle; dropped files go into the tray instead of being sent at once ([moho `15e7061`](https://github.com/Moho-Chat/moho/commit/15e7061), [nobilis `0f5669b`](https://github.com/Moho-Chat/nobilis/commit/0f5669b))
|
||||
- Composer: one box with the + on the left and emoji, stickers, voice and Send on the right ([moho `a59c1ab`](https://github.com/Moho-Chat/moho/commit/a59c1ab))
|
||||
- Several pictures in one message lay out as a grid, and SPOILER_ files are blurred until clicked ([moho `87ea3b8`](https://github.com/Moho-Chat/moho/commit/87ea3b8))
|
||||
- Pinned messages show as a banner over the log for Discord and Matrix, with a count on the header ([moho `3a74cf0`](https://github.com/Moho-Chat/moho/commit/3a74cf0))
|
||||
- Reactions say what they are and who gave them on hover ([moho `506140d`](https://github.com/Moho-Chat/moho/commit/506140d), [nobilis `1da3ad8`](https://github.com/Moho-Chat/nobilis/commit/1da3ad8))
|
||||
- User footer: muted and deafened are red, voice buttons stay on every service, and the status is a popout with a custom status for Discord and Matrix and Apply to all accounts ([moho `c026609`](https://github.com/Moho-Chat/moho/commit/c026609), [nobilis `f280563`](https://github.com/Moho-Chat/nobilis/commit/f280563))
|
||||
- The footer's join button is gone; the rail's + is the one way in, pinned under a rule above the settings cog while the server list scrolls ([moho `b4c1ced`](https://github.com/Moho-Chat/moho/commit/b4c1ced), [moho `dad5543`](https://github.com/Moho-Chat/moho/commit/dad5543))
|
||||
- Header: one 48px row across every column, filled toggles, and the channel topic from IRC, Matrix and Discord ([moho `2ef9e35`](https://github.com/Moho-Chat/moho/commit/2ef9e35), [nobilis `44d1611`](https://github.com/Moho-Chat/nobilis/commit/44d1611))
|
||||
- "(edited)" says when on hover, for edits seen live ([moho `7f4c356`](https://github.com/Moho-Chat/moho/commit/7f4c356), [nobilis `e51f139`](https://github.com/Moho-Chat/nobilis/commit/e51f139))
|
||||
- Incoming Discord and Matrix calls share one card style and stack ([moho `59d4893`](https://github.com/Moho-Chat/moho/commit/59d4893))
|
||||
- Settings: the gear opens it directly, with Accounts and Downloads as pages, a search, one switch for every boolean and a saved tick ([moho `66a0c0f`](https://github.com/Moho-Chat/moho/commit/66a0c0f), [moho `cc4c9f6`](https://github.com/Moho-Chat/moho/commit/cc4c9f6))
|
||||
- Settings: added Appearance, Notifications and Keybinds pages, the last with a recorder for the global hotkey ([moho `fe6ad97`](https://github.com/Moho-Chat/moho/commit/fe6ad97), [moho `adca84d`](https://github.com/Moho-Chat/moho/commit/adca84d), [moho `82a0bdf`](https://github.com/Moho-Chat/moho/commit/82a0bdf))
|
||||
- Accounts open by default with few accounts, Remove sits in a danger zone, and Join picks its account and says Join ([moho `f8dcccd`](https://github.com/Moho-Chat/moho/commit/f8dcccd))
|
||||
- Mentions page: service and unread filters, day groups, two-line bodies and put-away ([moho `078fe41`](https://github.com/Moho-Chat/moho/commit/078fe41))
|
||||
- Room settings is a tabbed dialog instead of a popover ([moho `f8936c6`](https://github.com/Moho-Chat/moho/commit/f8936c6))
|
||||
- Quiet buttons such as Cancel have their own fill, so they no longer vanish into a dialog ([moho `50aaa97`](https://github.com/Moho-Chat/moho/commit/50aaa97))
|
||||
- Fixed browsing an IRC network's channels freezing or blanking the window: the daemon holds the list and sends a page, it times out after a minute, and a refusal is reported ([moho `2b41c5e`](https://github.com/Moho-Chat/moho/commit/2b41c5e), [nobilis `152fdf1`](https://github.com/Moho-Chat/nobilis/commit/152fdf1))
|
||||
- Channels in the list no longer jump to the top as messages arrive ([moho `9f03d6b`](https://github.com/Moho-Chat/moho/commit/9f03d6b))
|
||||
- Fixed Discord's notices showing only a stand-in title: they are read as cards, and a banner replaces the message box in them and in channels this account cannot write in ([moho `ff7d313`](https://github.com/Moho-Chat/moho/commit/ff7d313), [moho `d7c4215`](https://github.com/Moho-Chat/moho/commit/d7c4215), [moho `2a4f797`](https://github.com/Moho-Chat/moho/commit/2a4f797), [nobilis `1b270be`](https://github.com/Moho-Chat/nobilis/commit/1b270be), [nobilis `97ac6bc`](https://github.com/Moho-Chat/nobilis/commit/97ac6bc), [nobilis `7561b88`](https://github.com/Moho-Chat/nobilis/commit/7561b88))
|
||||
- A window whose page dies is reloaded and the reason logged, instead of being left empty ([moho `6899694`](https://github.com/Moho-Chat/moho/commit/6899694), [moho `06bb812`](https://github.com/Moho-Chat/moho/commit/06bb812))
|
||||
- Discord forums, with live and archived posts read together ([moho `dd0c25e`](https://github.com/Moho-Chat/moho/commit/dd0c25e), [nobilis `d02bba0`](https://github.com/Moho-Chat/nobilis/commit/d02bba0), [nobilis `8363003`](https://github.com/Moho-Chat/nobilis/commit/8363003), [nobilis `3cf1a5b`](https://github.com/Moho-Chat/nobilis/commit/3cf1a5b), [nobilis `6abcb8e`](https://github.com/Moho-Chat/nobilis/commit/6abcb8e))
|
||||
- Added Rust crate tokio-util 0.7.19 ([nobilis `9690905`](https://github.com/Moho-Chat/nobilis/commit/9690905))
|
||||
- Channel rows in the sidebar now show a single `#` instead of two ([moho `ccdf090`](https://github.com/Moho-Chat/moho/commit/ccdf090))
|
||||
- Typing anywhere in the window now goes into the message box, without clicking it first ([moho `b5f920f`](https://github.com/Moho-Chat/moho/commit/b5f920f))
|
||||
- Added a formatting strip over selected text in the message box, offering the formats each service can carry ([moho `9ca477c`](https://github.com/Moho-Chat/moho/commit/9ca477c))
|
||||
- Added a right-click menu to every text field, with spelling suggestions, Undo, Cut, Copy, Paste and Select all ([moho `b2a6210`](https://github.com/Moho-Chat/moho/commit/b2a6210))
|
||||
- Fixed YouTube cards not appearing on Sneedchat when YouTube refused a Tor exit, so most links there had none ([moho `0368796`](https://github.com/Moho-Chat/moho/commit/0368796), [nobilis `71e6036`](https://github.com/Moho-Chat/nobilis/commit/71e6036))
|
||||
- Fixed live and premiere YouTube videos vanishing because they have no thumbnail yet ([moho `916abd4`](https://github.com/Moho-Chat/moho/commit/916abd4))
|
||||
- Fixed YouTube links with the video id after other parameters, `music.youtube.com` and `youtube-nocookie.com` links getting no card ([nobilis `f8409fc`](https://github.com/Moho-Chat/nobilis/commit/f8409fc))
|
||||
- Patched a flaw that let a message's content make moho load a local file ([moho `7138201`](https://github.com/Moho-Chat/moho/commit/7138201), [nobilis `f972611`](https://github.com/Moho-Chat/nobilis/commit/f972611))
|
||||
- Strict routing now sends a Tor or proxy account's pictures, links and embeds through its route instead of directly ([moho `cee8604`](https://github.com/Moho-Chat/moho/commit/cee8604), [nobilis `a3897ba`](https://github.com/Moho-Chat/nobilis/commit/a3897ba))
|
||||
- Discord voice and soundboard audio is now checked before it reaches the C decoder ([nobilis `8eca16e`](https://github.com/Moho-Chat/nobilis/commit/8eca16e))
|
||||
- Fixed Discord showing "connecting" after every successful resume ([nobilis `52b0001`](https://github.com/Moho-Chat/nobilis/commit/52b0001))
|
||||
- Fixed reconnect waits growing to a minute and never resetting ([nobilis `52b0001`](https://github.com/Moho-Chat/nobilis/commit/52b0001))
|
||||
- The interruption banner now counts down to the next reconnect attempt ([moho `89da9ba`](https://github.com/Moho-Chat/moho/commit/89da9ba))
|
||||
- Fixed Kick over Tor failing every channel lookup until a reconnect ([nobilis `5755c4e`](https://github.com/Moho-Chat/nobilis/commit/5755c4e))
|
||||
- Fixed failed Kick joins vanishing without a message ([nobilis `5755c4e`](https://github.com/Moho-Chat/nobilis/commit/5755c4e))
|
||||
- IRC now shows the server's own reason when it refuses a connection ([nobilis `9eeab7e`](https://github.com/Moho-Chat/nobilis/commit/9eeab7e))
|
||||
- Fixed Disconnect and Remove showing a "cancelled" error ([nobilis `8bcba8b`](https://github.com/Moho-Chat/nobilis/commit/8bcba8b))
|
||||
- Fixed new accounts not appearing on the server rail until a restart ([moho `e207224`](https://github.com/Moho-Chat/moho/commit/e207224))
|
||||
- Joining a channel now opens it ([moho `e207224`](https://github.com/Moho-Chat/moho/commit/e207224))
|
||||
- Removing an account now asks first ([moho `16fa8a2`](https://github.com/Moho-Chat/moho/commit/16fa8a2))
|
||||
- Fixed the Matrix form saying matrix.org takes no new accounts ([moho `16fa8a2`](https://github.com/Moho-Chat/moho/commit/16fa8a2))
|
||||
- Fixed Kick's mark drawing as a blank square in the accounts pane ([moho `0ea63f5`](https://github.com/Moho-Chat/moho/commit/0ea63f5))
|
||||
- Fixed a cleared service interruption staying on screen ([moho `fd09b6b`](https://github.com/Moho-Chat/moho/commit/fd09b6b))
|
||||
- Conversations now show at start, before their account has connected ([nobilis `7976452`](https://github.com/Moho-Chat/nobilis/commit/7976452))
|
||||
- Fixed Kick follows being saved before Kick confirmed the channel exists ([nobilis `c2e40dd`](https://github.com/Moho-Chat/nobilis/commit/c2e40dd), [nobilis `4d8f17b`](https://github.com/Moho-Chat/nobilis/commit/4d8f17b))
|
||||
- Kick requests are now spaced out, and a rate limit pauses every Kick request instead of retrying at once ([nobilis `f592a22`](https://github.com/Moho-Chat/nobilis/commit/f592a22), [nobilis `c99d52b`](https://github.com/Moho-Chat/nobilis/commit/c99d52b), [nobilis `2732d08`](https://github.com/Moho-Chat/nobilis/commit/2732d08))
|
||||
- Fixed a changed Sneedchat avatar never being fetched again ([nobilis `04cb759`](https://github.com/Moho-Chat/nobilis/commit/04cb759))
|
||||
- Every media cache now expires old files, with a way to fetch removed pictures again ([nobilis `25a0912`](https://github.com/Moho-Chat/nobilis/commit/25a0912), [moho `9e67ca6`](https://github.com/Moho-Chat/moho/commit/9e67ca6))
|
||||
- Fixed Kick emotes removed by the cache sweep not drawing again ([moho `70cd8d7`](https://github.com/Moho-Chat/moho/commit/70cd8d7))
|
||||
- Fixed Matrix reactions being counted more than once ([nobilis `8ea9a70`](https://github.com/Moho-Chat/nobilis/commit/8ea9a70))
|
||||
- Fixed the scrollback being written to disk once per message ([nobilis `bd33f88`](https://github.com/Moho-Chat/nobilis/commit/bd33f88))
|
||||
- Fixed the migration deleting the client's own config directory ([nobilis `9e885a4`](https://github.com/Moho-Chat/nobilis/commit/9e885a4), [nobilis `207de58`](https://github.com/Moho-Chat/nobilis/commit/207de58))
|
||||
- Fixed a removed account leaving its data behind ([nobilis `2dd755e`](https://github.com/Moho-Chat/nobilis/commit/2dd755e))
|
||||
- Fixed new members of an encrypted Matrix room not receiving its keys ([nobilis `b894dea`](https://github.com/Moho-Chat/nobilis/commit/b894dea))
|
||||
- Fixed two ways a Matrix sign-in could leave an account permanently unusable ([nobilis `ded9cc5`](https://github.com/Moho-Chat/nobilis/commit/ded9cc5))
|
||||
- Fixed sliding sync failing against Synapse ([nobilis `e2bb988`](https://github.com/Moho-Chat/nobilis/commit/e2bb988))
|
||||
- Fixed Discord media links expiring on screen; they now refresh themselves ([moho `dc15c4b`](https://github.com/Moho-Chat/moho/commit/dc15c4b), [nobilis `f5a455b`](https://github.com/Moho-Chat/nobilis/commit/f5a455b))
|
||||
- Repaired Discord messages stored with attachments in their text ([nobilis `15007d6`](https://github.com/Moho-Chat/nobilis/commit/15007d6))
|
||||
- Fixed Discord streams opening a second connection ([nobilis `7b64414`](https://github.com/Moho-Chat/nobilis/commit/7b64414))
|
||||
- Fixed lost video packets, phone streams and stream sound in Discord calls ([nobilis `92dffca`](https://github.com/Moho-Chat/nobilis/commit/92dffca))
|
||||
- Fixed the Discord stream heartbeat using the wrong protocol version ([nobilis `64dfa08`](https://github.com/Moho-Chat/nobilis/commit/64dfa08))
|
||||
- Fixed a running upload being reported as failed ([moho `ee60271`](https://github.com/Moho-Chat/moho/commit/ee60271))
|
||||
- Fixed thumbnails opening as the thumbnail instead of the picture ([moho `6af6ac3`](https://github.com/Moho-Chat/moho/commit/6af6ac3))
|
||||
- Fixed avif links not drawing as pictures ([moho `1511c4b`](https://github.com/Moho-Chat/moho/commit/1511c4b), [nobilis `4e3c65d`](https://github.com/Moho-Chat/nobilis/commit/4e3c65d))
|
||||
- Fixed links in brackets not being recognised ([nobilis `dd450eb`](https://github.com/Moho-Chat/nobilis/commit/dd450eb))
|
||||
- Fixed a full-size photo being downloaded to draw a thumbnail ([nobilis `873a908`](https://github.com/Moho-Chat/nobilis/commit/873a908))
|
||||
- Fixed a crash in the room preview panel ([moho `04a6b7d`](https://github.com/Moho-Chat/moho/commit/04a6b7d))
|
||||
- Fixed call renegotiation not waiting for its answer ([moho `db0f651`](https://github.com/Moho-Chat/moho/commit/db0f651))
|
||||
- Fixed a screen share on Wayland asking twice ([moho `2c146d9`](https://github.com/Moho-Chat/moho/commit/2c146d9))
|
||||
- Discord no longer attempts what it puts behind a captcha ([nobilis `8da2e7a`](https://github.com/Moho-Chat/nobilis/commit/8da2e7a))
|
||||
- The renderer now runs sandboxed ([moho `e791fcf`](https://github.com/Moho-Chat/moho/commit/e791fcf))
|
||||
- The installer warns when Windows Smart App Control will block moho ([moho `5a8d7f7`](https://github.com/Moho-Chat/moho/commit/5a8d7f7))
|
||||
- The uninstaller now offers to remove saved sign-ins and history, and stops the daemon first ([moho `76eabac`](https://github.com/Moho-Chat/moho/commit/76eabac))
|
||||
- Linux builds now run on Ubuntu 22.04 and later ([moho `4c6e24b`](https://github.com/Moho-Chat/moho/commit/4c6e24b))
|
||||
- Updated Rust crate h2 from 0.4.15 to 0.4.19 ([nobilis `2f5918e`](https://github.com/Moho-Chat/nobilis/commit/2f5918e))
|
||||
- Added Rust crate opus2 0.4.0 ([nobilis `f9b5be5`](https://github.com/Moho-Chat/nobilis/commit/f9b5be5))
|
||||
- Added Rust crate aes-gcm 0.10.3 ([nobilis `9d50723`](https://github.com/Moho-Chat/nobilis/commit/9d50723))
|
||||
- Added Rust crate chacha20poly1305 0.10.1 ([nobilis `9d50723`](https://github.com/Moho-Chat/nobilis/commit/9d50723))
|
||||
- Added Rust crate davey 0.1.4 ([nobilis `8ae2148`](https://github.com/Moho-Chat/nobilis/commit/8ae2148))
|
||||
- Removed Rust crate async-trait 0.1.92 ([nobilis `cbb044d`](https://github.com/Moho-Chat/nobilis/commit/cbb044d))
|
||||
- Removed Rust crate songbird 0.6.0 ([nobilis `cbb044d`](https://github.com/Moho-Chat/nobilis/commit/cbb044d))
|
||||
- Added Rust crate socket2 0.6.5 ([nobilis `92dffca`](https://github.com/Moho-Chat/nobilis/commit/92dffca))
|
||||
- Added Rust crate sonora 0.2.0 ([nobilis `72ef1b5`](https://github.com/Moho-Chat/nobilis/commit/72ef1b5))
|
||||
- Updated Rust crate arti-client from 0.44.0 to 0.47.0 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate matrix-sdk-common from 0.18.0 to 0.19.1 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate matrix-sdk-crypto from 0.18.0 to 0.19.1 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate matrix-sdk-sqlite from 0.18.0 to 0.19.1 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate ruma-client-api from 0.24.0 to 0.25.0 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate ruma-common from 0.19.0 to 0.20.0 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate ruma-events from 0.34.0 to 0.35.0 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate rusqlite from 0.37.0 to 0.40.2 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate tor-rtcompat from 0.44.0 to 0.47.0 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate rustls from 0.23.43 to 0.23.45 ([nobilis `2f5918e`](https://github.com/Moho-Chat/nobilis/commit/2f5918e))
|
||||
- Updated npm package electron-builder from 25.1.8 to 26.15.3 ([moho `e791fcf`](https://github.com/Moho-Chat/moho/commit/e791fcf))
|
||||
- Updated npm package electron from 33.4.11 to 44.4.4 ([moho `932a07a`](https://github.com/Moho-Chat/moho/commit/932a07a))
|
||||
- Added npm package lottie-web 5.13.0 ([moho `f67c753`](https://github.com/Moho-Chat/moho/commit/f67c753))
|
||||
- Added npm package happy-dom 15.11.7 ([moho `53a6f92`](https://github.com/Moho-Chat/moho/commit/53a6f92))
|
||||
- Added npm package vitest 2.1.9 ([moho `53a6f92`](https://github.com/Moho-Chat/moho/commit/53a6f92))
|
||||
|
||||
## [1.0.0-rc.5]
|
||||
|
||||
### Major features
|
||||
@@ -124,95 +314,3 @@ Each section has two parts:
|
||||
- A window whose page dies is reloaded and the reason logged, instead of being left empty ([moho `6899694`](https://github.com/Moho-Chat/moho/commit/6899694), [moho `06bb812`](https://github.com/Moho-Chat/moho/commit/06bb812))
|
||||
- Discord forums, with live and archived posts read together ([moho `dd0c25e`](https://github.com/Moho-Chat/moho/commit/dd0c25e), [nobilis `d02bba0`](https://github.com/Moho-Chat/nobilis/commit/d02bba0), [nobilis `8363003`](https://github.com/Moho-Chat/nobilis/commit/8363003), [nobilis `3cf1a5b`](https://github.com/Moho-Chat/nobilis/commit/3cf1a5b), [nobilis `6abcb8e`](https://github.com/Moho-Chat/nobilis/commit/6abcb8e))
|
||||
- Added Rust crate tokio-util 0.7.19 ([nobilis `9690905`](https://github.com/Moho-Chat/nobilis/commit/9690905))
|
||||
|
||||
## [1.0.0]
|
||||
|
||||
### Major features
|
||||
|
||||
- **Five protocols in one window:** IRC, Discord, Matrix with end-to-end encryption, Sneedchat, and Kick.
|
||||
- **Discord voice and video:** calls, cameras, screen sharing and Go Live, stage channels, the soundboard and per-person volume, all with Discord's DAVE end-to-end encryption.
|
||||
- **Matrix:** encrypted rooms with device verification and key backup, Element Call, sliding sync, spaces, and signing in by QR code from a phone.
|
||||
- **Per-account Tor and SOCKS5 routing:** Clearnet, Service tunnel or Strict - Strict sends every picture, link check and embed through the account's route too. Or send everything through Tor at once.
|
||||
- **One mentions inbox across every service,** link previews, YouTube cards that play in place, voice messages, polls, and exporting a conversation to disk.
|
||||
- **A background daemon** (nobilis) that keeps every connection alive with the window closed.
|
||||
|
||||
### Fixes, patches and changes
|
||||
|
||||
- Channel rows in the sidebar now show a single `#` instead of two ([moho `ccdf090`](https://github.com/Moho-Chat/moho/commit/ccdf090))
|
||||
- Typing anywhere in the window now goes into the message box, without clicking it first ([moho `b5f920f`](https://github.com/Moho-Chat/moho/commit/b5f920f))
|
||||
- Added a formatting strip over selected text in the message box, offering the formats each service can carry ([moho `9ca477c`](https://github.com/Moho-Chat/moho/commit/9ca477c))
|
||||
- Added a right-click menu to every text field, with spelling suggestions, Undo, Cut, Copy, Paste and Select all ([moho `b2a6210`](https://github.com/Moho-Chat/moho/commit/b2a6210))
|
||||
- Fixed YouTube cards not appearing on Sneedchat when YouTube refused a Tor exit, so most links there had none ([moho `0368796`](https://github.com/Moho-Chat/moho/commit/0368796), [nobilis `71e6036`](https://github.com/Moho-Chat/nobilis/commit/71e6036))
|
||||
- Fixed live and premiere YouTube videos vanishing because they have no thumbnail yet ([moho `916abd4`](https://github.com/Moho-Chat/moho/commit/916abd4))
|
||||
- Fixed YouTube links with the video id after other parameters, `music.youtube.com` and `youtube-nocookie.com` links getting no card ([nobilis `f8409fc`](https://github.com/Moho-Chat/nobilis/commit/f8409fc))
|
||||
- Patched a flaw that let a message's content make moho load a local file ([moho `7138201`](https://github.com/Moho-Chat/moho/commit/7138201), [nobilis `f972611`](https://github.com/Moho-Chat/nobilis/commit/f972611))
|
||||
- Strict routing now sends a Tor or proxy account's pictures, links and embeds through its route instead of directly ([moho `cee8604`](https://github.com/Moho-Chat/moho/commit/cee8604), [nobilis `a3897ba`](https://github.com/Moho-Chat/nobilis/commit/a3897ba))
|
||||
- Discord voice and soundboard audio is now checked before it reaches the C decoder ([nobilis `8eca16e`](https://github.com/Moho-Chat/nobilis/commit/8eca16e))
|
||||
- Fixed Discord showing "connecting" after every successful resume ([nobilis `52b0001`](https://github.com/Moho-Chat/nobilis/commit/52b0001))
|
||||
- Fixed reconnect waits growing to a minute and never resetting ([nobilis `52b0001`](https://github.com/Moho-Chat/nobilis/commit/52b0001))
|
||||
- The interruption banner now counts down to the next reconnect attempt ([moho `89da9ba`](https://github.com/Moho-Chat/moho/commit/89da9ba))
|
||||
- Fixed Kick over Tor failing every channel lookup until a reconnect ([nobilis `5755c4e`](https://github.com/Moho-Chat/nobilis/commit/5755c4e))
|
||||
- Fixed failed Kick joins vanishing without a message ([nobilis `5755c4e`](https://github.com/Moho-Chat/nobilis/commit/5755c4e))
|
||||
- IRC now shows the server's own reason when it refuses a connection ([nobilis `9eeab7e`](https://github.com/Moho-Chat/nobilis/commit/9eeab7e))
|
||||
- Fixed Disconnect and Remove showing a "cancelled" error ([nobilis `8bcba8b`](https://github.com/Moho-Chat/nobilis/commit/8bcba8b))
|
||||
- Fixed new accounts not appearing on the server rail until a restart ([moho `e207224`](https://github.com/Moho-Chat/moho/commit/e207224))
|
||||
- Joining a channel now opens it ([moho `e207224`](https://github.com/Moho-Chat/moho/commit/e207224))
|
||||
- Removing an account now asks first ([moho `16fa8a2`](https://github.com/Moho-Chat/moho/commit/16fa8a2))
|
||||
- Fixed the Matrix form saying matrix.org takes no new accounts ([moho `16fa8a2`](https://github.com/Moho-Chat/moho/commit/16fa8a2))
|
||||
- Fixed Kick's mark drawing as a blank square in the accounts pane ([moho `0ea63f5`](https://github.com/Moho-Chat/moho/commit/0ea63f5))
|
||||
- Fixed a cleared service interruption staying on screen ([moho `fd09b6b`](https://github.com/Moho-Chat/moho/commit/fd09b6b))
|
||||
- Conversations now show at start, before their account has connected ([nobilis `7976452`](https://github.com/Moho-Chat/nobilis/commit/7976452))
|
||||
- Fixed Kick follows being saved before Kick confirmed the channel exists ([nobilis `c2e40dd`](https://github.com/Moho-Chat/nobilis/commit/c2e40dd), [nobilis `4d8f17b`](https://github.com/Moho-Chat/nobilis/commit/4d8f17b))
|
||||
- Kick requests are now spaced out, and a rate limit pauses every Kick request instead of retrying at once ([nobilis `f592a22`](https://github.com/Moho-Chat/nobilis/commit/f592a22), [nobilis `c99d52b`](https://github.com/Moho-Chat/nobilis/commit/c99d52b), [nobilis `2732d08`](https://github.com/Moho-Chat/nobilis/commit/2732d08))
|
||||
- Fixed a changed Sneedchat avatar never being fetched again ([nobilis `04cb759`](https://github.com/Moho-Chat/nobilis/commit/04cb759))
|
||||
- Every media cache now expires old files, with a way to fetch removed pictures again ([nobilis `25a0912`](https://github.com/Moho-Chat/nobilis/commit/25a0912), [moho `9e67ca6`](https://github.com/Moho-Chat/moho/commit/9e67ca6))
|
||||
- Fixed Kick emotes removed by the cache sweep not drawing again ([moho `70cd8d7`](https://github.com/Moho-Chat/moho/commit/70cd8d7))
|
||||
- Fixed Matrix reactions being counted more than once ([nobilis `8ea9a70`](https://github.com/Moho-Chat/nobilis/commit/8ea9a70))
|
||||
- Fixed the scrollback being written to disk once per message ([nobilis `bd33f88`](https://github.com/Moho-Chat/nobilis/commit/bd33f88))
|
||||
- Fixed the migration deleting the client's own config directory ([nobilis `9e885a4`](https://github.com/Moho-Chat/nobilis/commit/9e885a4), [nobilis `207de58`](https://github.com/Moho-Chat/nobilis/commit/207de58))
|
||||
- Fixed a removed account leaving its data behind ([nobilis `2dd755e`](https://github.com/Moho-Chat/nobilis/commit/2dd755e))
|
||||
- Fixed new members of an encrypted Matrix room not receiving its keys ([nobilis `b894dea`](https://github.com/Moho-Chat/nobilis/commit/b894dea))
|
||||
- Fixed two ways a Matrix sign-in could leave an account permanently unusable ([nobilis `ded9cc5`](https://github.com/Moho-Chat/nobilis/commit/ded9cc5))
|
||||
- Fixed sliding sync failing against Synapse ([nobilis `e2bb988`](https://github.com/Moho-Chat/nobilis/commit/e2bb988))
|
||||
- Fixed Discord media links expiring on screen; they now refresh themselves ([moho `dc15c4b`](https://github.com/Moho-Chat/moho/commit/dc15c4b), [nobilis `f5a455b`](https://github.com/Moho-Chat/nobilis/commit/f5a455b))
|
||||
- Repaired Discord messages stored with attachments in their text ([nobilis `15007d6`](https://github.com/Moho-Chat/nobilis/commit/15007d6))
|
||||
- Fixed Discord streams opening a second connection ([nobilis `7b64414`](https://github.com/Moho-Chat/nobilis/commit/7b64414))
|
||||
- Fixed lost video packets, phone streams and stream sound in Discord calls ([nobilis `92dffca`](https://github.com/Moho-Chat/nobilis/commit/92dffca))
|
||||
- Fixed the Discord stream heartbeat using the wrong protocol version ([nobilis `64dfa08`](https://github.com/Moho-Chat/nobilis/commit/64dfa08))
|
||||
- Fixed a running upload being reported as failed ([moho `ee60271`](https://github.com/Moho-Chat/moho/commit/ee60271))
|
||||
- Fixed thumbnails opening as the thumbnail instead of the picture ([moho `6af6ac3`](https://github.com/Moho-Chat/moho/commit/6af6ac3))
|
||||
- Fixed avif links not drawing as pictures ([moho `1511c4b`](https://github.com/Moho-Chat/moho/commit/1511c4b), [nobilis `4e3c65d`](https://github.com/Moho-Chat/nobilis/commit/4e3c65d))
|
||||
- Fixed links in brackets not being recognised ([nobilis `dd450eb`](https://github.com/Moho-Chat/nobilis/commit/dd450eb))
|
||||
- Fixed a full-size photo being downloaded to draw a thumbnail ([nobilis `873a908`](https://github.com/Moho-Chat/nobilis/commit/873a908))
|
||||
- Fixed a crash in the room preview panel ([moho `04a6b7d`](https://github.com/Moho-Chat/moho/commit/04a6b7d))
|
||||
- Fixed call renegotiation not waiting for its answer ([moho `db0f651`](https://github.com/Moho-Chat/moho/commit/db0f651))
|
||||
- Fixed a screen share on Wayland asking twice ([moho `2c146d9`](https://github.com/Moho-Chat/moho/commit/2c146d9))
|
||||
- Discord no longer attempts what it puts behind a captcha ([nobilis `8da2e7a`](https://github.com/Moho-Chat/nobilis/commit/8da2e7a))
|
||||
- The renderer now runs sandboxed ([moho `e791fcf`](https://github.com/Moho-Chat/moho/commit/e791fcf))
|
||||
- The installer warns when Windows Smart App Control will block moho ([moho `5a8d7f7`](https://github.com/Moho-Chat/moho/commit/5a8d7f7))
|
||||
- The uninstaller now offers to remove saved sign-ins and history, and stops the daemon first ([moho `76eabac`](https://github.com/Moho-Chat/moho/commit/76eabac))
|
||||
- Linux builds now run on Ubuntu 22.04 and later ([moho `4c6e24b`](https://github.com/Moho-Chat/moho/commit/4c6e24b))
|
||||
- Updated Rust crate h2 from 0.4.15 to 0.4.19 ([nobilis `2f5918e`](https://github.com/Moho-Chat/nobilis/commit/2f5918e))
|
||||
- Added Rust crate opus2 0.4.0 ([nobilis `f9b5be5`](https://github.com/Moho-Chat/nobilis/commit/f9b5be5))
|
||||
- Added Rust crate aes-gcm 0.10.3 ([nobilis `9d50723`](https://github.com/Moho-Chat/nobilis/commit/9d50723))
|
||||
- Added Rust crate chacha20poly1305 0.10.1 ([nobilis `9d50723`](https://github.com/Moho-Chat/nobilis/commit/9d50723))
|
||||
- Added Rust crate davey 0.1.4 ([nobilis `8ae2148`](https://github.com/Moho-Chat/nobilis/commit/8ae2148))
|
||||
- Removed Rust crate async-trait 0.1.92 ([nobilis `cbb044d`](https://github.com/Moho-Chat/nobilis/commit/cbb044d))
|
||||
- Removed Rust crate songbird 0.6.0 ([nobilis `cbb044d`](https://github.com/Moho-Chat/nobilis/commit/cbb044d))
|
||||
- Added Rust crate socket2 0.6.5 ([nobilis `92dffca`](https://github.com/Moho-Chat/nobilis/commit/92dffca))
|
||||
- Added Rust crate sonora 0.2.0 ([nobilis `72ef1b5`](https://github.com/Moho-Chat/nobilis/commit/72ef1b5))
|
||||
- Updated Rust crate arti-client from 0.44.0 to 0.47.0 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate matrix-sdk-common from 0.18.0 to 0.19.1 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate matrix-sdk-crypto from 0.18.0 to 0.19.1 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate matrix-sdk-sqlite from 0.18.0 to 0.19.1 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate ruma-client-api from 0.24.0 to 0.25.0 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate ruma-common from 0.19.0 to 0.20.0 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate ruma-events from 0.34.0 to 0.35.0 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate rusqlite from 0.37.0 to 0.40.2 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate tor-rtcompat from 0.44.0 to 0.47.0 ([nobilis `c34b751`](https://github.com/Moho-Chat/nobilis/commit/c34b751))
|
||||
- Updated Rust crate rustls from 0.23.43 to 0.23.45 ([nobilis `2f5918e`](https://github.com/Moho-Chat/nobilis/commit/2f5918e))
|
||||
- Updated npm package electron-builder from 25.1.8 to 26.15.3 ([moho `e791fcf`](https://github.com/Moho-Chat/moho/commit/e791fcf))
|
||||
- Updated npm package electron from 33.4.11 to 44.4.4 ([moho `932a07a`](https://github.com/Moho-Chat/moho/commit/932a07a))
|
||||
- Added npm package lottie-web 5.13.0 ([moho `f67c753`](https://github.com/Moho-Chat/moho/commit/f67c753))
|
||||
- Added npm package happy-dom 15.11.7 ([moho `53a6f92`](https://github.com/Moho-Chat/moho/commit/53a6f92))
|
||||
- Added npm package vitest 2.1.9 ([moho `53a6f92`](https://github.com/Moho-Chat/moho/commit/53a6f92))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Maintainer: Salastil <[email protected]>
|
||||
pkgname=moho-git
|
||||
pkgver=r1.268751e
|
||||
pkgrel=1
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
# moho
|
||||
|
||||
**Every conversation you're in, in one window.**
|
||||
<img width="1545" height="912" alt="showcase-screenshot" src="https://github.com/user-attachments/assets/b9dabddb-bb3c-4a71-b872-9ad167ecd400" />
|
||||
|
||||
moho is a desktop chat client that speaks IRC, Discord, Matrix, Kick and Sneedchat at the
|
||||
same time. One buffer list down the side, one log in the middle, one set of habits — whether
|
||||
the room is a twenty-year-old IRC channel, a Discord server, an encrypted Matrix room, or a
|
||||
stream's chat going past at forty lines a minute.
|
||||
|
||||
It isn't five web apps in a trenchcoat. Underneath sits a small Rust daemon that holds the
|
||||
connections; the window is just what draws them. Close it and you stay connected. Open it
|
||||
again and everything is where you left it.
|
||||
moho is a desktop chat client that speaks IRC, Discord, Matrix, Kick and many more protocols at the
|
||||
same time. Whether the room is a twenty-year-old IRC channel, a Discord server, an encrypted Matrix room, or a
|
||||
stream's chat going past at forty lines a minute you're able to be part of the conversation.
|
||||
|
||||
## What it does
|
||||
|
||||
- **Five networks, one place.** IRC, Discord, Matrix, Kick and Sneedchat, side by side, with
|
||||
- **Many networks, one place.** IRC, Discord, Matrix, Kick and many more protocols soon, side by side, with
|
||||
the same reply, edit, react, search and tag-someone gestures on each — as far as each
|
||||
service actually supports them.
|
||||
- **A log, not a feed.** Compact IRC-style scrollback with nick colours, markdown and BBCode,
|
||||
spoilers, code blocks, inline images and video, and custom emoji. There's a roomier mode
|
||||
with avatars if you prefer one.
|
||||
- **It stays connected.** The daemon keeps IRC registered, Matrix syncing and Tor circuits
|
||||
open while the window is shut. Quitting says a proper goodbye rather than dropping the
|
||||
socket, so you don't come back to a ghosted nick.
|
||||
- **Calls.** Voice, camera and screen sharing on Discord and on Matrix, including the group
|
||||
calls Element holds on a media server. On Discord, Go Live streams both ways with sound,
|
||||
calls Element holds on a media server. On Discord, Go Live, streams both ways with sound,
|
||||
echo cancellation, volume per person, the soundboard and stage channels. A call can sit in
|
||||
the corner or pop out into a window of its own.
|
||||
- **Matrix, properly encrypted.** End-to-end encryption with device verification by emoji or
|
||||
QR, cross-signing, key backup, and encrypted attachments.
|
||||
- **Sneedchat, and Tor where you want it.** The site's proof-of-work gate and its login
|
||||
captcha both handled, and every room you've joined connected at once. Any account on any of
|
||||
the five can go through an embedded Tor client or a SOCKS5 proxy instead of the open internet.
|
||||
- **Kick streams in the window.** Watch the stream beside its chat, in the corner or in a
|
||||
window of its own, with VODs, clips, 7TV and BTTV emotes, polls and predictions.
|
||||
- **No captchas on Discord, on purpose.** Discord puts joining a server, adding a friend and
|
||||
making a server behind a captcha when they come from anything but its own client, and
|
||||
answering one gets an account flagged for spam. moho opens discord.com for those three
|
||||
instead of putting your account at risk.
|
||||
window of its own, polls, predictions and other interactive methods are functional and at your fingertips.
|
||||
- **Yours.** GPL-3.0, no telemetry, no account with us — your history is a SQLite file on
|
||||
your own disk.
|
||||
|
||||
@@ -47,10 +30,6 @@ Prebuilt Linux and Windows downloads are on the
|
||||
changed is in [CHANGELOG.md](CHANGELOG.md). moho tells you in its mentions inbox when a newer
|
||||
version is out.
|
||||
|
||||
Versions are MAJOR.FEATURE.FIX: a major release, then protocols added and major features,
|
||||
then bug and security fixes - 1.1.0 would add a protocol, 1.0.5 is the fifth fix release
|
||||
of 1.0.
|
||||
|
||||
The AppImage needs `libfuse2` on recent distributions; if it refuses to start, either install
|
||||
that or run it with `APPIMAGE_EXTRACT_AND_RUN=1`.
|
||||
|
||||
@@ -85,13 +64,18 @@ npm install-scripts approve electron esbuild
|
||||
## Test it
|
||||
|
||||
```bash
|
||||
npm test # the window's unit tests (Vitest)
|
||||
npm run test:ui # layout checks against the real app - popups must stay inside the window
|
||||
npm test # the window's unit tests (Vitest)
|
||||
npm run test:ui # layout checks against the real app - popups must stay inside the window
|
||||
npm run test:tray # the tray: its icon, tooltip, menu, statuses and restart
|
||||
npm run test:gif # GIFs drawn as pictures, and the GIF picker
|
||||
npm run test:scroll # scrolling and where a conversation opens
|
||||
npm run test:kick # where Kick event cards sit in each layout
|
||||
cd nobilis && cargo test --release
|
||||
```
|
||||
|
||||
`test:ui` builds the window, runs it against a scratch home and a fake IRC server, and needs the
|
||||
daemon built first. On a machine with no display, run it under `xvfb-run`.
|
||||
The `test:` scripts other than `test` build the window, run it against a scratch home and a
|
||||
fake IRC server, and need the daemon built first. On a machine with no display, run them under
|
||||
`xvfb-run`. `npm run shots` photographs the interface scene by scene, for looking at changes.
|
||||
|
||||
## Run it
|
||||
|
||||
@@ -107,9 +91,14 @@ npm run deploy
|
||||
```
|
||||
|
||||
Other ways out of the box: `npm run pack:dir` for a self-contained `dist/linux-unpacked/`
|
||||
directory to run in place, `npm run pack:win` for the Windows installer (cross-built with
|
||||
mingw-w64; packaging it also needs wine with 32-bit support), and `makepkg -si` against the
|
||||
bundled `PKGBUILD` to install it as an Arch package under `/opt/moho`.
|
||||
directory to run in place, `npm run pack:deb` for a Debian package, `npm run pack:win` for the
|
||||
Windows installer (cross-built with mingw-w64; packaging it also needs wine with 32-bit
|
||||
support), and `makepkg -si` against the bundled `PKGBUILD` to install it as an Arch package
|
||||
under `/opt/moho`.
|
||||
|
||||
Packaging leaves a lot behind - an AppImage per build, and Rust build output that Cargo never
|
||||
cleans up. `npm run deploy` first runs `npm run tidy`, which keeps the newest two AppImages and
|
||||
empties `nobilis/target/` once it passes 8 GB.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
Submodule nobilis updated: d232be9c5e...c26f968e19
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "moho",
|
||||
"version": "1.0.0-rc.5",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "moho",
|
||||
"version": "1.0.0-rc.5",
|
||||
"version": "1.0.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
|
||||
+4
-3
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "moho",
|
||||
"version": "1.0.0-rc.5",
|
||||
"version": "1.0.0",
|
||||
"description": "Unified desktop chat client for IRC, Discord, Sneedchat and Matrix",
|
||||
"author": "Salastil",
|
||||
"author": "Salastil <[email protected]>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"homepage": "https://github.com/Moho-Chat/moho",
|
||||
"repository": {
|
||||
@@ -24,6 +24,7 @@
|
||||
"test:tray": "electron-vite build && node scripts/tray-check.mjs",
|
||||
"test:gif": "electron-vite build && node scripts/gif-check.mjs",
|
||||
"test:scroll": "electron-vite build && node scripts/scroll-check.mjs",
|
||||
"test:kick": "electron-vite build && node scripts/kick-check.mjs",
|
||||
"shots": "electron-vite build && node scripts/ui-shots.mjs",
|
||||
"pack": "npm run daemon && npm run build && MOHO_BUILD=$(git rev-parse --short HEAD)$(git diff --quiet HEAD || echo -dirty) electron-builder --linux AppImage",
|
||||
"pack:deb": "npm run daemon && npm run build && MOHO_BUILD=$(git rev-parse --short HEAD)$(git diff --quiet HEAD || echo -dirty) electron-builder --linux deb",
|
||||
@@ -78,7 +79,7 @@
|
||||
"AppImage",
|
||||
"deb"
|
||||
],
|
||||
"maintainer": "Salastil <salastil@users.noreply.github.com>",
|
||||
"maintainer": "Salastil <salastil@cock.li>",
|
||||
"category": "Network",
|
||||
"synopsis": "Unified desktop chat client",
|
||||
"icon": "resources/icons/moho.png",
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Checks where a Kick event card sits in the log, in each layout.
|
||||
*
|
||||
* A card is what a person did, so it takes the column a person's picture would
|
||||
* (and not the mark a join gets), what is written in it starts where a name
|
||||
* does, and it has the air above it that a message and an embed have. These
|
||||
* are measured against a real chat line beside it rather than against numbers,
|
||||
* so they hold when the layout's own numbers change.
|
||||
*
|
||||
* npm run test:kick (builds first; needs the daemon built too)
|
||||
*/
|
||||
import { fakeIrc, launchApp, openIrcChannel, sleep, until, click } from './lib/harness.mjs'
|
||||
|
||||
let failures = 0
|
||||
const check = (ok, what, detail = '') => {
|
||||
console.log(`${ok ? 'ok ' : 'FAIL'} ${what}${ok ? '' : detail ? ` - ${detail}` : ''}`)
|
||||
if (!ok) failures++
|
||||
}
|
||||
setTimeout(() => {
|
||||
console.log('FAIL timed out')
|
||||
process.exit(1)
|
||||
}, 120000).unref()
|
||||
|
||||
const irc = await fakeIrc({ people: ['bob'], onJoin: (channel, _nick, send) => send(`:bob!b@h PRIVMSG ${channel} :hello there`) })
|
||||
const { page, stop } = await launchApp({ env: { MOHO_UI_SHOTS: '1' } })
|
||||
|
||||
const stage = `(() => {
|
||||
const shots = window.__mohoShots
|
||||
const s = shots.state()
|
||||
const id = s.activeBufferId
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const row = (n, kind, body, embed, from) => ({ id: 'k' + n, bufferId: id, from, senderId: from, body, ts: now - 600 + n * 70, isAction: false, isHighlight: false, kind, isOwn: false, embeds: embed ? [embed] : [] })
|
||||
shots.patch({ messagesByBuffer: { ...s.messagesByBuffer, [id]: [
|
||||
row(1, 'chat', 'that was a clean run', null, 'bob'),
|
||||
row(2, 'chat', 'and then it got worse', null, 'carol'),
|
||||
row(3, 'kicks', 'Currington sent 1 Kick', { kind: 'kicks', author: 'Currington', title: '1', color: 0x53fc18 }, 'kicker'),
|
||||
row(4, 'sub', 'ItsDez gifted 2 subscriptions', { kind: 'gift-sub', author: 'ItsDez', title: '2', description: 'a\\nb' }, 'kicker'),
|
||||
row(5, 'reward', 'poo_nana redeemed MUTE', { kind: 'redeem', author: 'poo_nana', title: 'MUTE (user) 30 MINUTES', description: 'O_C_T_O', color: 0xff4b1f }, 'kicker'),
|
||||
row(6, 'raid', 'x raided with 25 viewers', { kind: 'raid', author: 'kreationX', title: '25' }, 'kicker'),
|
||||
row(8, 'sub', 'bigkhernandez subscribed - 21 months', { kind: 'subscribe', author: 'bigkhernandez', title: '21' }, 'kicker'),
|
||||
row(9, 'moderation', 'troll was timed out by mod', { kind: 'moderation', author: 'troll', title: 'was timed out', description: 'mod' }, 'kicker'),
|
||||
row(10, 'stream', 'went live', { kind: 'stream', author: 'streamer', title: 'went live', icon: 'live' }, 'kicker'),
|
||||
row(11, 'chat', 'thanks everybody', null, 'dave')
|
||||
] } })
|
||||
})()`
|
||||
|
||||
const measure = `(() => {
|
||||
const x = (el) => (el ? Math.round(el.getBoundingClientRect().left) : null)
|
||||
const rows = [...document.querySelectorAll('.message-row')]
|
||||
const chat = rows.find((r) => !r.classList.contains('has-kick-event') && r.querySelector('.message-from'))
|
||||
const cards = rows.filter((r) => r.classList.contains('has-kick-event'))
|
||||
const margin = (r) => parseFloat(getComputedStyle(r).marginTop)
|
||||
return {
|
||||
marks: cards.filter((r) => r.querySelector('.system-mark')).length,
|
||||
cards: cards.length,
|
||||
chatEdge: x(chat.querySelector('.message-avatar img, .message-avatar .avatar-fallback') || chat.querySelector('.message-content')),
|
||||
chatName: x(chat.querySelector('.message-from')),
|
||||
cardEdge: cards.map((r) => x(r.querySelector('.kick-event'))),
|
||||
cardName: cards.map((r) => x(r.querySelector('.kick-event-name'))),
|
||||
gapAbove: cards.map(margin),
|
||||
chatGap: margin(chat) || 0,
|
||||
wide: cards.map((r) => Math.round(r.querySelector('.kick-event').getBoundingClientRect().width))
|
||||
}
|
||||
})()`
|
||||
|
||||
try {
|
||||
await until(page, 'window.__mohoShots', 'the harness hook')
|
||||
await openIrcChannel(page, { port: irc.address().port, nick: 'checker', channels: ['#lobby'], waitFor: 'hello there' })
|
||||
for (const mode of ['comfy', 'bubbles', 'classic']) {
|
||||
await page.evaluate(`window.moho.prefs.set('display.messageMode', ${JSON.stringify(mode)})`)
|
||||
await page.call('Page.reload')
|
||||
await until(page, 'window.__mohoShots && document.querySelector("nav[aria-label=Servers] [aria-label]")', 'the window after a reload')
|
||||
await sleep(500)
|
||||
await page.evaluate(click('nav[aria-label=Servers] [aria-label*="localhost"]'))
|
||||
await sleep(300)
|
||||
await page.evaluate(click('.buffer-row', '#lobby'))
|
||||
await sleep(500)
|
||||
await page.evaluate(stage)
|
||||
await sleep(700)
|
||||
const m = await page.evaluate(measure)
|
||||
console.log(`${mode}: ${JSON.stringify(m)}`)
|
||||
check(m.cards === 7, `${mode}: every kind of card is drawn`, `${m.cards}`)
|
||||
check(m.marks === 0, `${mode}: no join mark beside a card`, `${m.marks}`)
|
||||
if (mode !== 'classic') {
|
||||
// The card starts at the picture's edge.
|
||||
check(m.cardEdge.every((e) => Math.abs(e - m.chatEdge) <= 2), `${mode}: a card starts where a person's picture does`, `${m.cardEdge} vs ${m.chatEdge}`)
|
||||
// And what is written in it starts where a name does.
|
||||
check(m.cardName.every((n) => Math.abs(n - m.chatName) <= 2), `${mode}: the name in a card is where a name is in the log`, `${m.cardName} vs ${m.chatName}`)
|
||||
}
|
||||
check(m.gapAbove.every((g) => g >= (mode === 'comfy' ? m.chatGap : 4) - 0.5), `${mode}: a card has air above it`, `${m.gapAbove} vs ${m.chatGap}`)
|
||||
check(m.wide.every((w) => w > 200), `${mode}: and is not squeezed or centred away`, `${m.wide}`)
|
||||
}
|
||||
} finally {
|
||||
await stop()
|
||||
irc.close()
|
||||
}
|
||||
console.log(failures ? `${failures} FAILED` : 'all passed')
|
||||
process.exit(failures ? 1 : 0)
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mirrors a published GitHub release of moho to Gitea, and makes nobilis's
|
||||
# release of the same version on both hosts. Run here, by hand, once the
|
||||
# release workflow has published the tag on GitHub - Gitea releases are never
|
||||
# made by Actions.
|
||||
#
|
||||
# scripts/mirror-release.sh v1.0.0
|
||||
#
|
||||
# moho on Gitea: the same title, notes, pre-release flag and files as GitHub.
|
||||
# nobilis, on GitHub and Gitea: tagged at the commit moho's tag points to,
|
||||
# named after the same version, its notes the version's major features and
|
||||
# the nobilis lines of the changelog, with the two daemon binaries the
|
||||
# release's run built.
|
||||
#
|
||||
# Safe to run again: a release that exists is updated rather than repeated.
|
||||
set -euo pipefail
|
||||
|
||||
TAG="${1:?usage: scripts/mirror-release.sh vX.Y.Z}"
|
||||
VERSION="${TAG#v}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
# Through Cloudflare by default, which refuses a request body over 100 MB. The
|
||||
# files that are bigger go to Gitea directly across the network, which is what
|
||||
# MIRROR_GITEA is for - the same instance, reached by its own address:
|
||||
#
|
||||
# MIRROR_GITEA=http://192.168.1.212:8991/api/v1 MIRROR_MAX_BYTES=1000000000 \
|
||||
# scripts/mirror-release.sh v1.0.0
|
||||
GITEA="${MIRROR_GITEA:-https://git.salastil.com/api/v1}"
|
||||
TOKEN="$(cat "$HOME/.config/gitea/token")"
|
||||
# What Cloudflare in front of Gitea will take in one request.
|
||||
MAX_UPLOAD="${MIRROR_MAX_BYTES:-100000000}"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
gitea() { # method path [curl args...]
|
||||
local method="$1" path="$2"; shift 2
|
||||
curl -fsS -X "$method" -H "Authorization: token $TOKEN" "$GITEA$path" "$@"
|
||||
}
|
||||
|
||||
# Creates or updates a Gitea release and makes its files exactly those given.
|
||||
gitea_release() { # repo title notes-file prerelease file...
|
||||
local repo="$1" title="$2" notes="$3" pre="$4"; shift 4
|
||||
local body id
|
||||
body="$(python3 -c 'import json,sys; print(json.dumps({"tag_name": sys.argv[1], "name": sys.argv[2], "body": open(sys.argv[3]).read(), "prerelease": sys.argv[4] == "true"}))' "$TAG" "$title" "$notes" "$pre")"
|
||||
if id="$(gitea GET "/repos/$repo/releases/tags/$TAG" 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' 2>/dev/null)"; then
|
||||
gitea PATCH "/repos/$repo/releases/$id" -H 'Content-Type: application/json' -d "$body" >/dev/null
|
||||
for asset in $(gitea GET "/repos/$repo/releases/$id/assets" | python3 -c 'import json,sys; print(" ".join(str(a["id"]) for a in json.load(sys.stdin)))'); do
|
||||
gitea DELETE "/repos/$repo/releases/$id/assets/$asset" >/dev/null
|
||||
done
|
||||
else
|
||||
id="$(gitea POST "/repos/$repo/releases" -H 'Content-Type: application/json' -d "$body" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
|
||||
fi
|
||||
for f in "$@"; do
|
||||
# Gitea is behind Cloudflare, which refuses a request body over 100 MB (a
|
||||
# 413, or a 502 when the connection is cut mid-upload). A file over that
|
||||
# stays on GitHub only, said plainly rather than failing the whole mirror
|
||||
# and leaving everything after it - nobilis's releases - undone.
|
||||
if [ "$(stat -c %s "$f")" -gt "$MAX_UPLOAD" ]; then
|
||||
echo " SKIPPED $(basename "$f") ($(( $(stat -c %s "$f") / 1000000 )) MB) - over Cloudflare's ${MAX_UPLOAD} byte limit; it is on GitHub only" >&2
|
||||
continue
|
||||
fi
|
||||
gitea POST "/repos/$repo/releases/$id/assets?name=$(basename "$f")" -F "attachment=@$f" >/dev/null
|
||||
echo " uploaded $(basename "$f") -> Gitea $repo"
|
||||
done
|
||||
}
|
||||
|
||||
# --- moho -------------------------------------------------------------------
|
||||
echo "moho $TAG: GitHub -> Gitea"
|
||||
read -r PRE TITLE < <(gh release view "$TAG" --repo Moho-Chat/moho --json isPrerelease,name -q '"\(.isPrerelease) \(.name)"')
|
||||
gh release view "$TAG" --repo Moho-Chat/moho --json body -q .body > "$WORK/moho-notes.md"
|
||||
mkdir -p "$WORK/moho"
|
||||
gh release download "$TAG" --repo Moho-Chat/moho --dir "$WORK/moho"
|
||||
git -C "$ROOT" push -q origin "refs/tags/$TAG" 2>/dev/null || true
|
||||
gitea_release Salastil/moho "$TITLE" "$WORK/moho-notes.md" "$PRE" "$WORK"/moho/*
|
||||
|
||||
# --- nobilis ------------------------------------------------------------------
|
||||
NOBILIS_SHA="$(git -C "$ROOT" rev-parse "$TAG:nobilis")"
|
||||
echo "nobilis $TAG at ${NOBILIS_SHA:0:7}"
|
||||
if ! git -C "$ROOT/nobilis" rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
||||
git -C "$ROOT/nobilis" tag -a "$TAG" "$NOBILIS_SHA" -m "nobilis $VERSION"
|
||||
fi
|
||||
git -C "$ROOT/nobilis" push -q github "refs/tags/$TAG"
|
||||
git -C "$ROOT/nobilis" push -q origin "refs/tags/$TAG"
|
||||
|
||||
# Its notes: the version's major features, and the nobilis lines only.
|
||||
SECTION="$VERSION"
|
||||
grep -q "^## \[$VERSION\]" "$ROOT/CHANGELOG.md" || SECTION="${VERSION%%-*}"
|
||||
awk -v v="$SECTION" '
|
||||
index($0, "## [" v "]") == 1 { on = 1; next }
|
||||
on && /^## \[/ { exit }
|
||||
!on { next }
|
||||
/^### / { fixes = ($0 ~ /^### Fixes/); print; next }
|
||||
/^- / { if (!fixes || /\[nobilis /) print; next }
|
||||
{ print }
|
||||
' "$ROOT/CHANGELOG.md" > "$WORK/nobilis-notes.md"
|
||||
printf '\nnobilis is the daemon inside moho %s; these are its own builds, for running it on its own.\n' "$VERSION" >> "$WORK/nobilis-notes.md"
|
||||
|
||||
# The daemon binaries, from the run that built this release.
|
||||
RUN="$(gh run list --repo Moho-Chat/moho --workflow release.yml --branch "$TAG" --status success --limit 1 --json databaseId -q '.[0].databaseId')"
|
||||
[ -n "$RUN" ] || { echo "no successful release run for $TAG" >&2; exit 1; }
|
||||
mkdir -p "$WORK/nobilis"
|
||||
gh run download "$RUN" --repo Moho-Chat/moho -n daemon-linux -D "$WORK/d-linux"
|
||||
gh run download "$RUN" --repo Moho-Chat/moho -n daemon-windows -D "$WORK/d-win"
|
||||
install -m 755 "$WORK/d-linux/nobilis" "$WORK/nobilis/nobilis-$VERSION-linux-x86_64"
|
||||
install -m 644 "$WORK/d-win/nobilis.exe" "$WORK/nobilis/nobilis-$VERSION-windows-x86_64.exe"
|
||||
|
||||
NOBILIS_FLAGS=(--latest)
|
||||
[ "$PRE" = "true" ] && NOBILIS_FLAGS=(--prerelease --latest=false)
|
||||
if gh release view "$TAG" --repo Moho-Chat/nobilis >/dev/null 2>&1; then
|
||||
gh release edit "$TAG" --repo Moho-Chat/nobilis --notes-file "$WORK/nobilis-notes.md"
|
||||
gh release upload "$TAG" --repo Moho-Chat/nobilis "$WORK"/nobilis/* --clobber
|
||||
else
|
||||
gh release create "$TAG" --repo Moho-Chat/nobilis --verify-tag --title "nobilis $VERSION" \
|
||||
--notes-file "$WORK/nobilis-notes.md" "${NOBILIS_FLAGS[@]}" "$WORK"/nobilis/*
|
||||
fi
|
||||
echo " GitHub Moho-Chat/nobilis $TAG"
|
||||
gitea_release Salastil/nobilis "nobilis $VERSION" "$WORK/nobilis-notes.md" "$PRE" "$WORK"/nobilis/*
|
||||
echo "done: moho and nobilis $TAG on GitHub and Gitea"
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# Makes nobilis's release of the same version as one of moho's.
|
||||
#
|
||||
# Run by the maintainer once the release workflow has published moho's tag:
|
||||
# the daemon is its own repository with its own releases, and the job that
|
||||
# builds it holds a token that can only release moho.
|
||||
#
|
||||
# scripts/release-nobilis.sh v1.0.0
|
||||
# scripts/release-nobilis.sh v1.0.0 --keep DIR leave the notes and the two
|
||||
# daemon binaries in DIR
|
||||
#
|
||||
# nobilis is tagged at the commit moho's tag points to, named after the same
|
||||
# version, its notes the version's major features and the nobilis lines of the
|
||||
# changelog, with the two daemon binaries the release's run built. Safe to run
|
||||
# again: a release that exists is updated rather than repeated.
|
||||
set -euo pipefail
|
||||
|
||||
TAG="${1:?usage: scripts/release-nobilis.sh vX.Y.Z [--keep DIR]}"
|
||||
KEEP=""
|
||||
[ "${2:-}" = "--keep" ] && KEEP="${3:?--keep needs a directory}"
|
||||
VERSION="${TAG#v}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
read -r PRE < <(gh release view "$TAG" --repo Moho-Chat/moho --json isPrerelease -q .isPrerelease)
|
||||
|
||||
NOBILIS_SHA="$(git -C "$ROOT" rev-parse "$TAG:nobilis")"
|
||||
echo "nobilis $TAG at ${NOBILIS_SHA:0:7}"
|
||||
if ! git -C "$ROOT/nobilis" rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
||||
git -C "$ROOT/nobilis" tag -a "$TAG" "$NOBILIS_SHA" -m "nobilis $VERSION"
|
||||
fi
|
||||
git -C "$ROOT/nobilis" push -q github "refs/tags/$TAG"
|
||||
|
||||
# Its notes: the version's major features, and the nobilis lines only.
|
||||
SECTION="$VERSION"
|
||||
grep -q "^## \[$VERSION\]" "$ROOT/CHANGELOG.md" || SECTION="${VERSION%%-*}"
|
||||
awk -v v="$SECTION" '
|
||||
index($0, "## [" v "]") == 1 { on = 1; next }
|
||||
on && /^## \[/ { exit }
|
||||
!on { next }
|
||||
/^### / { fixes = ($0 ~ /^### Fixes/); print; next }
|
||||
/^- / { if (!fixes || /\[nobilis /) print; next }
|
||||
{ print }
|
||||
' "$ROOT/CHANGELOG.md" > "$WORK/nobilis-notes.md"
|
||||
printf '\nnobilis is the daemon inside moho %s; these are its own builds, for running it on its own.\n' "$VERSION" >> "$WORK/nobilis-notes.md"
|
||||
|
||||
# The daemon binaries, from the run that built this release.
|
||||
RUN="$(gh run list --repo Moho-Chat/moho --workflow release.yml --branch "$TAG" --status success --limit 1 --json databaseId -q '.[0].databaseId')"
|
||||
[ -n "$RUN" ] || { echo "no successful release run for $TAG" >&2; exit 1; }
|
||||
mkdir -p "$WORK/nobilis"
|
||||
gh run download "$RUN" --repo Moho-Chat/moho -n daemon-linux -D "$WORK/d-linux"
|
||||
gh run download "$RUN" --repo Moho-Chat/moho -n daemon-windows -D "$WORK/d-win"
|
||||
install -m 755 "$WORK/d-linux/nobilis" "$WORK/nobilis/nobilis-$VERSION-linux-x86_64"
|
||||
install -m 644 "$WORK/d-win/nobilis.exe" "$WORK/nobilis/nobilis-$VERSION-windows-x86_64.exe"
|
||||
|
||||
NOBILIS_FLAGS=(--latest)
|
||||
[ "$PRE" = "true" ] && NOBILIS_FLAGS=(--prerelease --latest=false)
|
||||
if gh release view "$TAG" --repo Moho-Chat/nobilis >/dev/null 2>&1; then
|
||||
gh release edit "$TAG" --repo Moho-Chat/nobilis --notes-file "$WORK/nobilis-notes.md"
|
||||
gh release upload "$TAG" --repo Moho-Chat/nobilis "$WORK"/nobilis/* --clobber
|
||||
else
|
||||
gh release create "$TAG" --repo Moho-Chat/nobilis --verify-tag --title "nobilis $VERSION" \
|
||||
--notes-file "$WORK/nobilis-notes.md" "${NOBILIS_FLAGS[@]}" "$WORK"/nobilis/*
|
||||
fi
|
||||
echo " GitHub Moho-Chat/nobilis $TAG"
|
||||
if [ -n "$KEEP" ]; then
|
||||
mkdir -p "$KEEP"
|
||||
cp "$WORK/nobilis-notes.md" "$KEEP/"
|
||||
cp -r "$WORK/nobilis" "$KEEP/"
|
||||
fi
|
||||
@@ -30,7 +30,7 @@ const irc = await fakeIrc({
|
||||
const { page, stop } = await launchApp({ env: { MOHO_UI_SHOTS: '1' } })
|
||||
try {
|
||||
await until(page, 'window.__mohoShots', 'the harness hook')
|
||||
await openIrcChannel(page, { port: irc.address().port, nick: 'checker', channels: ['#lobby'], waitFor: 'hello there' })
|
||||
await openIrcChannel(page, { port: irc.address().port, nick: 'checker', channels: ['#lobby', '#other'], waitFor: 'hello there' })
|
||||
|
||||
// Three hundred messages, the last two hundred of them unread.
|
||||
await page.evaluate(`(() => {
|
||||
@@ -48,7 +48,19 @@ try {
|
||||
dividerTsByBuffer: { ...s.dividerTsByBuffer, [id]: list[99].ts }
|
||||
})
|
||||
})()`)
|
||||
await until(page, `!!document.querySelector('.new-divider')`, 'the New line')
|
||||
// Two hundred unread is more than a screenful: opened at its end, as
|
||||
// Discord does, with the bar saying how much is above and not the log
|
||||
// dropped hundreds of messages up.
|
||||
await until(page, `!!document.querySelector('.unread-bar')`, 'the unread bar')
|
||||
await sleep(600)
|
||||
const opened = await page.evaluate(`(() => {
|
||||
const el = document.querySelector('.messagelist-scroll')
|
||||
return { fromBottom: el.scrollHeight - el.scrollTop - el.clientHeight, line: !!document.querySelector('.new-divider'), bar: document.querySelector('.unread-bar-text')?.textContent }
|
||||
})()`)
|
||||
check(opened.fromBottom < 8 && !opened.line, 'a long unread run opens at the end of the conversation', JSON.stringify(opened))
|
||||
check(/^200 new messages/.test(opened.bar ?? ''), 'and the bar says how much there is', opened.bar)
|
||||
await page.evaluate(`document.querySelector('.unread-bar-text').click()`)
|
||||
await until(page, `!!document.querySelector('.new-divider')`, 'the New line after pressing the bar')
|
||||
await sleep(600)
|
||||
|
||||
// Reading down past the line, the way a wheel does: the bar comes up.
|
||||
@@ -93,6 +105,60 @@ try {
|
||||
// And the position itself did not fall back towards where the bar appeared.
|
||||
const after = await page.evaluate(`document.querySelector('.messagelist-scroll').scrollTop`)
|
||||
check(after >= before - 4, 'the view did not go back up the log', `${before} -> ${after}`)
|
||||
|
||||
// Leaving, and coming back after more has been said. The line was kept from
|
||||
// the first visit, so the return went up the log to a place nothing was
|
||||
// unread any more.
|
||||
const ids = await page.evaluate(`(() => {
|
||||
const s = window.__mohoShots.state()
|
||||
return { lobby: s.activeBufferId, other: s.buffers.find((b) => b.name.endsWith('#other')).id }
|
||||
})()`)
|
||||
const said = (count) => `(() => {
|
||||
const shots = window.__mohoShots
|
||||
const s = shots.state()
|
||||
const id = ${JSON.stringify(ids.lobby)}
|
||||
const base = Math.max(...s.messagesByBuffer[id].map((m) => m.ts)) + 1
|
||||
const more = Array.from({ length: ${count} }, (_, n) => ({
|
||||
id: 'later' + Date.now() + '-' + n, bufferId: id, from: 'bob', senderId: 'bob', body: 'said while away ' + n, ts: base + n,
|
||||
isAction: false, isHighlight: false, kind: 'chat', isOwn: false
|
||||
}))
|
||||
shots.patch({
|
||||
messagesByBuffer: { ...s.messagesByBuffer, [id]: [...s.messagesByBuffer[id], ...more] },
|
||||
buffers: s.buffers.map((b) => (b.id === id ? { ...b, unread: ${count} } : b))
|
||||
})
|
||||
})()`
|
||||
const away = async (count) => {
|
||||
await page.evaluate(`window.__mohoShots.store.selectBuffer(${JSON.stringify(ids.other)}, false)`)
|
||||
await sleep(500)
|
||||
await page.evaluate(said(count))
|
||||
await page.evaluate(`window.__mohoShots.store.selectBuffer(${JSON.stringify(ids.lobby)}, false)`)
|
||||
await sleep(1200)
|
||||
return page.evaluate(`(() => {
|
||||
const el = document.querySelector('.messagelist-scroll')
|
||||
const line = document.querySelector('.new-divider')
|
||||
const rows = [...el.querySelectorAll('[data-msg-id]')]
|
||||
const after = line ? rows.filter((r) => line.compareDocumentPosition(r) & Node.DOCUMENT_POSITION_FOLLOWING).length : -1
|
||||
return { fromBottom: el.scrollHeight - el.scrollTop - el.clientHeight, line: !!line, after, bar: document.querySelector('.unread-bar-text')?.textContent ?? '' }
|
||||
})()`)
|
||||
}
|
||||
|
||||
// A few unread: opens at the first of them, and the line is this visit's -
|
||||
// thirty rows under it, not the two hundred of the visit before.
|
||||
const few = await away(30)
|
||||
check(few.line && few.after === 30, 'a short unread run opens at its first message, with the line where this visit began', JSON.stringify(few))
|
||||
|
||||
// A great many: opens at the end, with the bar to go back by.
|
||||
const many = await away(100)
|
||||
check(many.fromBottom < 8, 'a long one opens at the end and not hundreds of messages up', JSON.stringify(many))
|
||||
check(/^100 new messages/.test(many.bar), 'with the bar to go back by', many.bar)
|
||||
|
||||
// Nothing unread: the end.
|
||||
await page.evaluate(`window.__mohoShots.store.selectBuffer(${JSON.stringify(ids.other)}, false)`)
|
||||
await sleep(500)
|
||||
await page.evaluate(`window.__mohoShots.store.selectBuffer(${JSON.stringify(ids.lobby)}, false)`)
|
||||
await sleep(1000)
|
||||
const nothing = await page.evaluate(`(() => { const el = document.querySelector('.messagelist-scroll'); return { fromBottom: el.scrollHeight - el.scrollTop - el.clientHeight, line: !!document.querySelector('.new-divider') } })()`)
|
||||
check(nothing.fromBottom < 8 && !nothing.line, 'a conversation left read and returned to is at its end', JSON.stringify(nothing))
|
||||
} finally {
|
||||
await stop()
|
||||
irc.close()
|
||||
|
||||
@@ -352,6 +352,40 @@ const SCENES = [
|
||||
name: 'gif-picker',
|
||||
setup: async (p) => { await setMode(p, 'comfy'); await openChannel(p, '#general'); await openGifPicker(p) }
|
||||
},
|
||||
{
|
||||
// What somebody did on a Kick stream: KICKs sent, subscriptions gifted. Put
|
||||
// on a channel's messages directly - a stream is not here to do it.
|
||||
name: 'kick-events',
|
||||
setup: async (p, mode = 'classic') => {
|
||||
await setMode(p, mode)
|
||||
await openChannel(p, '#general')
|
||||
await p.evaluate(`(() => {
|
||||
const shots = window.__mohoShots
|
||||
const s = shots.state()
|
||||
const id = s.activeBufferId
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const row = (n, kind, body, embed, from = 'bob') => ({ id: 'k' + n, bufferId: id, from, senderId: from, body, ts: now - 60 + n, isAction: false, isHighlight: false, kind, isOwn: false, embeds: embed ? [embed] : [] })
|
||||
const chat = (n, from, body) => row(n, 'chat', body, null, from)
|
||||
shots.patch({ messagesByBuffer: { ...s.messagesByBuffer, [id]: [
|
||||
chat(1, 'bob', 'that was a clean run'),
|
||||
row(2, 'kicks', 'Currington sent 1 Kick', { kind: 'kicks', author: 'Currington', authorColor: '#75FD46', title: '1', provider: 'Hell Yeah', color: 0x53fc18 }),
|
||||
chat(3, 'carol', 'lets gooo'),
|
||||
row(4, 'kicks', 'Maverick sent 500 Kicks: take my money', { kind: 'kicks', author: 'Maverick', authorColor: '#E26EFF', title: '500', provider: 'Full Send', description: 'take my money', color: 0xb45cff }),
|
||||
row(5, 'sub', 'ItsDez gifted 5 subscriptions', { kind: 'gift-sub', author: 'ItsDez', title: '5', description: 'Interstelar\\nRon_OSRS\\nAuzzzii\\nMeowtimeJay\\nstradivarius31' }),
|
||||
row(6, 'sub', 'LurkinLeepa gifted a subscription', { kind: 'gift-sub', author: 'LurkinLeepa', title: '1', description: 'onkhell' }),
|
||||
row(10, 'sub', 'bigkhernandez subscribed - 21 months', { kind: 'subscribe', author: 'bigkhernandez', title: '21' }),
|
||||
row(11, 'moderation', 'troll was timed out by mod', { kind: 'moderation', author: 'troll', title: 'was timed out', description: 'mod' }),
|
||||
row(12, 'stream', 'went live', { kind: 'stream', author: 'streamer', title: 'went live', icon: 'live' }),
|
||||
row(7, 'raid', 'kreationX raided with 25 viewers', { kind: 'raid', author: 'kreationX', title: '25' }),
|
||||
row(8, 'reward', 'onkhell redeemed 5 TONGUE TWISTERS', { kind: 'redeem', author: 'onkhell', title: '5 TONGUE TWISTERS', color: 0xff55b3 }),
|
||||
chat(9, 'bob', 'thanks everybody')
|
||||
] } })
|
||||
})()`)
|
||||
await sleep(500)
|
||||
}
|
||||
},
|
||||
{ name: 'kick-events-comfy', setup: async (p) => { await SCENES.find((x) => x.name === 'kick-events').setup(p, 'comfy') } },
|
||||
{ name: 'kick-events-bubbles', setup: async (p) => { await SCENES.find((x) => x.name === 'kick-events').setup(p, 'bubbles') } },
|
||||
{ name: 'irc-channel-comfy', setup: async (p) => { await setMode(p, 'comfy'); await openChannel(p, '#general') } },
|
||||
{
|
||||
// History from several days, scrolled to its start: where date
|
||||
|
||||
+204
-1
@@ -1572,6 +1572,208 @@ button.video-tile {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* What somebody did on a Kick stream - KICKs sent, subscriptions gifted - as
|
||||
the card Kick draws it: a bar down the edge, who and what at the left, and
|
||||
the thing itself at the right. On this client's own surfaces; the accent is
|
||||
the gift's tier (Kick green for an ordinary one), tinted into the plate so a
|
||||
large gift is bigger at a glance without the card leaving the theme. */
|
||||
.message-row.has-kick-event {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Where the card sits in the log, which is where the picture of the person
|
||||
speaking would: it starts at the picture's edge, in the column a join's mark
|
||||
would have taken, and what is written inside it starts where a name does. So
|
||||
the indent is the picture, the gap after it, and - in a bubble - the
|
||||
bubble's own padding. A message row that is not a person speaking gets no
|
||||
avatar, and a card with none lets the content start at the edge. */
|
||||
.message-row.comfy:not(.bubbles).has-kick-event {
|
||||
--kick-lead: var(--avatar);
|
||||
--kick-indent: calc(var(--avatar) + var(--spacing-m));
|
||||
}
|
||||
|
||||
.message-row.bubbles.system.has-kick-event {
|
||||
--kick-lead: 28px;
|
||||
--kick-indent: calc(28px + var(--spacing-s) + var(--spacing-s));
|
||||
justify-content: flex-start;
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
.message-row.bubbles.system.has-kick-event .message-content {
|
||||
flex: 1 1 auto;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
background: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.message-row.classic.has-kick-event,
|
||||
.message-row:not(.comfy).has-kick-event {
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.message-row.comfy.has-kick-event .message-content {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-row.comfy.has-kick-event .kick-event {
|
||||
position: relative;
|
||||
max-width: min(100%, 560px);
|
||||
/* Less the bar, which is in the card's own edge. */
|
||||
padding-left: calc(var(--kick-indent) - var(--kick-border, 3px));
|
||||
}
|
||||
|
||||
.message-row.comfy.has-kick-event .kick-event.raid,
|
||||
.message-row.comfy.has-kick-event .kick-event.stream {
|
||||
--kick-border: 1px;
|
||||
}
|
||||
|
||||
/* The mark at the left takes the picture's column - centred on where the
|
||||
picture would be - instead of sitting in the text's way. */
|
||||
.message-row.comfy.has-kick-event .kick-event > .kick-event-badge.lead {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: var(--kick-lead);
|
||||
}
|
||||
|
||||
.kick-event {
|
||||
--kick-accent: #53fc18;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-l);
|
||||
min-width: 260px;
|
||||
max-width: min(100%, 520px);
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
border: 1px solid var(--outline-medium);
|
||||
border-left: 3px solid var(--kick-accent);
|
||||
border-radius: var(--corner-radius);
|
||||
background: color-mix(in srgb, var(--kick-accent) 7%, var(--surface-container-high));
|
||||
color: var(--surface-text);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Kick draws a gifted subscription in purple and a raid with no bar at all; a
|
||||
redeemed reward takes the streamer's colour, set inline. */
|
||||
.kick-event.gift {
|
||||
--kick-accent: #a970ff;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.kick-event.redeem,
|
||||
.kick-event.subscribe,
|
||||
.kick-event.moderation {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.kick-event.subscribe {
|
||||
--kick-accent: #a970ff;
|
||||
}
|
||||
|
||||
/* A ban, a timeout, a chat cleared: said in the room's own danger colour. */
|
||||
.kick-event.moderation {
|
||||
--kick-accent: var(--danger);
|
||||
}
|
||||
|
||||
/* The stream going live or ending is a plate like a raid's, and the mark is
|
||||
the part that says which. */
|
||||
.kick-event.stream {
|
||||
justify-content: flex-start;
|
||||
border: 1px solid var(--outline-medium);
|
||||
border-radius: var(--radius-pill);
|
||||
padding-inline: var(--spacing-m);
|
||||
background: var(--surface-container-high);
|
||||
}
|
||||
|
||||
.kick-event.stream.live .kick-event-badge {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.kick-event.stream.offline .kick-event-badge {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.kick-event.raid {
|
||||
justify-content: flex-start;
|
||||
border: 1px solid var(--outline-medium);
|
||||
border-radius: var(--radius-pill);
|
||||
padding-inline: var(--spacing-m);
|
||||
background: var(--surface-container-high);
|
||||
}
|
||||
|
||||
.kick-event.raid .kick-event-badge {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.kick-event-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kick-event-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.kick-event-name {
|
||||
font-weight: var(--weight-bold);
|
||||
}
|
||||
|
||||
.kick-event-verb {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.kick-event-amount {
|
||||
font-size: var(--font-size-large, 1.15em);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.kick-event-message {
|
||||
color: var(--surface-text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.kick-event-recipients {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.kick-event-recipient {
|
||||
padding: 0 var(--spacing-s);
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--surface-container-highest, var(--surface-hover));
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
|
||||
.kick-event-badge {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--kick-accent);
|
||||
}
|
||||
|
||||
.kicks-mark {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--kick-accent);
|
||||
color: #0b0e0a;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.message-row.comfy {
|
||||
align-items: flex-start;
|
||||
padding-top: var(--spacing-xxs);
|
||||
@@ -1590,7 +1792,8 @@ button.video-tile {
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.message-row.comfy:not(.bubbles):not(.grouped):not(.system) {
|
||||
.message-row.comfy:not(.bubbles):not(.grouped):not(.system),
|
||||
.message-row.comfy:not(.bubbles):not(.grouped).has-kick-event {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Icon } from './Icon'
|
||||
import { embedColor } from '../lib/format'
|
||||
import { nickColor } from '../lib/util'
|
||||
import type { Embed } from '../../../shared/wire'
|
||||
|
||||
/** The embed kinds the daemon draws as these cards instead of as a link preview. */
|
||||
export const KICK_EVENT_KINDS = ['kicks', 'gift-sub', 'raid', 'redeem', 'subscribe', 'moderation', 'stream']
|
||||
|
||||
export function isKickEvent(embed: Embed): boolean {
|
||||
return !!embed.kind && KICK_EVENT_KINDS.includes(embed.kind)
|
||||
}
|
||||
|
||||
/** Kick's own mark: a K on a disc. Drawn rather than fetched - it is a few shapes. */
|
||||
function KicksMark({ size = 16 }: { size?: number }): JSX.Element {
|
||||
return (
|
||||
<span className="kicks-mark" style={{ width: size, height: size, fontSize: size * 0.68 }} aria-hidden="true">
|
||||
K
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* What somebody did on a Kick stream, as the card Kick draws it - a bar down
|
||||
* the edge, a mark for what it was, who did it in bold, and the words - on this
|
||||
* client's own surfaces and tokens rather than Kick's page.
|
||||
*
|
||||
* Every kind of event a Kick channel has, all from the daemon
|
||||
* (nobilis/src/backend/kick/chat.rs): KICKs sent, a subscription and one gifted
|
||||
* to somebody, a raid, a channel reward redeemed - which keeps the colour the
|
||||
* streamer gave it - a ban, a timeout or a chat cleared, and the stream going
|
||||
* live or ending.
|
||||
*/
|
||||
export function KickEventCard({ embed }: { embed: Embed }): JSX.Element | null {
|
||||
const who = embed.author
|
||||
if (!who) return null
|
||||
const name = (colour?: string): JSX.Element => (
|
||||
<strong className="kick-event-name" style={{ color: colour ?? embed.authorColor ?? nickColor(who) }}>
|
||||
{who}
|
||||
</strong>
|
||||
)
|
||||
const accent = embedColor(embed.color) || undefined
|
||||
const tint = accent ? ({ '--kick-accent': accent } as React.CSSProperties) : undefined
|
||||
|
||||
if (embed.kind === 'kicks') {
|
||||
return (
|
||||
<div className="kick-event kicks selectable" style={tint}>
|
||||
<div className="kick-event-main">
|
||||
<div className="kick-event-line">
|
||||
{name()}
|
||||
<span className="kick-event-verb">sent</span>
|
||||
<KicksMark />
|
||||
<strong className="kick-event-amount">{embed.title}</strong>
|
||||
{embed.provider && <span className="kick-event-gift small muted">{embed.provider}</span>}
|
||||
</div>
|
||||
{embed.description && <div className="kick-event-message">{embed.description}</div>}
|
||||
</div>
|
||||
<span className="kick-event-badge">
|
||||
<KicksMark size={30} />
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (embed.kind === 'raid') {
|
||||
return (
|
||||
<div className="kick-event raid selectable">
|
||||
<span className="kick-event-badge lead">
|
||||
<Icon name="groups" size={20} />
|
||||
</span>
|
||||
<div className="kick-event-main">
|
||||
<div className="kick-event-line">
|
||||
{name()}
|
||||
<span className="kick-event-verb">
|
||||
{embed.title ? 'has raided with' : 'has raided the channel'}
|
||||
</span>
|
||||
{embed.title && (
|
||||
<>
|
||||
<strong className="kick-event-amount">{embed.title}</strong>
|
||||
<span className="kick-event-verb">{embed.title === '1' ? 'viewer.' : 'viewers.'}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{embed.description && <div className="kick-event-message">{embed.description}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (embed.kind === 'redeem') {
|
||||
return (
|
||||
<div className="kick-event redeem selectable" style={tint}>
|
||||
<span className="kick-event-badge lead">
|
||||
<Icon name="stars" size={22} />
|
||||
</span>
|
||||
<div className="kick-event-main">
|
||||
{name(accent)}
|
||||
<div className="kick-event-line">
|
||||
<span className="kick-event-verb">has redeemed</span>
|
||||
<strong>{embed.title}</strong>
|
||||
</div>
|
||||
{embed.description && <div className="kick-event-message">{embed.description}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (embed.kind === 'subscribe') {
|
||||
return (
|
||||
<div className="kick-event subscribe selectable">
|
||||
<span className="kick-event-badge lead">
|
||||
<Icon name="workspace_premium" size={22} />
|
||||
</span>
|
||||
<div className="kick-event-line">
|
||||
{name()}
|
||||
<span className="kick-event-verb">subscribed</span>
|
||||
{embed.title && (
|
||||
<>
|
||||
<span className="kick-event-verb">-</span>
|
||||
<strong>{embed.title}</strong>
|
||||
<span className="kick-event-verb">months</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (embed.kind === 'moderation') {
|
||||
return (
|
||||
<div className="kick-event moderation selectable">
|
||||
<span className="kick-event-badge lead">
|
||||
<Icon name="gavel" size={20} />
|
||||
</span>
|
||||
<div className="kick-event-line">
|
||||
<strong className="kick-event-name">{who}</strong>
|
||||
<span className="kick-event-verb">{embed.title}</span>
|
||||
{embed.description && (
|
||||
<>
|
||||
<span className="kick-event-verb">by</span>
|
||||
<strong>{embed.description}</strong>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (embed.kind === 'stream') {
|
||||
const live = embed.icon === 'live'
|
||||
return (
|
||||
<div className={`kick-event stream selectable ${live ? 'live' : 'offline'}`}>
|
||||
<span className="kick-event-badge lead">
|
||||
<Icon name={live ? 'sensors' : 'sensors_off'} size={20} />
|
||||
</span>
|
||||
<div className="kick-event-line">
|
||||
<strong className="kick-event-name">{who}</strong>
|
||||
<span className="kick-event-verb">{embed.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const count = Number(embed.title) || 1
|
||||
const recipients = (embed.description ?? '').split('\n').filter(Boolean)
|
||||
return (
|
||||
<div className="kick-event gift selectable">
|
||||
<span className="kick-event-badge lead">
|
||||
<Icon name="redeem" size={22} />
|
||||
</span>
|
||||
<div className="kick-event-main">
|
||||
<div className="kick-event-line">
|
||||
{name()}
|
||||
<span className="kick-event-verb">gifted {count === 1 ? 'a sub' : `${count} subs`}</span>
|
||||
{recipients.length === 1 && (
|
||||
<>
|
||||
<span className="kick-event-verb">to</span>
|
||||
<strong style={{ color: nickColor(recipients[0]) }}>{recipients[0]}</strong>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{recipients.length > 1 && (
|
||||
<div className="kick-event-recipients">
|
||||
<span className="kick-event-verb">to</span>
|
||||
{recipients.map((r) => (
|
||||
<strong key={r} className="kick-event-recipient" style={{ color: nickColor(r) }}>
|
||||
{r}
|
||||
</strong>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -165,6 +165,16 @@ const RENDER_WINDOW = 120
|
||||
const RENDER_STEP = 120
|
||||
/** Rows kept below a jumped-to message, so it has somewhere to sit. */
|
||||
const JUMP_MARGIN = 40
|
||||
/**
|
||||
* The most unread a conversation can have and still open at its first unread
|
||||
* message. Discord's rule, and for the same reason: its first screenful of
|
||||
* history is fifty messages, so an unread run that fits in it is somewhere to
|
||||
* start reading, and one that does not is somewhere nobody wants to be taken -
|
||||
* hundreds or thousands of messages up, with the conversation they came for
|
||||
* a long scroll away. Past it the log opens at its end, as any other does,
|
||||
* and the bar across the top says how much there is and goes back to it.
|
||||
*/
|
||||
const OPEN_AT_UNREAD_LIMIT = 50
|
||||
|
||||
export function MessageList(): JSX.Element {
|
||||
const store = useStore()
|
||||
@@ -527,13 +537,21 @@ export function MessageList(): JSX.Element {
|
||||
// of switching, because what has been read from the daemon may not have
|
||||
// arrived yet - the line has to be in the drawn part of the log, which is a
|
||||
// tail, before it can be scrolled to.
|
||||
// The divider marks the first message newer than the snapshot taken when the
|
||||
// buffer was opened; it deliberately doesn't move as more arrive.
|
||||
const dividerIndex = dividerTs > 0 ? messages.findIndex((m) => m.ts > dividerTs) : -1
|
||||
// Where it is against what is drawn, for the callbacks below that have to
|
||||
// know whether the line exists on the page or only in the log.
|
||||
const dividerRef = useRef({ index: -1, start: 0, length: 0 })
|
||||
dividerRef.current = { index: dividerIndex, start, length: messages.length }
|
||||
|
||||
const openedAtRef = useRef('')
|
||||
useLayoutEffect(() => {
|
||||
const key = `${bufferId}|${dividerTs}`
|
||||
if (openedAtRef.current === key || messages.length === 0) return
|
||||
openedAtRef.current = key
|
||||
const first = dividerTs > 0 ? messages.findIndex((m) => m.ts > dividerTs) : -1
|
||||
if (first >= 0 && messages.length - first > 8) {
|
||||
if (first >= 0 && messages.length - first > 8 && messages.length - first <= OPEN_AT_UNREAD_LIMIT) {
|
||||
wantDividerRef.current = true
|
||||
setShown((n) => Math.max(n, messages.length - first + JUMP_MARGIN))
|
||||
}
|
||||
@@ -543,8 +561,13 @@ export function MessageList(): JSX.Element {
|
||||
const measureDivider = useCallback(() => {
|
||||
const el = scrollRef.current
|
||||
const line = el?.querySelector<HTMLElement>('.new-divider')
|
||||
if (!el || !line) {
|
||||
setDividerAbove(false)
|
||||
if (!el) return
|
||||
if (!line) {
|
||||
// Not drawn is not the same as not there. A conversation opened with a
|
||||
// long unread run is at its end, and its line is up in the log beyond
|
||||
// what is built - which is exactly when the bar is wanted.
|
||||
const { index, start } = dividerRef.current
|
||||
setDividerAbove(index >= 0 && index < start)
|
||||
return
|
||||
}
|
||||
// Clearly above, not merely just off the top: with the line a few pixels
|
||||
@@ -556,7 +579,18 @@ export function MessageList(): JSX.Element {
|
||||
const toDivider = useCallback((smooth: boolean) => {
|
||||
const el = scrollRef.current
|
||||
const line = el?.querySelector<HTMLElement>('.new-divider')
|
||||
if (!el || !line) return
|
||||
if (!el) return
|
||||
if (!line) {
|
||||
// In the log and not on the page: build the log back to it, and the
|
||||
// layout effect below scrolls to it once it is there.
|
||||
const { index, length } = dividerRef.current
|
||||
if (index < 0) return
|
||||
anchor(false)
|
||||
setMissedCount(0)
|
||||
wantDividerRef.current = true
|
||||
setShown((n) => Math.max(n, length - index + JUMP_MARGIN))
|
||||
return
|
||||
}
|
||||
anchor(false)
|
||||
// Going back to the line is not missing anything: what is below is what
|
||||
// was already there, not news.
|
||||
@@ -566,6 +600,13 @@ export function MessageList(): JSX.Element {
|
||||
el.scrollTo({ top: Math.max(0, target), behavior: smooth ? 'smooth' : 'auto' })
|
||||
}, [anchor, messages])
|
||||
|
||||
// Whether the line is out of sight, asked as the log changes as well as when
|
||||
// it is scrolled: a conversation opened at its end never scrolls, and is
|
||||
// the one with the most to say about what is above it.
|
||||
useLayoutEffect(() => {
|
||||
measureDivider()
|
||||
}, [bufferId, dividerTs, messages.length, shown, measureDivider])
|
||||
|
||||
// Once the line is drawn, start there.
|
||||
useLayoutEffect(() => {
|
||||
if (!wantDividerRef.current) return
|
||||
@@ -728,10 +769,6 @@ export function MessageList(): JSX.Element {
|
||||
return () => window.removeEventListener('moho:jump-to-present', go)
|
||||
}, [])
|
||||
|
||||
// The divider marks the first message newer than the snapshot taken when the
|
||||
// buffer was opened; it deliberately doesn't move as more arrive.
|
||||
const dividerIndex = dividerTs > 0 ? messages.findIndex((m) => m.ts > dividerTs) : -1
|
||||
|
||||
return (
|
||||
// Whose conversation this is, for everything drawn inside it: a strictly
|
||||
// routed account's media goes through its route (#257). The attribute is
|
||||
|
||||
@@ -7,6 +7,7 @@ import { EventSource } from './EventSource'
|
||||
import { ForwardPicker } from './ForwardPicker'
|
||||
import { ContextMenu, useContextMenu, type MenuEntry } from './ContextMenu'
|
||||
import { MediaEmbed } from './MediaEmbed'
|
||||
import { KickEventCard, isKickEvent } from './KickEventCard'
|
||||
import { COMMON_EMOJI, EmojiPicker, readRecent } from './EmojiPicker'
|
||||
import { reactionLabel, reactorsSentence } from '../lib/reactions'
|
||||
import { UploadMeter } from './UploadMeter'
|
||||
@@ -406,6 +407,9 @@ function MessageRowBody({
|
||||
// label on.
|
||||
const [highlightNotices] = usePref<boolean>('matrix.highlightNotices', true)
|
||||
const reward = isReward(message.kind)
|
||||
// KICKs sent, or subscriptions gifted: the daemon says which and to whom in
|
||||
// a card, and the line of text is what a copy or a search finds.
|
||||
const kickEvent = (message.embeds ?? []).find(isKickEvent)
|
||||
// Both of the modes that draw an avatar column. Bubbles is otherwise
|
||||
// nothing like comfy, but it wants the same picture beside the same first
|
||||
// line of a group.
|
||||
@@ -786,6 +790,7 @@ function MessageRowBody({
|
||||
message.failed && 'failed',
|
||||
isSystem && 'system',
|
||||
reward && 'reward',
|
||||
kickEvent && 'has-kick-event',
|
||||
grouped && 'grouped',
|
||||
fresh && 'fresh',
|
||||
comfy && 'comfy',
|
||||
@@ -828,7 +833,7 @@ function MessageRowBody({
|
||||
{/* A join, a part, a topic change: not somebody speaking, so no face -
|
||||
but a small mark in the picture's own column, so the line's text
|
||||
starts where every other line's does instead of at the edge. */}
|
||||
{mode === 'comfy' && isSystem && (
|
||||
{mode === 'comfy' && isSystem && !kickEvent && (
|
||||
<span className="message-avatar system-mark" aria-hidden>
|
||||
<Icon name={SYSTEM_MARKS[message.kind] ?? 'info'} size={16} />
|
||||
</span>
|
||||
@@ -1058,6 +1063,8 @@ function MessageRowBody({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : kickEvent ? (
|
||||
<KickEventCard embed={kickEvent} />
|
||||
) : (
|
||||
parts.html && (
|
||||
<span className="message-body selectable">
|
||||
@@ -1101,7 +1108,7 @@ function MessageRowBody({
|
||||
))}
|
||||
|
||||
{(message.embeds || []).map((embed, i) =>
|
||||
embed.kind === 'gif' ? null : (
|
||||
embed.kind === 'gif' || isKickEvent(embed) ? null : (
|
||||
<div
|
||||
key={`e${i}`}
|
||||
className={classes('rich-embed selectable', embed.kind === 'notice' && 'notice')}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shouldOffer } from './matrixcall'
|
||||
|
||||
describe('which end of a Matrix call offers', () => {
|
||||
it('is exactly one end of every pair, whichever way round they ask', () => {
|
||||
// Both ends compute this about each other and must not agree, or the call
|
||||
// collides with itself.
|
||||
expect(shouldOffer('@a:example.org|AAA', '@b:example.org|BBB')).toBe(true)
|
||||
expect(shouldOffer('@b:example.org|BBB', '@a:example.org|AAA')).toBe(false)
|
||||
})
|
||||
|
||||
it('tells two devices of the same person apart', () => {
|
||||
// The case that makes a user-id comparison alone wrong.
|
||||
expect(shouldOffer('@a:example.org|AAA', '@a:example.org|ZZZ')).toBe(true)
|
||||
expect(shouldOffer('@a:example.org|ZZZ', '@a:example.org|AAA')).toBe(false)
|
||||
})
|
||||
|
||||
it('never has an end offer to itself', () => {
|
||||
expect(shouldOffer('@a:example.org|AAA', '@a:example.org|AAA')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -166,7 +166,7 @@ export function isWhisper(kind: string | undefined): boolean {
|
||||
* does not have.
|
||||
*/
|
||||
export function isReward(kind: string | undefined): boolean {
|
||||
return kind === 'reward' || kind === 'sub' || kind === 'raid' || kind === 'poll'
|
||||
return kind === 'reward' || kind === 'sub' || kind === 'raid' || kind === 'poll' || kind === 'kicks'
|
||||
}
|
||||
|
||||
export function bufferKindGlyph(kind: string): string {
|
||||
|
||||
@@ -3983,6 +3983,17 @@ export class ChatStore {
|
||||
b.id === bufferId ? { ...b, unread: 0, highlight: false, mentions: 0 } : b
|
||||
)
|
||||
}
|
||||
// The line a conversation was last opened with is that visit's, and goes
|
||||
// when it is left. Kept, it was found again on every return: a buffer read
|
||||
// to its end, left, and come back to had the old line still in it with
|
||||
// everything said since below it - so it opened far up the history at a
|
||||
// place that was no longer where anything was unread, instead of at the
|
||||
// end. A conversation opened again works out its line afresh, from what
|
||||
// is unread now.
|
||||
if (leaving && leaving !== bufferId && this.state.dividerTsByBuffer[leaving] !== undefined) {
|
||||
const { [leaving]: _gone, ...rest } = this.state.dividerTsByBuffer
|
||||
patch.dividerTsByBuffer = rest
|
||||
}
|
||||
if (dividerTs === undefined && buffer.unread > 0) {
|
||||
const list = this.state.messagesByBuffer[bufferId] || []
|
||||
const firstUnread = list.length >= buffer.unread ? list[list.length - buffer.unread] : list[0]
|
||||
@@ -3992,7 +4003,7 @@ export class ChatStore {
|
||||
// time it was last marked read.
|
||||
const stoppedAt = list.length < buffer.unread ? this.state.lastReadTs?.[bufferId] : undefined
|
||||
patch.dividerTsByBuffer = {
|
||||
...this.state.dividerTsByBuffer,
|
||||
...(patch.dividerTsByBuffer ?? this.state.dividerTsByBuffer),
|
||||
[bufferId]: stoppedAt ?? (firstUnread ? firstUnread.ts - 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -471,6 +471,8 @@ export interface Embed {
|
||||
provider?: string
|
||||
/** Who made it: a video's channel. */
|
||||
author?: string
|
||||
/** The colour the service draws that name in, as `#rrggbb`. */
|
||||
authorColor?: string
|
||||
/** The labelled lines under the description. */
|
||||
fields?: { name: string; value: string }[]
|
||||
/** The small line along the bottom. */
|
||||
@@ -479,7 +481,10 @@ export interface Embed {
|
||||
cta?: { label: string; url?: string }
|
||||
/**
|
||||
* "notice" for the platform's own notices to an account; "gif" for a moving
|
||||
* picture that was linked, drawn as the picture and not as a card.
|
||||
* picture that was linked, drawn as the picture and not as a card; and
|
||||
* "kicks" and "gift-sub" for what somebody did on a Kick stream - `author`
|
||||
* is who, `title` the amount, and for a gift `description` is who received
|
||||
* it, one name to a line - drawn as the card Kick draws it.
|
||||
*/
|
||||
kind?: string
|
||||
/** For a "gif": the video or image to play as it stands, which is not the page that was linked. */
|
||||
|
||||
Reference in New Issue
Block a user