Versions only: tag-built releases with a changelog, Gitea mirrored, an update notice (#239)

- The rolling "Latest build" is retired. release.yml (was rolling-release)
  still builds and tests every push to master, as the check that it is
  releasable, and publishes only for a v* tag. A pre-release suffix
  (v1.0.0-rc.1) publishes as a pre-release, never GitHub's "Latest".
- A release page is the version's section of CHANGELOG.md - major
  features first, then fixes, patches and changes, one line each linked to
  its commit - and the downloads table. A version with no section is not
  published; a release candidate uses its release's section unless it has
  its own. Daemon binaries are kept from a tag's run for nobilis's release.
- CHANGELOG.md, with the 1.0.0 section drafted: the major features, and
  the fixes since the first public build, every link checked against its
  repository.
- scripts/changelog-deps.mjs lists direct dependency changes between two
  releases, linked to the commit that made each, net of anything the range
  put back as it found it.
- scripts/mirror-release.sh, run by hand once GitHub has published: moho's
  release copied to Gitea with its files, and nobilis's release made on both
  hosts - tagged at moho's pointer, its notes the major features and the
  nobilis lines, with the two daemon binaries.
- A newer moho is announced at the top of the mentions inbox, counted in
  its badge, until dismissed (per version) - checked on connect and daily
  through the daemon, so through Tor or the proxy when anything is routed.
  Settings > General > Updates turns it off.
- README: downloads from the latest release, and the versioning scheme.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-04 16:22:11 -04:00
co-authored by Claude Opus 5.5
parent 1782558f52
commit d9a9d0c722
12 changed files with 649 additions and 206 deletions
@@ -1,7 +1,13 @@
# Builds moho on every push to master and replaces the files on the single
# rolling release that lives on the moving `latest` tag. A pushed version tag
# (v1.0.0) builds the same way and publishes a release of its own beside it,
# leaving `latest` alone (#239).
# Builds moho and, for a pushed version tag (v1.0.0), publishes the release.
#
# A push to master builds and tests everything the same way and publishes
# nothing: it is the check that master is releasable. Only a tag publishes,
# with the notes for that version taken from CHANGELOG.md and its files named
# after the version (#239). A tag with a pre-release suffix (v1.0.0-rc.1)
# publishes as a pre-release, to rehearse a release without making one.
#
# The rolling `latest` build this file used to keep is retired: there are
# versions now, and only versions.
#
# The shape mirrors how this is built by hand: the daemon first, then the app
# around it, with the Windows installer cross-compiled from Linux exactly as
@@ -16,19 +22,19 @@
#
# Two properties this file is arranged to keep:
#
# - Nothing reaches the release unless typecheck and the Rust tests passed.
# - Nothing reaches a release unless typecheck and the Rust tests passed.
# That is structural rather than a convention: `publish` has `needs:` on
# both build jobs, and a needs-dependency on a failed job never runs.
# - The asset names and the release body agree about which commit they came
# from. See the "the tree is clean" step for why that is not automatic.
# - What is published was built from a clean tree. See the "the tree is
# clean" step for why that is not automatic.
name: rolling release
name: release
on:
push:
branches: [master]
# A versioned release: its own page, its own notes, files named after the
# version. The path filter below does not apply to tags.
# A version: built, then published with its changelog. The path filter
# below does not apply to tags.
tags: ['v*']
# A README edit should not cost forty minutes of compute. Note the absence
# of .github/** - editing this workflow SHOULD trigger a run that
@@ -37,21 +43,14 @@ on:
paths-ignore:
- '**/*.md'
- 'LICENSE'
# The escape hatch for the case above: master moved for documentation
# reasons and the release should catch up anyway.
workflow_dispatch:
# Newest push wins. A build for a commit master has already moved past is of
# no use to a release that only ever holds the latest one.
#
# The cost is a few seconds of exposure: a cancellation landing inside
# `publish` could leave the release holding a mix of old and new files. The
# next run's prune step repairs that. If that window ever matters, split this
# into a build group with cancel-in-progress true and a publish group with it
# false, so publishes queue rather than cancel.
# Newest push wins on master: a check of a commit master has already moved
# past is of no use. A tag is its own ref, so a release is never cancelled by
# a push to master, nor one tag by another.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ github.ref_type != 'tag' }}
# Read by default. Only `publish` asks for more, and only for itself.
permissions:
@@ -321,6 +320,17 @@ jobs:
if-no-files-found: error
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
# here. Not an asset of moho's release: the AppImage already carries it.
- uses: actions/upload-artifact@v4
if: github.ref_type == 'tag'
with:
name: daemon-linux
path: nobilis/target/release/nobilis
if-no-files-found: error
retention-days: 30
# --------------------------------------------------------------------
# Windows, cross-compiled. Same runner OS, same scripts, plus mingw.
#
@@ -480,15 +490,24 @@ jobs:
if-no-files-found: error
retention-days: 7
- uses: actions/upload-artifact@v4
if: github.ref_type == 'tag'
with:
name: daemon-windows
path: nobilis/target/release/nobilis.exe
if-no-files-found: error
retention-days: 30
# --------------------------------------------------------------------
# Move the `latest` tag onto this commit, and make the release sitting on
# it hold exactly the two files the build jobs produced.
# A version tag only: the release, its files and its changelog.
#
# No checkout at all: everything here is the GitHub API through gh, which
# is preinstalled on the runner, with the built-in token. Nothing to clone
# and no credentials to arrange.
# The notes are the version's section of CHANGELOG.md - major features,
# then fixes, patches and changes, each linked to its commit - followed by
# the downloads table. A version with no section is not published: a
# release without its changelog is the one thing this step exists to stop.
# --------------------------------------------------------------------
publish:
if: github.ref_type == 'tag'
needs: [build-linux, build-windows]
runs-on: ubuntu-latest
timeout-minutes: 10
@@ -501,204 +520,88 @@ jobs:
BUILD: ${{ needs.build-linux.outputs.build }}
steps:
# Both jobs' artifacts into one flat directory, which is then the
# authoritative list of what the release should contain.
# Only the changelog is needed from the tree.
- uses: actions/checkout@v4
with:
sparse-checkout: CHANGELOG.md
sparse-checkout-cone-mode: false
- uses: actions/download-artifact@v4
with:
pattern: '{linux,windows}'
path: artifacts
merge-multiple: true
- name: what was built
run: |
ls -l artifacts
# Three files, no more and no fewer: AppImage, deb, Windows
# installer. If electron-builder ever starts emitting something else
# into those globs, this says so here rather than by surprise on the
# release page.
test "$(find artifacts -maxdepth 1 -type f | wc -l)" -eq 3
# A release is tied to a tag, so a rolling release means a moving tag.
# The refs API rather than `git push -f` because it needs no checkout
# and no credential helper. The PATCH fails the very first time - there
# is no such ref yet - which is what the POST is for.
#
# -F force=true, capital: it sends a JSON boolean. Lowercase -f would
# send the string "true", which the API rejects.
#
# Refs written with GITHUB_TOKEN do not start workflow runs, so this
# cannot loop back into itself.
- name: move the latest tag onto this commit
if: github.ref_type != 'tag'
run: |
gh api -X PATCH "repos/$GH_REPO/git/refs/tags/latest" \
-f sha="$GITHUB_SHA" -F force=true \
|| gh api -X POST "repos/$GH_REPO/git/refs" \
-f ref=refs/tags/latest -f sha="$GITHUB_SHA"
# Rewritten every run, because the whole job of this text is to say
# which commit the files below came from.
#
# The warning about asset links is not boilerplate: the files are named
# after the commit, so a direct link to one stops resolving the moment
# the next push lands. The release page URL is the stable thing.
- name: compose the release notes
if: github.ref_type != 'tag'
env:
SUBJECT: ${{ github.event.head_commit.message }}
run: |
# A dispatched run has no head_commit, so there is no subject to
# quote - say the commit without a dangling dash after it.
if [ -n "${SUBJECT:-}" ]; then
commit="**Commit:** \`$BUILD\` — ${SUBJECT%%$'\n'*}"
else
commit="**Commit:** \`$BUILD\`"
fi
{
echo "Rolling build of \`master\`, rebuilt and its files replaced on every"
echo "push. They are named after the commit, so a direct link to one stops"
echo "resolving once the next commit lands - the release page URL is the"
echo "stable thing to share."
echo
echo "$commit"
echo "**Built:** $(date -u '+%Y-%m-%d %H:%M UTC')"
echo "**Run:** $GITHUB_SERVER_URL/$GH_REPO/actions/runs/$GITHUB_RUN_ID"
echo
echo "| file | platform |"
echo "| --- | --- |"
echo "| \`moho-$BUILD.AppImage\` | Linux x86_64 - mark it executable and run it |"
echo "| \`moho-$BUILD.deb\` | Debian / Ubuntu x86_64 - installs the sandbox profile with it |"
echo "| \`moho-$BUILD.exe\` | Windows x64 - a per-user NSIS installer, no administrator needed |"
echo
echo "Each carries the \`nobilis\` daemon built alongside it, so the app and"
echo "the daemon in one download always match."
echo
echo "**The Windows installer is not code-signed**, and cross-built with mingw."
echo
echo "- SmartScreen warns when you run it: choose *More info*, then *Run anyway*."
echo "- If Windows 11's **Smart App Control** is on, moho will install and then"
echo " never open - it blocks every unsigned program, silently. Turn it off in"
echo " *Windows Security > App & browser control > Smart App Control settings*."
echo " The installer checks and warns before installing."
echo
echo "Not a versioned release, and no stability promise."
} > "$RUNNER_TEMP/notes.md"
cat "$RUNNER_TEMP/notes.md"
- name: create or refresh the release
if: github.ref_type != 'tag'
run: |
if gh release view latest >/dev/null 2>&1; then
gh release edit latest \
--title "Latest build" \
--notes-file "$RUNNER_TEMP/notes.md"
else
gh release create latest \
--title "Latest build" \
--notes-file "$RUNNER_TEMP/notes.md"
fi
# Upload first and prune second, deliberately. The files are named after
# the commit, so --clobber alone never removes anything and the release
# would grow a pair per push forever; deleting first would leave a
# window where somebody landing on the page finds nothing to download.
# In this order it briefly holds four files and never holds none.
- name: upload this build's files
if: github.ref_type != 'tag'
run: gh release upload latest artifacts/* --clobber
- name: drop the previous build's files
if: github.ref_type != 'tag'
run: |
keep="$(cd artifacts && ls -1)"
for asset in $(gh release view latest --json assets -q '.assets[].name'); do
printf '%s\n' "$keep" | grep -qxF "$asset" && continue
echo "dropping $asset"
gh release delete-asset latest "$asset" --yes
done
# Last, because this is what dates the release, and it should say when
# the files below it were finished rather than when the job started.
#
# `gh release edit` above rewrites the title, the body and the assets,
# but it cannot touch published_at - that field only moves when a
# release goes from draft to published, and this one was published once,
# in the run that first created it. Everything after that refreshed the
# contents underneath a header still dated to that first run, so the
# page read as though the build had stopped publishing while it was in
# fact replacing both files every push. Twelve builds landed behind a
# timestamp that never moved before anyone noticed.
#
# Draft and back is the only way to move it. Addressed by release id
# rather than by tag because a draft is not reliably addressable by tag,
# and the id is read while the release is still published.
#
# The release is invisible between the two calls - a second or so - and
# /releases/latest 404s for anyone who lands in exactly that gap. That
# is the same brief exposure the concurrency note above already accepts,
# and it is at the very end, after the assets are in place, so the
# release never reappears holding the wrong files.
- name: date the release to now
if: github.ref_type != 'tag'
run: |
id="$(gh api "repos/$GH_REPO/releases/tags/latest" -q .id)"
gh api -X PATCH "repos/$GH_REPO/releases/$id" -F draft=true >/dev/null
gh api -X PATCH "repos/$GH_REPO/releases/$id" -F draft=false >/dev/null
# --- a version tag: a release of its own (#239) -----------------------
#
# Named after the version rather than the commit, because this one is
# meant to be linked to and kept: moho-1.0.0.AppImage, not
# moho-<sha>.AppImage. The build itself is named after the commit (the
# name is decided inside the npm script), so the files are renamed here.
# Three files, no more and no fewer: AppImage, deb, Windows installer.
# Built named after the commit (the name is decided inside the npm
# script); published named after the version.
- name: name the files after the version
if: github.ref_type == 'tag'
run: |
version="${GITHUB_REF_NAME#v}"
test "$(find artifacts -maxdepth 1 -type f | wc -l)" -eq 3
for f in artifacts/moho-"$BUILD".*; do
mv -v "$f" "artifacts/moho-$version.${f##*.}"
done
ls -l artifacts
- name: compose the version's notes
if: github.ref_type == 'tag'
- name: compose the notes from the changelog
run: |
version="${GITHUB_REF_NAME#v}"
# The section headed "## [<version>]", up to the next "## [".
section() {
awk -v v="$1" '
index($0, "## [" v "]") == 1 { on = 1; next }
on && /^## \[/ { exit }
on { print }
' CHANGELOG.md
}
section "$version" > "$RUNNER_TEMP/changes.md"
# A release candidate rehearses its release's notes, unless it has
# a section of its own: v1.0.0-rc.1 publishes "## [1.0.0]".
if ! grep -q '[^[:space:]]' "$RUNNER_TEMP/changes.md" && [ "${version%%-*}" != "$version" ]; then
section "${version%%-*}" > "$RUNNER_TEMP/changes.md"
fi
if ! grep -q '[^[:space:]]' "$RUNNER_TEMP/changes.md"; then
echo "::error::CHANGELOG.md has no section for $version - write it, then tag again"
exit 1
fi
{
echo "moho $version, built from \`$BUILD\`."
cat "$RUNNER_TEMP/changes.md"
echo
echo "| file | platform |"
echo "## Downloads"
echo
echo "| File | Platform |"
echo "| --- | --- |"
echo "| \`moho-$version.AppImage\` | Linux x86_64 - mark it executable and run it |"
echo "| \`moho-$version.deb\` | Debian / Ubuntu x86_64 - installs the sandbox profile with it |"
echo "| \`moho-$version.exe\` | Windows x64 - a per-user NSIS installer, no administrator needed |"
echo "| \`moho-$version.deb\` | Debian / Ubuntu x86_64 |"
echo "| \`moho-$version.exe\` | Windows x64 - a per-user installer, no administrator needed |"
echo
echo "Each carries the \`nobilis\` daemon built alongside it."
echo
echo "**The Windows installer is not code-signed.**"
echo
echo "- SmartScreen warns when you run it: choose *More info*, then *Run anyway*."
echo "- If Windows 11's **Smart App Control** is on, moho will install and then"
echo " never open - it blocks every unsigned program, silently. Turn it off in"
echo " *Windows Security > App & browser control > Smart App Control settings*."
echo " The installer checks and warns before installing."
echo
echo "**Built:** $(date -u '+%Y-%m-%d %H:%M UTC')"
echo "**Run:** $GITHUB_SERVER_URL/$GH_REPO/actions/runs/$GITHUB_RUN_ID"
echo "**The Windows installer is not code-signed.** SmartScreen warns when you run it:"
echo "choose *More info*, then *Run anyway*. If Windows 11's **Smart App Control** is on,"
echo "moho installs but never opens; turn it off in *Windows Security > App & browser"
echo "control > Smart App Control settings*. The installer checks and warns."
} > "$RUNNER_TEMP/notes.md"
cat "$RUNNER_TEMP/notes.md"
# Created as a draft and published only once its files are attached, so
# nobody lands on a version page with nothing to download. A tag pushed
# again (a failed run retried) finds its release already there: the
# files are replaced and the notes kept, since they may have been edited
# by hand since.
# Created as a draft and published once its files are attached, so
# nobody lands on a version with nothing to download. A pre-release
# (a suffix after the version) is never GitHub's "Latest". A tag pushed
# again finds its release there: the files are replaced, the notes too,
# since the changelog is where they are corrected.
- name: publish the version
if: github.ref_type == 'tag'
run: |
version="${GITHUB_REF_NAME#v}"
case "$version" in
*-*) kind="--prerelease --latest=false" ;;
*) kind="--latest" ;;
esac
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release edit "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/notes.md"
gh release upload "$GITHUB_REF_NAME" artifacts/* --clobber
else
gh release create "$GITHUB_REF_NAME" --draft --verify-tag \
--title "moho ${GITHUB_REF_NAME#v}" --notes-file "$RUNNER_TEMP/notes.md"
--title "moho $version" --notes-file "$RUNNER_TEMP/notes.md"
gh release upload "$GITHUB_REF_NAME" artifacts/*
gh release edit "$GITHUB_REF_NAME" --draft=false --latest
# shellcheck disable=SC2086 - two flags, deliberately split
gh release edit "$GITHUB_REF_NAME" --draft=false $kind
fi
+103
View File
@@ -0,0 +1,103 @@
# Changelog
Every release of moho, newest first. The release page for a version is its
section here, copied by the release workflow - a version tagged without one
is not published.
Versions are MAJOR.FEATURE.FIX: the first number is a major release, the
middle one counts protocols added and major features, the last counts
bug and security fixes. 1.1.0 would add a protocol; 1.0.5 is the fifth fix
release of 1.0.
Each section has two parts:
- **Major features** - new protocols and features too large for one commit,
without links.
- **Fixes, patches and changes** - one short line each, linked to the commit
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.
### Fixes, patches and changes
- 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))
+7 -2
View File
@@ -43,8 +43,13 @@ again and everything is where you left it.
## Get it
Prebuilt Linux and Windows downloads are on the
[latest release](https://github.com/Moho-Chat/moho/releases/tag/latest), rebuilt from `master`
on every change.
[latest release](https://github.com/Moho-Chat/moho/releases/latest); what each version
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`.
+1 -1
Submodule nobilis updated: 8eca16e175...b697fbb23c
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env node
// Lists dependency updates for CHANGELOG.md, one line per change, linked to
// the commit that made it:
//
// - Updated Rust crate rustls from 0.23.43 to 0.23.45 ([nobilis `2f5918e`](...))
//
// Usage: node scripts/changelog-deps.mjs <from> [<to>]
// <from>, <to>: moho refs - the previous release's tag, and HEAD by default.
// nobilis is followed through the submodule pointer at each end.
//
// Direct dependencies only - what nobilis's Cargo.toml and moho's
// package.json name. A transitive bump is visible under the direct one that
// pulled it in, and listing every one would bury the changelog.
import { execFileSync } from 'node:child_process'
import path from 'node:path'
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..')
const NOBILIS = path.join(ROOT, 'nobilis')
const LINK = { moho: 'https://github.com/Moho-Chat/moho/commit/', nobilis: 'https://github.com/Moho-Chat/nobilis/commit/' }
const git = (dir, ...args) => execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', maxBuffer: 64 << 20 })
const show = (dir, rev, file) => {
try {
return git(dir, 'show', `${rev}:${file}`)
} catch {
return null
}
}
/** Names Cargo.toml depends on directly, any section or target. */
function cargoDirect(toml) {
const names = new Set()
let inDeps = false
for (const raw of (toml ?? '').split('\n')) {
const line = raw.trim()
const table = line.match(/^\[(.+)\]$/)
if (table) {
const t = table[1]
const dotted = t.match(/(?:^|\.)(?:dependencies|build-dependencies)\.([A-Za-z0-9_-]+)$/)
if (dotted) names.add(dotted[1])
inDeps = /(?:^|\.)(?:dependencies|build-dependencies)$/.test(t)
continue
}
if (!inDeps) continue
const key = line.match(/^([A-Za-z0-9_-]+)\s*(=|\.)/)
if (key) names.add(key[1])
}
return names
}
/**
* name -> the version nobilis itself resolves it to, from Cargo.lock.
*
* Read from nobilis's own entry rather than from every package of that name:
* a crate can be in the lock twice - the one nobilis uses and an older or
* newer one something else pulled in - and only the first is nobilis's.
* Cargo writes "name version" in that list exactly when the name alone is
* ambiguous.
*/
function cargoVersions(lock, wanted) {
const blocks = (lock ?? '').split('[[package]]').map((block) => ({
name: block.match(/^name = "([^"]+)"/m)?.[1],
version: block.match(/^version = "([^"]+)"/m)?.[1],
deps: [...(block.match(/^dependencies = \[([\s\S]*?)\]/m)?.[1] ?? '').matchAll(/"([^"]+)"/g)].map((m) => m[1])
}))
const root = blocks.find((b) => b.name === 'nobilis')
const versionsOf = (name) => blocks.filter((b) => b.name === name).map((b) => b.version)
const found = new Map()
for (const entry of root?.deps ?? []) {
const [name, version] = entry.split(' ')
if (!wanted.has(name)) continue
const resolved = version ?? versionsOf(name)[0]
if (resolved) found.set(name, resolved)
}
return found
}
function npmVersions(pkgJson, lockJson) {
const out = new Map()
if (!pkgJson || !lockJson) return out
const pkg = JSON.parse(pkgJson)
const lock = JSON.parse(lockJson)
for (const name of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })) {
const v = lock.packages?.[`node_modules/${name}`]?.version
if (v) out.set(name, v)
}
return out
}
function diff(before, after, kind, repo, sha) {
const changes = []
for (const [name, v] of after) {
const was = before.get(name)
if (was !== v) changes.push({ kind, name, was, now: v, repo, sha })
}
for (const [name, was] of before) {
if (!after.has(name)) changes.push({ kind, name, was, now: undefined, repo, sha })
}
return changes
}
function line({ kind, name, was, now, repo, sha }) {
const link = `([${repo} \`${sha}\`](${LINK[repo]}${sha}))`
if (was === undefined) return `- Added ${kind} ${name} ${now} ${link}`
if (now === undefined) return `- Removed ${kind} ${name} ${was} ${link}`
return `- Updated ${kind} ${name} from ${was} to ${now} ${link}`
}
/**
* Drops a dependency the range left as it found it - removed and put back,
* or bumped and reverted. Its steps happened, but the release did not change
* it, and the changelog is about the release.
*/
function net(changes) {
const first = new Map()
const last = new Map()
for (const c of changes) {
const key = `${c.kind}\0${c.name}`
if (!first.has(key)) first.set(key, c.was)
last.set(key, c.now)
}
return changes.filter((c) => {
const key = `${c.kind}\0${c.name}`
return first.get(key) !== last.get(key)
})
}
/** Each commit in the range that touched the lockfile, oldest first. */
function walk(dir, from, to, files, versionsAt, kind, repo) {
const commits = git(dir, 'log', '--reverse', '--format=%h', `${from}..${to}`, '--', ...files).split('\n').filter(Boolean)
const changes = []
for (const sha of commits) {
changes.push(...diff(versionsAt(`${sha}^`), versionsAt(sha), kind, repo, sha))
}
return changes
}
const [from, to = 'HEAD'] = process.argv.slice(2)
if (!from) {
console.error('usage: node scripts/changelog-deps.mjs <previous release tag> [<to>]')
process.exit(2)
}
const gitlink = (rev) => git(ROOT, 'rev-parse', `${rev}:nobilis`).trim()
const rustAt = (rev) => cargoVersions(show(NOBILIS, rev, 'Cargo.lock'), cargoDirect(show(NOBILIS, rev, 'Cargo.toml')))
const npmAt = (rev) => npmVersions(show(ROOT, rev, 'package.json'), show(ROOT, rev, 'package-lock.json'))
const out = [
...walk(NOBILIS, gitlink(from), gitlink(to), ['Cargo.lock', 'Cargo.toml'], rustAt, 'Rust crate', 'nobilis'),
...walk(ROOT, from, to, ['package-lock.json', 'package.json'], npmAt, 'npm package', 'moho')
]
const lines = net(out).map(line)
console.log(lines.length ? lines.join('\n') : '(no direct dependency changed)')
+101
View File
@@ -0,0 +1,101 @@
#!/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)"
GITEA="https://git.salastil.com/api/v1"
TOKEN="$(cat "$HOME/.config/gitea/token")"
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"])')"; 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 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"
+33
View File
@@ -8413,3 +8413,36 @@ video.lightbox-media {
border: 1px solid #c9403c;
border-radius: var(--corner-radius-small);
}
/* A newer moho, at the top of the mentions inbox. */
.release-row {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.release-open {
display: flex;
flex: 1;
min-width: 0;
align-items: center;
gap: var(--spacing-s);
padding: 0;
border: none;
background: none;
color: inherit;
text-align: left;
cursor: pointer;
}
.release-icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
flex: none;
border-radius: 50%;
background: var(--primary);
color: var(--on-primary, #fff);
}
+51 -6
View File
@@ -23,6 +23,12 @@ import { bufferDisplayName, formatFullTime, formatRelativeTime, serviceLabel } f
export function MentionsInbox(): JSX.Element {
const store = useStore()
const mentions = useChat((s) => s.mentions)
const newRelease = useChat((s) => s.newRelease)
// Dismissed by version: the next release is news again.
const [dismissedRelease, setDismissedRelease] = usePref<string>('updates.dismissed', '')
// Switched off in Settings: gone at once, not at the next check.
const [notify] = usePref<boolean>('updates.notify', true)
const release = notify && newRelease && newRelease.version !== dismissedRelease ? newRelease : null
const buffers = useChat((s) => s.buffers)
const accounts = useChat((s) => s.accounts)
const [muted] = usePref<string[]>('mutedBuffers', [])
@@ -100,20 +106,27 @@ export function MentionsInbox(): JSX.Element {
store.setJumpTarget(id)
}
// A new release counts as one more thing waiting, so the badge shows it.
const waiting = unread.length + (release ? 1 : 0)
return (
<div className="mentions-inbox" ref={box}>
<IconButton
name="alternate_email"
title={unread.length > 0 ? `${unread.length} unread mentions` : 'Mentions'}
className={unread.length > 0 ? 'has-mentions' : undefined}
title={
release && unread.length === 0
? `moho ${release.version} is out`
: unread.length > 0
? `${unread.length} unread mentions`
: 'Mentions'
}
className={waiting > 0 ? 'has-mentions' : undefined}
onClick={() => setOpen(!open)}
/>
{/* Capped in the label rather than the list: past a certain number the
exact count stops meaning anything, and a three-digit badge stops
fitting on the button. */}
{unread.length > 0 && (
<span className="mentions-count">{unread.length > 99 ? '99+' : unread.length}</span>
)}
{waiting > 0 && <span className="mentions-count">{waiting > 99 ? '99+' : waiting}</span>}
{open && (
<HeaderPopover anchor={box.current} width={380} className="mentions-panel" onClose={() => setOpen(false)}>
@@ -136,7 +149,39 @@ export function MentionsInbox(): JSX.Element {
)}
</div>
{unread.length === 0 && (
{/* A newer moho, at the top: not a mention, but the same kind of
thing - a notice owed a look, gone once it has had one. The page
it opens is that version's changelog. */}
{release && (
<div className="mention-row release-row">
<button
type="button"
className="release-open"
onClick={() => void window.moho.openExternal(release.url)}
title={`Opens ${release.url}`}
>
<span className="release-icon">
<Icon name="new_releases" size={20} />
</span>
<span className="mention-body">
<span className="mention-head small">
<span className="mention-from">moho {release.version} is out</span>
</span>
<span className="mention-text small muted">
You have {__APP_VERSION__}. See what changed and download it.
</span>
</span>
</button>
<IconButton
name="close"
size={16}
title="Dismiss until the next release"
onClick={() => setDismissedRelease(release.version)}
/>
</div>
)}
{unread.length === 0 && !release && (
<div className="mentions-panel-empty muted">
<Icon name="alternate_email" size={24} />
<span className="small">Nothing is waiting on you.</span>
@@ -489,6 +489,15 @@ function GeneralSettings(): JSX.Element {
<GlobalHighlightKeywords />
</SettingsSection>
<SettingsSection title="Updates">
<ToggleSetting
settingKey="updates.notify"
label="Tell me when a new version is out"
description="Checks GitHub for a newer release at start and once a day, through Tor or your proxy whenever anything is routed, and says so in the mentions inbox"
defaultValue={true}
/>
</SettingsSection>
<SettingsSection title="Media">
<ToggleSetting
settingKey="media.autoplay"
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest'
import { isNewerVersion } from './version'
describe('isNewerVersion', () => {
it('compares each part as a number', () => {
expect(isNewerVersion('1.0.10', '1.0.9')).toBe(true)
expect(isNewerVersion('1.1.0', '1.0.9')).toBe(true)
expect(isNewerVersion('2.0.0', '1.9.9')).toBe(true)
expect(isNewerVersion('1.0.9', '1.0.10')).toBe(false)
expect(isNewerVersion('1.0.0', '1.0.0')).toBe(false)
})
it('counts a release as newer than its own pre-releases', () => {
expect(isNewerVersion('1.0.0', '1.0.0-rc.1')).toBe(true)
expect(isNewerVersion('1.0.0', '1.0.1-rc.1')).toBe(false)
expect(isNewerVersion('1.0.0', '0.1.0')).toBe(true)
})
it('is never true for something that is not a version', () => {
expect(isNewerVersion('latest', '1.0.0')).toBe(false)
expect(isNewerVersion('1.0', '0.1.0')).toBe(false)
expect(isNewerVersion('1.0.0', 'dev')).toBe(false)
})
})
+21
View File
@@ -0,0 +1,21 @@
/**
* Whether `candidate` is a newer moho than `current`.
*
* MAJOR.FEATURE.FIX, compared as numbers (1.0.10 is after 1.0.9). A release
* is newer than its own pre-releases: somebody on 1.0.0-rc.1 is told when
* 1.0.0 is out. Anything that is not a version is never newer - a build
* should not nag about a release it cannot even read the number of.
*/
export function isNewerVersion(candidate: string, current: string): boolean {
const parse = (v: string): { nums: number[]; pre: boolean } | null => {
const m = /^v?(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(v.trim())
return m ? { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: !!m[4] } : null
}
const a = parse(candidate)
const b = parse(current)
if (!a || !b) return false
for (let i = 0; i < 3; i++) {
if (a.nums[i] !== b.nums[i]) return a.nums[i] > b.nums[i]
}
return b.pre && !a.pre
}
+46
View File
@@ -34,6 +34,7 @@ import { firstFrame, SCREEN_CHOICE_MS } from '../lib/capture'
import { cameraKey, closeFeeds, feedFrame, streamKey as feedStreamKey } from '../lib/framefeed'
import { DEFAULT_QUALITY, encodeSettings, sendCamera, shareScreen, type VideoQuality } from '../lib/discordscreen'
import type { PopoutState } from '../../../shared/ipc'
import { isNewerVersion } from '../lib/version'
/**
* The whole client-side model, held in one immutable object that is replaced
@@ -362,6 +363,12 @@ export interface ChatState {
* reopened, and so the composer can send into it.
*/
openThread: OpenThread | null
/**
* A released moho newer than this one, shown at the top of the mentions
* inbox until it is dismissed or installed. Null when there is none, or
* when asking has been switched off.
*/
newRelease: { version: string; name: string; url: string } | null
linkUp: boolean
accounts: Account[]
buffers: BufferEntry[]
@@ -662,6 +669,7 @@ export interface Toast {
}
const INITIAL: ChatState = {
newRelease: null,
linkUp: false,
accounts: [],
buffers: [],
@@ -1230,9 +1238,45 @@ export class ChatStore {
const status = await window.moho.daemonStatus()
this.set({ linkUp: status.linkUp })
if (status.linkUp) await this.refreshAll()
// Again once a day, for a window that stays open for weeks. A release
// has no event to wait for, so this is asked rather than heard.
if (!this.state.pinnedBufferId) {
this.releaseTimer = setInterval(() => void this.checkForRelease(), 24 * 60 * 60 * 1000)
}
}
private releaseTimer: ReturnType<typeof setInterval> | null = null
/**
* Whether a newer moho has been released - see newRelease.
*
* Asked of the daemon, which goes the way any request belonging to no
* account goes: through Tor or the proxy whenever anything is routed.
* Quiet on failure - offline, rate-limited, no route - since the next
* reconnect or the next day asks again, and an error about an update
* check is worse than no news.
*/
async checkForRelease(): Promise<void> {
if (this.state.pinnedBufferId) return
try {
const prefs = await window.moho.prefs.getAll()
if (prefs['updates.notify'] === false) {
this.set({ newRelease: null })
return
}
// A setting with no switch for it: a fork names its own repository.
const repo = typeof prefs['updates.repo'] === 'string' && prefs['updates.repo'] ? prefs['updates.repo'] : 'Moho-Chat/moho'
const found = await window.moho.rpc<{ version: string; name: string; url: string } | null>('latestRelease', {
repo
})
this.set({ newRelease: found && isNewerVersion(found.version, __APP_VERSION__) ? found : null })
} catch {
// Left as it was; asked again on the next reconnect or the next day.
}
}
dispose(): void {
if (this.releaseTimer) clearInterval(this.releaseTimer)
if (this.sweepTimer) clearInterval(this.sweepTimer)
if (this.mentionsTimer) clearTimeout(this.mentionsTimer)
}
@@ -1249,6 +1293,8 @@ export class ChatStore {
this.refreshVoiceSessions(),
this.refreshIncomingCalls()
])
// Not awaited: GitHub being slow is no reason to hold the window up.
void this.checkForRelease()
if (this.state.activeBufferId) await this.selectBuffer(this.state.activeBufferId)
}