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:
@@ -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
@@ -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))
|
||||
@@ -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
@@ -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)')
|
||||
Executable
+101
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user