Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .github/workflows/tauri-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ jobs:
ANDROID_KEY_BASE64: ${{ secrets.ANDROID_KEY_BASE64 }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
# Release tags only: excludes nightly and unparseable tags (empty version).
IS_RELEASE: ${{ startsWith(needs.setup-release.outputs.tag, 'v') }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -323,6 +325,24 @@ jobs:
base64 -d <<< "$ANDROID_KEY_BASE64" > "$RUNNER_TEMP/keystore.jks"
echo "storeFile=$RUNNER_TEMP/keystore.jks" >> keystore.properties

# Must run before the Google build: they share one Gradle output directory.
- name: Build FOSS Android APK
if: ${{ env.IS_RELEASE == 'true' }}
shell: bash
env:
SABLE_FOSS: '1'
run: |
# tauri-build validates every file in capabilities/, so the geolocation
# permissions must be absent, not just unreferenced.
rm src-tauri/capabilities/geolocation.json
pnpm tauri android build --apk --target aarch64 armv7 -- --no-default-features --features wry,matrix-crypto
git checkout -- src-tauri/capabilities/geolocation.json
OUT='src-tauri/gen/android/app/build/outputs/apk/universal/release'
APK=$(find "$OUT" -name '*.apk' -type f | head -1)
[ -n "$APK" ] || { echo 'FOSS APK not found' >&2; exit 1; }
mkdir -p android-artifacts
mv "$APK" "android-artifacts/Sable-${VERSION}-android-universal-foss.apk"

- name: Build Android bundles
shell: bash
env:
Expand All @@ -349,6 +369,7 @@ jobs:
subject-path: |
src-tauri/gen/android/app/build/outputs/apk/universal/release/*.apk
src-tauri/gen/android/app/build/outputs/bundle/universalRelease/*.aab
${{ env.IS_RELEASE == 'true' && 'android-artifacts/*.apk' || '' }}

- name: Attach Android bundles to release
shell: bash
Expand All @@ -357,7 +378,8 @@ jobs:
run: |
OUT="src-tauri/gen/android/app/build/outputs"
for f in "$OUT"/apk/universal/release/*.apk \
"$OUT"/bundle/universalRelease/*.aab; do
"$OUT"/bundle/universalRelease/*.aab \
android-artifacts/*.apk; do
[ -e "$f" ] || continue
echo "Uploading $f"
gh release upload "$TAG" "$f" --clobber
Expand Down
4 changes: 4 additions & 0 deletions fastlane/metadata/android/en-US/changelogs/1022000.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
• Add support for description lists
• Allow swiping away in-app notifications on mobile
• Add a configurable gif provider (Tenor, Giphy or Klipy) and send gifs through the media proxy
• Add a customizable Global shortcut to show or hide the app window
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/changelogs/1022001.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
• Cap update banner changelog text so the update is updateable.
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/changelogs/1022003.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
• Enable native FCM push on Android by adding the Firebase app config.
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/changelogs/1022004.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
• Fix devices repeatedly reverting to unverified.
12 changes: 12 additions & 0 deletions fastlane/metadata/android/en-US/full_description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Sable is a Matrix client built to enhance the user experience with quality-of-life features, cosmetics, utilities, and sheer usability. It is a fork of Cinny.

Features:

* End-to-end encrypted messaging, with encrypted push notification previews decrypted on-device
* Push without Google Play Services, using a built-in UnifiedPush distributor — no separate distributor app required
* Voice and video calls over MatrixRTC
* Spaces, threads, and forum rooms
* Customisable app icons, themes, and a configurable GIF provider
* Message reporting, forwarding, and read-receipt controls

Sable is free software licensed under the AGPL-3.0. Crash reporting is opt-in and disabled until you consent.
Binary file added fastlane/metadata/android/en-US/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/short_description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
An almost stable Matrix client
1 change: 1 addition & 0 deletions fastlane/metadata/android/en-US/title.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sable
6 changes: 6 additions & 0 deletions knope.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ command = "git switch -c release"
[[workflows.steps]]
type = "PrepareRelease"

# IzzyOnDroid reads fastlane metadata from the tagged commit, so the changelog
# has to be written here rather than in tauri-build.yml, which runs after tagging.
[[workflows.steps]]
type = "Command"
command = "node scripts/fastlane-changelog.js $version && git add fastlane/metadata/android/en-US/changelogs"

[[workflows.steps]]
type = "Command"
command = "git commit -m \"chore: prepare release $version\""
Expand Down
54 changes: 54 additions & 0 deletions scripts/fastlane-changelog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env node
//MISE hide=true
//MISE description="Generate the fastlane changelog for the current version"
/* oxlint-disable no-console */

import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';
import process from 'node:process';

const version = process.argv[2];
if (!version || !/^\d+\.\d+\.\d+/.test(version)) {
console.error(
`Usage: node scripts/fastlane-changelog.js <version> (got: ${version ?? '<none>'})`
);
process.exit(1);
}

// Matches the versionCode Tauri writes into gen/android/app/tauri.properties,
// which is what fastlane keys changelog files by.
const [major, minor, patch] = version.split(/[.-]/, 3).map(Number);
const versionCode = major * 1_000_000 + minor * 1_000 + patch;

// F-Droid and Play both reject changelogs over 500 characters. Measured in
// bytes so the multi-byte bullets cannot push an accepted file over the limit.
const LIMIT = 500;
const size = (text) => Buffer.byteLength(text, 'utf8');

const changelog = readFileSync('CHANGELOG.md', 'utf8');
const start = changelog.indexOf(`\n## ${version} `);
if (start === -1) {
console.error(`No "## ${version}" section in CHANGELOG.md`);
process.exit(1);
}
const rest = changelog.slice(start + 1);
const end = rest.indexOf('\n## ', 1);
const section = end === -1 ? rest : rest.slice(0, end);

const entries = section
.split('\n')
.filter((line) => line.startsWith('* '))
// Attribution is noise on a store listing and eats the character budget.
.map((line) => `• ${line.slice(2).replace(/ by @\S+( in #\d+)?\.?$/, '')}`);

let body = '';
for (const entry of entries) {
if (size(body) + size(entry) + 1 > LIMIT) break;
body += `${entry}\n`;
}
if (!body) body = `${entries[0].slice(0, LIMIT / 4)}\n`;

const out = `fastlane/metadata/android/en-US/changelogs/${versionCode}.txt`;
mkdirSync(dirname(out), { recursive: true });
writeFileSync(out, body);
console.log(`Wrote ${out} (${size(body)} bytes)`);
4 changes: 2 additions & 2 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,12 @@ windows = { version = "0.62", features = [
tauri-plugin-single-instance = { version = "2.4.3", features = ["deep-link"] }

[target.'cfg(any(windows, target_os = "linux"))'.dependencies]
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "658491c87be246740216f97cc367e29e31e690af" }
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "92c96b300517c49024a48aaff863338e3bd63616" }

# default-features = false drops notify-rust so macOS uses the native
# UNUserNotificationCenter backend (needs a signed .app to deliver).
[target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "658491c87be246740216f97cc367e29e31e690af", default-features = false }
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "92c96b300517c49024a48aaff863338e3bd63616", default-features = false }

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = { version = "2", optional = true }
Expand All @@ -139,14 +139,14 @@ libloading = "0.9"
zbus = "5"

[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "658491c87be246740216f97cc367e29e31e690af", features = [
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "92c96b300517c49024a48aaff863338e3bd63616", features = [
"push-notifications",
] }
tauri-plugin-edge-to-edge = { git = "https://github.com/SableClient/tauri-plugin-edge-to-edge.git", rev = "33c6116c27be28c06df5a9d02231ecc5fdeb93c5" }
tauri-plugin-sharekit = { git = "https://github.com/Choochmeque/tauri-plugin-sharekit", rev = "9f2b4c5d8a4f0ab910d900ba234ed8bae0ab854b" }
tauri-plugin-app-icon = { git = "https://github.com/SableClient/tauri-plugin-app-icon", rev = "bab80ae17f6d018702e472c539d0f02eba480c21" }
tauri-plugin-livekit-mobile = { git = "https://github.com/SableClient/tauri-plugin-livekit-mobile.git", rev = "c6f0d02ab3da1174535bc1dc8e598a2e6873f977" }
tauri-plugin-geolocation = "2.3.3"
tauri-plugin-geolocation = { version = "2.3.3", optional = true }

[target.'cfg(target_os = "ios")'.dependencies]
objc2 = "0.6"
Expand Down Expand Up @@ -198,7 +198,7 @@ tauri-plugin-android-fs = { version = "=29.0.0", features = ["legacy-storage-per
jni = "0.22"

[features]
default = ["wry", "updater", "matrix-crypto"]
default = ["wry", "updater", "matrix-crypto", "geolocation"]
custom-protocol = ["tauri/custom-protocol"]
wry = ["tauri/wry", "dep:webkit2gtk"]
# Tauri auto-updater. Disable with --no-default-features --features wry,cef.
Expand All @@ -209,6 +209,8 @@ cef = ["dep:tauri-runtime-cef", "dep:cef"]
# session, which costs hundreds of MB per minute once tauri-plugin-log feeds it.
devtools = ["dep:tauri-plugin-devtools"]
matrix-crypto = ["dep:matrix-sdk", "dep:matrix-sdk-crypto", "dep:matrix-sdk-sqlite", "dep:http"]
# Pulls play-services-location on Android. Off for FOSS-repo builds.
geolocation = ["dep:tauri-plugin-geolocation"]
[patch.crates-io]
tauri-typegen = { git = "https://github.com/SableClient/tauri-typegen", branch = "fix/nondeterministic-generation-cache" }

Expand Down
11 changes: 11 additions & 0 deletions src-tauri/capabilities/geolocation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "../gen/schemas/mobile-schema.json",
"identifier": "geolocation-capability",
"platforms": ["android", "iOS"],
"windows": ["main"],
"permissions": [
"geolocation:allow-check-permissions",
"geolocation:allow-request-permissions",
"geolocation:allow-get-current-position"
]
}
3 changes: 0 additions & 3 deletions src-tauri/capabilities/mobile.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
"app-icon:default",
"deep-link:default",
"edge-to-edge:default",
"geolocation:allow-check-permissions",
"geolocation:allow-request-permissions",
"geolocation:allow-get-current-position",
"livekit-mobile:default",
"notifications:default",
"sharekit:default"
Expand Down
8 changes: 7 additions & 1 deletion src-tauri/gen/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ plugins {
id("rust")
}

val fossBuild = providers.environmentVariable("SABLE_FOSS").orNull == "1"

val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
Expand All @@ -24,6 +26,8 @@ android {
targetSdk = 36
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
// Not an app flavour: that would change the output paths tauri-build.yml globs.
missingDimensionStrategy("push", if (fossBuild) "foss" else "gms")
}
signingConfigs {
create("release") {
Expand Down Expand Up @@ -105,6 +109,8 @@ apply(from = "tauri.build.gradle.kts")

// Native FCM push (Sygnal): applies only once google-services.json is added to this
// directory, so builds without Firebase configured still succeed.
if (file("google-services.json").exists()) {
// Skipped for FOSS builds: the plugin injects the Firebase project ids as string
// resources even when no Firebase library is linked.
if (!fossBuild && file("google-services.json").exists()) {
apply(plugin = "com.google.gms.google-services")
}
4 changes: 3 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,12 @@ pub fn run() {
let builder = builder
.plugin(tauri_plugin_app_icon::init())
.plugin(tauri_plugin_edge_to_edge::init())
.plugin(tauri_plugin_geolocation::init())
.plugin(tauri_plugin_sharekit::init())
.plugin(tauri_plugin_livekit_mobile::init());

#[cfg(all(mobile, feature = "geolocation"))]
let builder = builder.plugin(tauri_plugin_geolocation::init());

#[cfg(target_os = "android")]
let builder = builder.plugin(tauri_plugin_android_fs::init());

Expand Down
2 changes: 1 addition & 1 deletion src/app/features/room/location-modal/LocationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export enum LocationErrors {
none,
permissions = 'You have denied Sable access to you location services',
module = 'Your device does not have a gps module, or it may not be turned on',
unsupported = 'Location services are unavailable on desktop, enter or paste the coordinates instead',
unsupported = 'Location services are unavailable in this build, enter or paste the coordinates instead',
unknown = 'The sharing failed for unknown reasons',
clipboard = 'Unable to identify the coordinates from clipboard',
missingClipboard = 'Unable to retrieve clipboard contents',
Expand Down
4 changes: 2 additions & 2 deletions src/app/utils/geolocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
getCurrentPosition,
requestPermissions,
} from '@tauri-apps/plugin-geolocation';
import { isMobileTauri } from './platform';
import { isGeolocationEnabled, isMobileTauri } from './platform';

export type Coordinates = { lat: number; lon: number };

Expand Down Expand Up @@ -52,7 +52,7 @@ const getWebCoordinates = (): Promise<Coordinates> =>

export const getCurrentCoordinates = async (): Promise<Coordinates> => {
if (!isTauri()) return getWebCoordinates();
if (!isMobileTauri()) throw new GeolocationError('unsupported');
if (!isMobileTauri() || !isGeolocationEnabled()) throw new GeolocationError('unsupported');
try {
return await getNativeCoordinates();
} catch (err) {
Expand Down
4 changes: 4 additions & 0 deletions src/app/utils/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export function isDesktopUpdaterEnabled(): boolean {
return DESKTOP_UPDATER_ENABLED;
}

export function isGeolocationEnabled(): boolean {
return SABLE_GEOLOCATION_ENABLED;
}

export function isMobileTauri(): boolean {
const tauriOS = getTauriOS();
return tauriOS === 'ios' || tauriOS === 'android';
Expand Down
1 change: 1 addition & 0 deletions src/ext.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ declare const APP_VERSION: string;
declare const BUILD_HASH: string;
declare const IS_RELEASE_TAG: boolean;
declare const DESKTOP_UPDATER_ENABLED: boolean;
declare const SABLE_GEOLOCATION_ENABLED: boolean;

declare module 'browser-encrypt-attachment' {
export interface EncryptedAttachmentInfo {
Expand Down
3 changes: 3 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ const isTauriBuild = Boolean(process.env.TAURI_ENV_PLATFORM);
const isTauriDebug = process.env.TAURI_ENV_DEBUG === 'true';
const tauriBuildMinify = !isTauriDebug ? 'esbuild' : false;
const desktopUpdaterEnabled = process.env.VITE_DESKTOP_UPDATER_ENABLED !== 'false';
// FOSS builds drop tauri-plugin-geolocation, so its commands are not registered.
const geolocationEnabled = process.env.SABLE_FOSS !== '1';
const sentryUploadEnabled = Boolean(
process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_ORG && process.env.SENTRY_PROJECT
);
Expand Down Expand Up @@ -163,6 +165,7 @@ export default defineConfig(({ command }) => {
SABLE_PRODUCT_NAME: JSON.stringify(baseProductName),
SABLE_BUILD_FLAVOR: JSON.stringify(buildFlavor),
DESKTOP_UPDATER_ENABLED: JSON.stringify(desktopUpdaterEnabled),
SABLE_GEOLOCATION_ENABLED: JSON.stringify(geolocationEnabled),
},
resolve: {
alias: {
Expand Down
Loading