Compare commits

...

71 Commits

Author SHA1 Message Date
GitHub Actions
c9ac92a0c4 chore: bump version to 4.11.88 [skip ci] 2026-04-26 00:18:07 +00:00
cb44bc92e4 Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-android (push) Successful in 5m9s
Build / create-release (push) Successful in 13s
2026-04-26 10:17:58 +10:00
bc07974b4b feat: implement push ping receiver and one-shot sync fetch functionality
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 10:17:56 +10:00
GitHub Actions
abe44f6ad1 chore: bump version to 4.11.87 [skip ci] 2026-04-18 21:57:34 +00:00
c17e1c74ee chore: update subproject commit reference to a3dac17
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-android (push) Successful in 5m4s
Build / create-release (push) Successful in 14s
2026-04-19 07:57:24 +10:00
GitHub Actions
255bfe2262 chore: bump version to 4.11.86 [skip ci] 2026-04-18 21:37:27 +00:00
8eea325e21 Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-android (push) Successful in 5m46s
Build / create-release (push) Successful in 14s
2026-04-19 07:37:19 +10:00
3afb2ee80d chore: update subproject commit reference 2026-04-19 07:37:17 +10:00
GitHub Actions
245188c1a1 chore: bump version to 4.11.85 [skip ci] 2026-04-18 21:24:49 +00:00
1d2057d75a Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-android (push) Successful in 4m43s
Build / create-release (push) Successful in 13s
2026-04-19 07:24:41 +10:00
d8343d089b feat: implement Android app signing setup and update .gitignore for keystore files 2026-04-19 07:24:38 +10:00
GitHub Actions
5cc087330c chore: bump version to 4.11.84 [skip ci] 2026-04-18 21:09:32 +00:00
4163f22f71 Update README.md
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-android (push) Successful in 4m37s
Build / create-release (push) Successful in 14s
2026-04-19 07:09:24 +10:00
GitHub Actions
ed0761bd20 chore: bump version to 4.11.83 [skip ci] 2026-04-18 21:01:35 +00:00
6fdaa8dfd3 update base
All checks were successful
Build / increment-version (push) Successful in 7s
Build / build-android (push) Successful in 5m10s
Build / create-release (push) Successful in 15s
2026-04-19 07:00:49 +10:00
GitHub Actions
88d0a3f887 chore: bump version to 4.11.82 [skip ci] 2026-04-18 20:58:45 +00:00
240d39593e Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-android (push) Failing after 1m47s
Build / create-release (push) Has been skipped
2026-04-19 06:58:36 +10:00
f06d11711b fix: update subproject commit reference 2026-04-19 06:58:34 +10:00
GitHub Actions
75e1fe6812 chore: bump version to 4.11.81 [skip ci] 2026-04-18 20:54:19 +00:00
f8c1e0641d Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
Some checks failed
Build / increment-version (push) Successful in 5s
Build / build-android (push) Failing after 1m9s
Build / create-release (push) Has been skipped
2026-04-19 06:54:12 +10:00
6795e692d6 fix: update subproject commit reference 2026-04-19 06:54:10 +10:00
GitHub Actions
1c42974fe8 chore: bump version to 4.11.80 [skip ci] 2026-04-18 10:32:07 +00:00
2cde79e87d Update README.md
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-android (push) Successful in 4m43s
Build / create-release (push) Successful in 13s
2026-04-18 20:32:00 +10:00
GitHub Actions
6ec54c6018 chore: bump version to 4.11.79 [skip ci] 2026-04-17 10:16:12 +00:00
e203a46b5a fix: move capacitor dependencies to root package.json
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-android (push) Successful in 4m34s
Build / create-release (push) Successful in 14s
Capacitor CLI and Android libraries need to be in root node_modules since capacitor.config.json and android/ folder are at root. The overlay no longer needs to add these deps to cinny.
2026-04-17 20:16:04 +10:00
GitHub Actions
746b7e6166 chore: bump version to 4.11.78 [skip ci] 2026-04-17 10:10:03 +00:00
0d089d0244 fix: run cap sync from cinny directory where capacitor cli is installed
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-android (push) Failing after 3m3s
Build / create-release (push) Has been skipped
2026-04-17 20:09:54 +10:00
GitHub Actions
1302bead0e chore: bump version to 4.11.77 [skip ci] 2026-04-17 10:06:13 +00:00
875c4d3b54 ci: use npm install for cinny deps after overlay modifies package.json
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-android (push) Failing after 1m42s
Build / create-release (push) Has been skipped
2026-04-17 20:06:05 +10:00
GitHub Actions
bc3de084d2 chore: bump version to 4.11.76 [skip ci] 2026-04-17 10:03:23 +00:00
849f4b980d ci: apply overlay before installing dependencies, fix cap sync path
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-android (push) Failing after 49s
Build / create-release (push) Has been skipped
2026-04-17 20:03:14 +10:00
GitHub Actions
f7b79b3e20 chore: bump version to 4.11.75 [skip ci] 2026-04-17 09:59:37 +00:00
149ebfbe1f ci: install cinny dependencies before build
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-android (push) Failing after 1m38s
Build / create-release (push) Has been skipped
2026-04-17 19:59:27 +10:00
GitHub Actions
684b74a890 chore: bump version to 4.11.74 [skip ci] 2026-04-17 09:58:06 +00:00
b2538670cd feat: add android:apk:linux script for CI builds
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-android (push) Failing after 41s
Build / create-release (push) Has been skipped
2026-04-17 19:57:58 +10:00
GitHub Actions
da9f19e331 chore: bump version to 4.11.73 [skip ci] 2026-04-17 09:55:59 +00:00
91bf4d6053 ci: trigger build with conflict fix
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-android (push) Failing after 40s
Build / create-release (push) Has been skipped
2026-04-17 19:55:51 +10:00
6ef5fcc2ed fix: resolve package.json merge conflict [skip ci] 2026-04-17 19:51:22 +10:00
GitHub Actions
1f74a133a5 chore: bump version to 4.11.72 [skip ci] 2026-04-17 09:49:26 +00:00
61946d5842 ci: restructure workflow to match desktop pattern with separate build and release jobs
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-android (push) Failing after 38s
Build / create-release (push) Has been skipped
2026-04-17 19:49:17 +10:00
GitHub Actions
e634217d81 chore: bump version to 4.11.71 [skip ci] 2026-04-17 09:45:29 +00:00
84ab4a76dd ci: trigger build to test workflow changes
Some checks failed
Build / increment-version (push) Successful in 7s
Build / build-and-release (push) Failing after 1m16s
2026-04-17 19:45:21 +10:00
dc5a416949 ci: use npm ci, use upload_url from github api for releases [skip ci] 2026-04-17 19:43:36 +10:00
1bfb18ba86 ci: retry push with rebase to prevent version bump race condition [skip ci] 2026-04-17 19:41:29 +10:00
GitHub Actions
49cc17d105 chore: bump version to 4.11.70 [skip ci] 2026-04-17 09:39:50 +00:00
174d62e68a chore: remove outdated GitHub templates and configuration files
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-and-release (push) Failing after 38s
2026-04-17 19:39:41 +10:00
GitHub Actions
0d61eed963 chore: bump version to 4.11.69 [skip ci] 2026-04-17 09:38:24 +00:00
4f58736f7f ci: add clean workflow
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-and-release (push) Has been cancelled
2026-04-17 19:38:17 +10:00
GitHub Actions
f6cf5696dd chore: bump version to 4.11.68 [skip ci] 2026-04-17 09:28:19 +00:00
2ca09bbdf0 Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
Some checks failed
Build / increment-version (push) Failing after 6s
Build / build-and-release (push) Failing after 58s
2026-04-17 19:28:10 +10:00
c39a0ae248 feat: add GitHub release creation and asset upload to build process 2026-04-17 19:28:08 +10:00
GitHub Actions
1e848bc622 chore: bump version to 4.11.67 [skip ci] 2026-04-15 12:14:35 +00:00
3fe00eb3d7 Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
Some checks failed
Build / increment-version (push) Successful in 7s
Build / build-and-release (push) Failing after 51s
2026-04-15 22:14:24 +10:00
85851d7288 fix: update version to 4.11.66 and streamline Android build scripts 2026-04-15 22:14:14 +10:00
GitHub Actions
47f1b2c40f chore: bump version to 4.11.83 [skip ci] 2026-04-07 12:48:26 +00:00
d04c1e8f77 fix: rename build job and streamline APK upload process
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-and-release (push) Successful in 4m45s
2026-04-07 22:48:17 +10:00
GitHub Actions
76a19f7e02 chore: bump version to 4.11.82 [skip ci] 2026-04-07 10:57:10 +00:00
3d35ea5930 Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
Some checks failed
Build / increment-version (push) Successful in 5s
Build / build-android (push) Failing after 4m37s
Build / create-release (push) Has been skipped
2026-04-07 20:57:03 +10:00
9fe79de3b5 fix: update upload artifact action to v4 and adjust APK naming convention 2026-04-07 20:57:02 +10:00
GitHub Actions
044631b05d chore: bump version to 4.11.81 [skip ci] 2026-04-07 10:45:54 +00:00
4bc5b4d790 Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
Some checks failed
Build / increment-version (push) Successful in 7s
Build / build-android (push) Successful in 5m11s
Build / create-release (push) Failing after 3m7s
2026-04-07 20:45:46 +10:00
da52ec84b7 fix: improve handling of joined rooms and event notifications in MatrixSyncService 2026-04-07 20:45:44 +10:00
GitHub Actions
f758796d83 chore: bump version to 4.11.80 [skip ci] 2026-04-07 10:42:53 +00:00
77d4071728 Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
Some checks failed
Build / increment-version (push) Successful in 6s
Build / create-release (push) Has been cancelled
Build / build-android (push) Has been cancelled
2026-04-07 20:42:44 +10:00
ca855a6945 feat: sync version from package.json into build.gradle and set APK output name 2026-04-07 20:42:43 +10:00
GitHub Actions
a56662ab69 chore: bump version to 4.11.79 [skip ci] 2026-04-07 10:36:35 +00:00
f870fd1c2a Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-android (push) Failing after 4m21s
Build / create-release (push) Has been skipped
2026-04-07 20:36:27 +10:00
70f7d41d9a feat: implement background sync service for Matrix integration 2026-04-07 20:36:25 +10:00
GitHub Actions
eba647c9c5 chore: bump version to 4.11.78 [skip ci] 2026-04-07 09:42:27 +00:00
08b6dce4ee Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-android (push) Successful in 4m31s
Build / create-release (push) Successful in 8s
2026-04-07 19:42:19 +10:00
226f4863ff feat: add POST_NOTIFICATIONS permission to AndroidManifest.xml 2026-04-07 19:42:17 +10:00
33 changed files with 1913 additions and 372 deletions

View File

@@ -15,30 +15,34 @@ jobs:
outputs: outputs:
version: ${{ steps.bump.outputs.VERSION }} version: ${{ steps.bump.outputs.VERSION }}
should_build: ${{ steps.bump.outputs.SHOULD_BUILD }} should_build: ${{ steps.bump.outputs.SHOULD_BUILD }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Bump patch version - name: Bump patch version
id: bump id: bump
run: | run: |
CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1) CURRENT=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
echo "Current version: $CURRENT" echo "Current version: $CURRENT"
MAJOR=$(echo $CURRENT | cut -d. -f1) MAJOR=$(echo $CURRENT | cut -d. -f1)
MINOR=$(echo $CURRENT | cut -d. -f2) MINOR=$(echo $CURRENT | cut -d. -f2)
PATCH=$(echo $CURRENT | cut -d. -f3) PATCH=$(echo $CURRENT | cut -d. -f3)
NEW_PATCH=$((PATCH + 1)) NEW_PATCH=$((PATCH + 1))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
echo "New version: $NEW_VERSION" echo "New version: $NEW_VERSION"
sed -i "s/\"version\": \"${CURRENT}\"/\"version\": \"${NEW_VERSION}\"/" package.json sed -i "s/\"version\": \"${CURRENT}\"/\"version\": \"${NEW_VERSION}\"/" package.json
grep '"version"' package.json | head -1 grep '"version"' package.json | head -1
echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "SHOULD_BUILD=true" >> $GITHUB_OUTPUT echo "SHOULD_BUILD=true" >> $GITHUB_OUTPUT
- name: Commit and push version bump - name: Commit and push version bump
run: | run: |
git config user.name "GitHub Actions" git config user.name "GitHub Actions"
@@ -51,69 +55,76 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: increment-version needs: increment-version
if: always() && (needs.increment-version.outputs.should_build == 'true' || needs.increment-version.result == 'skipped') if: always() && (needs.increment-version.outputs.should_build == 'true' || needs.increment-version.result == 'skipped')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: false submodules: false
ref: ${{ github.ref_name }} ref: ${{ github.ref_name }}
- name: Checkout submodules manually - name: Checkout submodules manually
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Pull latest (after version bump) - name: Pull latest (after version bump)
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
run: git pull origin master run: git pull origin master
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '22' node-version: '22'
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: '21' java-version: '21'
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
- name: Install dependencies (root) - name: Install dependencies (root)
run: npm ci --prefer-offline
- name: Apply overlay
run: npm run apply-overlay
- name: Install dependencies (cinny)
working-directory: ./cinny
run: npm install --prefer-offline run: npm install --prefer-offline
- name: Clean previous cinny dist - name: Clean previous builds
run: rm -rf cinny/dist run: rm -rf cinny/dist
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x android/gradlew run: chmod +x android/gradlew
- name: Apply overlay and build Android APK - name: Build Android APK
run: npm run android:apk:linux run: npm run android:apk:linux
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: Paarrot-Android-debug.apk name: Paarrot-Android.apk
path: android/app/build/outputs/apk/debug/app-debug.apk path: android/app/build/outputs/apk/debug/Paarrot-*.apk
create-release: create-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [increment-version, build-android] needs: [increment-version, build-android]
if: always() && needs.build-android.result == 'success' if: always() && needs.build-android.result == 'success'
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.ref_name }} ref: ${{ github.ref_name }}
- name: Pull latest (after version bump) - name: Pull latest (after version bump)
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
run: git pull origin master run: git pull origin master
- name: Get version - name: Get version
id: version id: version
run: | run: |
@@ -124,17 +135,19 @@ jobs:
fi fi
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION" echo "Version: $VERSION"
- name: Check if tag exists - name: Check if tag exists
id: tag_check id: tag_check
run: | run: |
VERSION="${{ steps.version.outputs.VERSION }}" VERSION="${{ steps.version.outputs.VERSION }}"
if git rev-parse "v$VERSION" >/dev/null 2>&1; then if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "Tag v$VERSION already exists"
echo "exists=true" >> $GITHUB_OUTPUT echo "exists=true" >> $GITHUB_OUTPUT
else else
echo "Tag v$VERSION does not exist"
echo "exists=false" >> $GITHUB_OUTPUT echo "exists=false" >> $GITHUB_OUTPUT
fi fi
- name: Create and push tag - name: Create and push tag
if: steps.tag_check.outputs.exists == 'false' if: steps.tag_check.outputs.exists == 'false'
run: | run: |
@@ -142,23 +155,19 @@ jobs:
git config user.email "actions@github.com" git config user.email "actions@github.com"
git tag -a "v${{ steps.version.outputs.VERSION }}" -m "Release v${{ steps.version.outputs.VERSION }}" git tag -a "v${{ steps.version.outputs.VERSION }}" -m "Release v${{ steps.version.outputs.VERSION }}"
git push origin "v${{ steps.version.outputs.VERSION }}" git push origin "v${{ steps.version.outputs.VERSION }}"
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
path: artifacts path: artifacts
- name: Prepare release files - name: Prepare release files
run: | run: |
mkdir -p release-files mkdir -p release-files
find artifacts -type f \( -name "*.exe" -o -name "*.AppImage" -o -name "*.nupkg" -o -name "RELEASES*" \) -exec cp {} release-files/ \; find artifacts -type f -name "*.apk" -exec cp {} release-files/ \;
APK=$(find artifacts -type f -name "*.apk" | head -1)
if [ -n "$APK" ]; then
cp "$APK" "release-files/Paarrot.apk"
fi
ls -la release-files/ ls -la release-files/
- name: Create or Update Release - name: Create or Update Gitea Release
env: env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
@@ -166,10 +175,10 @@ jobs:
TAG_NAME="v${VERSION}" TAG_NAME="v${VERSION}"
API_BASE="http://synbox.ruv.wtf:8418/api/v1" API_BASE="http://synbox.ruv.wtf:8418/api/v1"
REPO="litruv/cinny-mobile" REPO="litruv/cinny-mobile"
RELEASE_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \ RELEASE_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty') "${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
if [ -z "$RELEASE_ID" ]; then if [ -z "$RELEASE_ID" ]; then
echo "Creating new release for ${TAG_NAME}..." echo "Creating new release for ${TAG_NAME}..."
RELEASE_ID=$(curl -s -X POST \ RELEASE_ID=$(curl -s -X POST \
@@ -179,18 +188,20 @@ jobs:
"${API_BASE}/repos/${REPO}/releases" | jq -r '.id') "${API_BASE}/repos/${REPO}/releases" | jq -r '.id')
echo "Created release with ID: ${RELEASE_ID}" echo "Created release with ID: ${RELEASE_ID}"
else else
echo "Release exists with ID: ${RELEASE_ID}, deleting existing assets..." echo "Release exists with ID: ${RELEASE_ID}"
echo "Deleting existing assets..."
ASSETS=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \ ASSETS=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets" | jq -r '.[].id') "${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets" | jq -r '.[].id')
for ASSET_ID in $ASSETS; do for ASSET_ID in $ASSETS; do
echo "Deleting asset ${ASSET_ID}..."
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \ curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets/${ASSET_ID}" "${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
done done
fi fi
echo "Uploading assets..." echo "Uploading assets..."
for FILE in release-files/*; do for FILE in release-files/*; do
FILENAME=$(basename "$FILE") FILENAME="Paarrot-${VERSION}.apk"
echo "Uploading ${FILENAME}..." echo "Uploading ${FILENAME}..."
curl -s -X POST \ curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
@@ -199,5 +210,45 @@ jobs:
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}" "${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
echo "" echo ""
done done
echo "Release complete!" echo "Gitea release complete!"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GHTOKEN }}
run: |
VERSION="${{ steps.version.outputs.VERSION }}"
TAG_NAME="v${VERSION}"
GITHUB_REPO="Paarrot/Paarrot-Mobile"
RELEASE_ID=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
if [ -z "$RELEASE_ID" ]; then
echo "Creating new GitHub release for ${TAG_NAME}..."
RELEASE_ID=$(curl -s -X POST \
-H "Authorization: token ${GH_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r '.id')
echo "Created GitHub release with ID: ${RELEASE_ID}"
else
echo "GitHub release exists with ID: ${RELEASE_ID}"
fi
echo "Uploading assets to GitHub..."
UPLOAD_URL=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/${RELEASE_ID}" | jq -r '.upload_url' | sed 's/{?name,label}//')
for FILE in release-files/*; do
FILENAME="Paarrot-${VERSION}.apk"
echo "Uploading ${FILENAME} to GitHub..."
curl -s -X POST \
-H "Authorization: token ${GH_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${FILE}" \
"${UPLOAD_URL}?name=${FILENAME}"
echo ""
done
echo "GitHub release complete!"

View File

@@ -0,0 +1,30 @@
name: Clean
on:
workflow_dispatch:
jobs:
clean:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: false
- name: Clean build artifacts
run: |
echo "Cleaning cinny dist..."
rm -rf cinny/dist
echo "Cleaning Android build outputs..."
rm -rf android/app/build
echo "Cleaning root node_modules..."
rm -rf node_modules
echo "Cleaning cinny node_modules..."
rm -rf cinny/node_modules
echo "Done."

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: ajbura
liberapay: ajbura
open_collective: cinny

View File

@@ -1,58 +0,0 @@
name: 🐞 Bug Report
description: Report a bug
labels: 'type: bug'
body:
- type: markdown
attributes:
value: |
## First of all
1. Please search for [existing issues](https://github.com/cinnyapp/cinny-desktop/issues?q=is%3Aissue) about this problem first.
2. Make sure Cinny is up to date.
3. Make sure it's an issue with Cinny and not something else you are using.
4. Remember to be friendly.
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear description of what the bug is. Include screenshots if applicable.
placeholder: Bug description
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Steps to reproduce the behavior.
placeholder: |
1. Go to ...
2. Click on ...
3. See error
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear description of what you expected to happen.
- type: textarea
id: info
attributes:
label: Platform and versions
description: "Provide OS, browser and Cinny version with your Homeserver."
placeholder: |
1. OS: [e.g. Windows 10, MacOS]
2. Cinny version: [e.g. 1.8.1]
3. Matrix homeserver: [e.g. matrix.org]
4. Downloaded from: [e.g. GitHub, Flatpak]
render: shell
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.

View File

@@ -1,4 +0,0 @@
contact_links:
- name: 💬 Matrix Chat
url: https://matrix.to/#/#cinny:matrix.org
about: Ask questions and talk to other Cinny users and the maintainers

View File

@@ -1,34 +0,0 @@
name: 💡 Feature Request
description: Suggest an idea
labels: 'type: feature'
body:
- type: textarea
id: problem
attributes:
label: Describe the problem
description: A clear description of the problem this feature would solve
placeholder: "I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Describe the solution you'd like"
description: A clear description of what change you would like
placeholder: "I would like to..."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: "Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the problem here.

View File

@@ -1,22 +0,0 @@
<!-- Please read https://github.com/cinnyapp/cinny/blob/dev/CONTRIBUTING.md before submitting your pull request -->
### Description
<!-- Please include a summary of the change. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes #
#### Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
### Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings

3
.github/SECURITY.md vendored
View File

@@ -1,3 +0,0 @@
# Reporting a Vulnerability
**If you've found a security vulnerability, please report it to cinnyapp@gmail.com**

View File

@@ -1,30 +0,0 @@
# Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates>
version: 2
updates:
# - package-ecosystem: npm
# directory: /
# schedule:
# interval: weekly
# day: "tuesday"
# time: "01:00"
# timezone: "Asia/Kolkata"
# open-pull-requests-limit: 15
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
# - package-ecosystem: cargo
# directory: /src-tauri/
# schedule:
# interval: weekly
# day: "tuesday"
# time: "01:00"
# timezone: "Asia/Kolkata"
# open-pull-requests-limit: 5

21
.github/renovate.json vendored
View File

@@ -1,21 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboardApproval"
],
"labels": [
"Dependencies"
],
"packageRules": [
{
"matchUpdateTypes": [
"lockFileMaintenance"
]
}
],
"lockFileMaintenance": {
"enabled": true
},
"dependencyDashboard": true
}

View File

@@ -1,22 +0,0 @@
name: "Upload zip-archive"
on:
release:
types: [published]
jobs:
zip-archive:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
with:
submodules: true
- name: Create zip including submodules
run: |
cd ..
zip ${{ github.event.repository.name }}/${{ github.event.repository.name }}-${{ github.ref_name }}.zip ${{ github.event.repository.name }} -r
- name: Upload zip to release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
with:
files: |
${{ github.event.repository.name }}-${{ github.ref_name }}.zip

View File

@@ -1,36 +0,0 @@
name: 'CLA Assistant'
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened, closed, synchronize]
jobs:
CLAssistant:
runs-on: ubuntu-latest
steps:
- name: 'CLA Assistant'
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: cla-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}
with:
path-to-signatures: 'signatures.json'
path-to-document: 'https://github.com/cinnyapp/cla/blob/main/cla.md' # e.g. a CLA or a DCO document
# branch should not be protected
branch: 'main'
allowlist: ajbura,bot*
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
remote-organization-name: cinnyapp
remote-repository-name: cla
#create-file-commit-message: 'For example: Creating file for storing CLA Signatures'
#signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo'
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
#use-dco-flag: true - If you are using DCO instead of CLA

View File

@@ -1,26 +0,0 @@
name: NPM Lockfile Changes
on:
pull_request:
paths:
- 'package-lock.json'
jobs:
lockfile_changes:
runs-on: ubuntu-latest
# Permission overwrite is required for Dependabot PRs, see "Common issues" below.
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4.2.0
- name: NPM Lockfile Changes
uses: codepunkt/npm-lockfile-changes@b40543471c36394409466fdb277a73a0856d7891
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Optional inputs, can be deleted safely if you are happy with default values.
collapsibleThreshold: 25
failOnDowngrade: false
path: package-lock.json
updateComment: true

View File

@@ -1,40 +0,0 @@
name: "Build pull request"
on:
#pull_request:
#types: ['opened', 'synchronize']
jobs:
publish-tauri:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-20.04, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.0
with:
submodules: true
- name: Setup node
uses: actions/setup-node@v4.4.0
with:
node-version: 20.12.2
cache: 'npm'
- name: Install Rust stable
uses: actions-rs/toolchain@v1.0.7
with:
toolchain: stable
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Install cinny dependencies
run: cd cinny && npm ci
- name: Install tauri dependencies
run: npm ci
- name: Build desktop app with Tauri
uses: tauri-apps/tauri-action@v0.5.14
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,7 +1,7 @@
# Paarrot # Paarrot
Paarrot is a Matrix client focusing primarily on simple, elegant and secure interface. The desktop app is built with Electron and based on Cinny. Paarrot is a Matrix client focusing primarily on simple, elegant and secure interface. The desktop app is built with Electron and based on Cinny.
## Download ## Download
Installers for Windows and Linux can be downloaded from [releases](http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases). Installers for Windows and Linux can be downloaded from [releases](http://synbox.ruv.wtf:8418/litruv/cinny-desktop/releases).

8
android/.gitignore vendored
View File

@@ -52,10 +52,10 @@ captures/
# Comment next line if keeping position of elements in Navigation Editor is relevant for you # Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml .idea/navEditor.xml
# Keystore files # Keystore files - NEVER commit these!
# Uncomment the following lines if you do not want to check your keystore files in. *.jks
#*.jks *.keystore
#*.keystore keystore.properties
# External native build folder generated in Android Studio 2.2 and later # External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild .externalNativeBuild

61
android/SIGNING_SETUP.md Normal file
View File

@@ -0,0 +1,61 @@
# Android App Signing Setup
## Why This Matters
Android requires apps to be signed with the same key for updates to work. Without proper signing, users cannot update your app - they must uninstall and reinstall, losing all data.
## Setup Instructions
### 1. Create a Keystore (if you don't have one)
Run this command in the `android/` directory:
```bash
keytool -genkey -v -keystore paarrot-release.keystore -alias paarrot -keyalg RSA -keysize 2048 -validity 10000
```
You'll be asked for:
- Keystore password (remember this!)
- Key password (remember this!)
- Your name, organization, etc.
**IMPORTANT**: Back up this keystore file and remember the passwords! If you lose either, you cannot update your app.
### 2. Create keystore.properties
Copy the example file:
```bash
cp keystore.properties.example keystore.properties
```
Then edit `keystore.properties` and fill in:
- `storeFile`: path to your keystore (e.g., `paarrot-release.keystore`)
- `storePassword`: the keystore password you set
- `keyAlias`: the alias (e.g., `paarrot`)
- `keyPassword`: the key password you set
### 3. Verify
The file `keystore.properties` should be gitignored (check `.gitignore`).
### 4. Build
Now your release builds will be properly signed:
```bash
cd android
./gradlew assembleRelease
```
The signed APK will be in: `app/build/outputs/apk/release/`
## Troubleshooting
### "Updates not installing"
- You're using a different keystore than the original APK
- Solution: Use the same keystore that was used for the first release
### "keystore.properties not found"
- Normal for debug builds
- Only needed for release builds
- Create the file following step 2 above

View File

@@ -1,8 +1,28 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
// Load keystore properties
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android { android {
namespace = "com.paarrot.app" namespace = "com.paarrot.app"
compileSdk = rootProject.ext.compileSdkVersion compileSdk = rootProject.ext.compileSdkVersion
signingConfigs {
release {
if (keystorePropertiesFile.exists()) {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
}
}
}
defaultConfig { defaultConfig {
applicationId "com.paarrot.app" applicationId "com.paarrot.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
@@ -20,6 +40,14 @@ android {
release { release {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
if (keystorePropertiesFile.exists()) {
signingConfig signingConfigs.release
}
}
}
applicationVariants.all { variant ->
variant.outputs.all { output ->
outputFileName = "Paarrot-${variant.versionName}.apk"
} }
} }
} }
@@ -36,6 +64,7 @@ dependencies {
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android') implementation project(':capacitor-android')
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"

View File

@@ -33,11 +33,38 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data> android:resource="@xml/file_paths"></meta-data>
</provider> </provider>
<service
android:name=".MatrixSyncService"
android:foregroundServiceType="dataSync"
android:exported="false"
android:stopWithTask="false" />
<receiver
android:name=".BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".PushPingReceiver"
android:exported="true">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
<action android:name="com.paarrot.app.ACTION_PUSH_PING" />
</intent-filter>
</receiver>
</application> </application>
<!-- Permissions --> <!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

View File

@@ -0,0 +1,17 @@
package com.paarrot.app
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
/**
* Triggers a one-shot sync fetch after boot if credentials exist.
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
MatrixSyncService.requestSyncFetch(context, "boot_completed")
}
}

View File

@@ -1,5 +1,12 @@
package com.paarrot.app; package com.paarrot.app;
import android.os.Bundle;
import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {} public class MainActivity extends BridgeActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
registerPlugin(SyncServicePlugin.class);
super.onCreate(savedInstanceState);
}
}

View File

@@ -0,0 +1,288 @@
package com.paarrot.app
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
/**
* ForegroundService that performs a short, one-shot Matrix /sync fetch.
*
* This service is wake-triggered by push pings (UnifiedPush/FCM bridge) and
* then fetches message content from Matrix before posting local notifications.
*/
class MatrixSyncService : Service() {
private val job = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.IO + job)
/** Event IDs already shown as notifications this session — prevents duplicates on restart. */
private val shownEventIds = HashSet<String>(64)
override fun onBind(intent: Intent?) = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val prefs = applicationContext.getSharedPreferences(SyncServicePlugin.PREFS, Context.MODE_PRIVATE)
val homeserver = intent?.getStringExtra(EXTRA_HOMESERVER)
?: prefs.getString(EXTRA_HOMESERVER, null)
val token = intent?.getStringExtra(EXTRA_TOKEN)
?: prefs.getString(EXTRA_TOKEN, null)
val userId = intent?.getStringExtra(EXTRA_USER_ID)
?: prefs.getString(EXTRA_USER_ID, null) ?: ""
if (homeserver == null || token == null) {
stopSelf()
return START_NOT_STICKY
}
val triggerReason = intent?.getStringExtra(EXTRA_TRIGGER_REASON) ?: "unknown"
startForeground(NOTIF_ID_STATUS, buildStatusNotification())
serviceScope.launch {
try {
runSingleSyncFetch(homeserver, token, userId)
Log.d(TAG, "One-shot sync completed (reason=$triggerReason)")
} finally {
stopForegroundCompat()
stopSelf(startId)
}
}
return START_NOT_STICKY
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
private suspend fun runSingleSyncFetch(homeserver: String, token: String, userId: String) {
val prefs = applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val since = prefs.getString(KEY_SINCE, null)
val isFirstSync = since == null
try {
val url = buildSyncUrl(homeserver.trimEnd('/'), since)
val (responseCode, body) = doHttpGet(url, token)
when (responseCode) {
200 -> {
val json = JSONObject(body ?: "{}")
val nextBatch = json.optString("next_batch").takeIf { it.isNotBlank() }
if (nextBatch != null) {
prefs.edit().putString(KEY_SINCE, nextBatch).apply()
if (!isFirstSync && !appInForeground) {
processRoomEvents(json, userId)
}
}
}
401, 403 -> {
Log.w(TAG, "Auth error $responseCode — clearing credentials")
applicationContext
.getSharedPreferences(SyncServicePlugin.PREFS, Context.MODE_PRIVATE)
.edit()
.clear()
.apply()
}
else -> {
Log.w(TAG, "Sync fetch failed with HTTP $responseCode")
}
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.w(TAG, "Sync error: ${e.message}")
}
}
private fun buildSyncUrl(base: String, since: String?): String {
val filter = """{"room":{"timeline":{"limit":10,"types":["m.room.message"]},"state":{"types":[]},"account_data":{"types":[]},"ephemeral":{"types":[]}},"account_data":{"types":[]},"presence":{"types":[]}}"""
val encodedFilter = URLEncoder.encode(filter, "UTF-8")
val sinceParam = if (since != null) "&since=${URLEncoder.encode(since, "UTF-8")}" else ""
return "$base/_matrix/client/v3/sync?timeout=12000&filter=$encodedFilter$sinceParam"
}
private suspend fun doHttpGet(urlString: String, token: String): Pair<Int, String?> =
withContext(Dispatchers.IO) {
val conn = URL(urlString).openConnection() as HttpURLConnection
try {
conn.requestMethod = "GET"
conn.setRequestProperty("Authorization", "Bearer $token")
conn.setRequestProperty("Accept", "application/json")
conn.connectTimeout = 5_000
conn.readTimeout = 35_000
val code = conn.responseCode
val body = if (code == 200) conn.inputStream.bufferedReader().readText() else null
Pair(code, body)
} catch (e: Exception) {
Pair(-1, null)
} finally {
conn.disconnect()
}
}
private fun processRoomEvents(sync: JSONObject, myUserId: String) {
val joinedRooms = sync.optJSONObject("rooms")?.optJSONObject("join") ?: return
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
ensureMessageChannel(nm)
val roomIds = joinedRooms.keys().asSequence().toList()
for (roomId in roomIds) {
val timeline = joinedRooms.optJSONObject(roomId)
?.optJSONObject("timeline") ?: continue
val events = timeline.optJSONArray("events") ?: continue
for (i in 0 until events.length()) {
val event = events.optJSONObject(i) ?: continue
val eventId = event.optString("event_id")
if (event.optString("type") != "m.room.message") continue
if (event.optString("sender") == myUserId) continue
if (eventId.isNotBlank() && !shownEventIds.add(eventId)) continue
val content = event.optJSONObject("content") ?: continue
val body = content.optString("body").takeIf { it.isNotBlank() } ?: continue
val sender = event.optString("sender")
val senderName = sender.substringAfter("@").substringBefore(":")
showMessageNotification(nm, senderName, body)
}
}
}
private fun showMessageNotification(nm: NotificationManager, sender: String, body: String) {
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
?: Intent(this, MainActivity::class.java)
val pi = PendingIntent.getActivity(
this, 0, launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val notification = NotificationCompat.Builder(this, CHANNEL_MESSAGES)
.setSmallIcon(R.drawable.ic_stat_paarrot)
.setContentTitle(sender)
.setContentText(body)
.setAutoCancel(true)
.setContentIntent(pi)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
nm.notify(System.currentTimeMillis().toInt(), notification)
}
private fun buildStatusNotification(): Notification {
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_STATUS, "Sync Status",
NotificationManager.IMPORTANCE_MIN,
).apply {
description = "Paarrot background sync status"
setShowBadge(false)
}
nm.createNotificationChannel(channel)
}
return NotificationCompat.Builder(this, CHANNEL_STATUS)
.setSmallIcon(R.drawable.ic_stat_paarrot)
.setContentTitle("Paarrot")
.setContentText("Checking for new messages")
.setPriority(NotificationCompat.PRIORITY_MIN)
.setOngoing(true)
.build()
}
private fun stopForegroundCompat() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION")
stopForeground(true)
}
}
private fun ensureMessageChannel(nm: NotificationManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_MESSAGES, "Messages",
NotificationManager.IMPORTANCE_HIGH,
).apply { description = "Matrix message notifications" }
nm.createNotificationChannel(channel)
}
}
companion object {
private const val TAG = "MatrixSyncService"
const val EXTRA_HOMESERVER = "homeserver_url"
const val EXTRA_TOKEN = "access_token"
const val EXTRA_USER_ID = "user_id"
const val EXTRA_DEVICE_ID = "device_id"
const val EXTRA_TRIGGER_REASON = "trigger_reason"
const val PREFS = "matrix_sync_prefs"
const val KEY_SINCE = "since_token"
private const val KEY_LAST_WAKE_MS = "last_wake_ms"
private const val NOTIF_ID_STATUS = 1001
private const val CHANNEL_STATUS = "sync_status"
private const val CHANNEL_MESSAGES = "messages"
private const val MIN_WAKE_INTERVAL_MS = 7_500L
const val ACTION_PUSH_PING = "com.paarrot.app.ACTION_PUSH_PING"
/** Set by [SyncServicePlugin] — true when the Capacitor WebView UI is visible. */
@Volatile
var appInForeground = false
/**
* Starts a one-shot sync fetch if credentials are available and the call is not rate-limited.
*/
fun requestSyncFetch(context: Context, reason: String) {
val credsPrefs = context.getSharedPreferences(SyncServicePlugin.PREFS, Context.MODE_PRIVATE)
val homeserver = credsPrefs.getString(EXTRA_HOMESERVER, null) ?: return
val token = credsPrefs.getString(EXTRA_TOKEN, null) ?: return
val userId = credsPrefs.getString(EXTRA_USER_ID, null) ?: ""
val deviceId = credsPrefs.getString(EXTRA_DEVICE_ID, null) ?: ""
val syncPrefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val now = System.currentTimeMillis()
val lastWake = syncPrefs.getLong(KEY_LAST_WAKE_MS, 0L)
if (now - lastWake < MIN_WAKE_INTERVAL_MS) {
Log.d(TAG, "Skipping wake: rate-limited ($reason)")
return
}
syncPrefs.edit().putLong(KEY_LAST_WAKE_MS, now).apply()
val intent = Intent(context, MatrixSyncService::class.java).apply {
putExtra(EXTRA_HOMESERVER, homeserver)
putExtra(EXTRA_TOKEN, token)
putExtra(EXTRA_USER_ID, userId)
putExtra(EXTRA_DEVICE_ID, deviceId)
putExtra(EXTRA_TRIGGER_REASON, reason)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}
}

View File

@@ -0,0 +1,25 @@
package com.paarrot.app
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
/**
* Receives tiny push wake pings and triggers one Matrix fetch.
*
* - `org.unifiedpush.android.connector.MESSAGE` supports UnifiedPush distributors.
* - [MatrixSyncService.ACTION_PUSH_PING] is an app-local fallback action
* that other bridges (e.g., FCM service) can emit.
*/
class PushPingReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val reason = when (intent.action) {
"org.unifiedpush.android.connector.MESSAGE" -> "unifiedpush_ping"
MatrixSyncService.ACTION_PUSH_PING -> "app_push_ping"
else -> return
}
MatrixSyncService.requestSyncFetch(context, reason)
}
}

View File

@@ -0,0 +1,85 @@
package com.paarrot.app
import android.content.Context
import android.content.Intent
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
/**
* Capacitor plugin that controls [MatrixSyncService] from JS.
*
* JS API:
* - `start({ homeserverUrl, accessToken, userId, deviceId })` — persist/update sync credentials
* - `triggerPing({ reason })` — force a one-shot fetch (for testing/manual wake)
* - `stop()` — clear persisted credentials and stop any in-flight fetch
* - `setAppForeground({ foreground })` — tell the service whether the app UI is visible
* - `getStatus()` — returns `{ running: boolean }`
*/
@CapacitorPlugin(name = "MatrixBackgroundSync")
class SyncServicePlugin : Plugin() {
/** Persists Matrix credentials used later by push-triggered sync fetches. */
@PluginMethod
fun start(call: PluginCall) {
val homeserver = call.getString("homeserverUrl")
?: return call.reject("homeserverUrl required")
val token = call.getString("accessToken")
?: return call.reject("accessToken required")
val userId = call.getString("userId") ?: ""
val deviceId = call.getString("deviceId") ?: ""
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
.putString(MatrixSyncService.EXTRA_HOMESERVER, homeserver)
.putString(MatrixSyncService.EXTRA_TOKEN, token)
.putString(MatrixSyncService.EXTRA_USER_ID, userId)
.putString(MatrixSyncService.EXTRA_DEVICE_ID, deviceId)
.apply()
call.resolve()
}
/** Manually triggers a one-shot sync fetch (mainly for diagnostics/testing). */
@PluginMethod
fun triggerPing(call: PluginCall) {
val reason = call.getString("reason") ?: "manual_plugin_ping"
MatrixSyncService.requestSyncFetch(context, reason)
call.resolve()
}
/** Stops the sync service and erases persisted credentials. */
@PluginMethod
fun stop(call: PluginCall) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
context.stopService(Intent(context, MatrixSyncService::class.java))
call.resolve()
}
/**
* Notifies the service whether the Capacitor WebView UI is currently visible.
* When `foreground == true` the service skips firing notifications because
* the JS layer handles them directly via LocalNotifications.
*/
@PluginMethod
fun setAppForeground(call: PluginCall) {
MatrixSyncService.appInForeground = call.getBoolean("foreground", false) ?: false
call.resolve()
}
/** Returns whether the sync service is currently running. */
@PluginMethod
fun getStatus(call: PluginCall) {
val am = context.getSystemService(Context.ACTIVITY_SERVICE)
as android.app.ActivityManager
@Suppress("DEPRECATION")
val running = am.getRunningServices(Int.MAX_VALUE)
.any { it.service.className == MatrixSyncService::class.java.name }
call.resolve(JSObject().apply { put("running", running) })
}
companion object {
const val PREFS = "sync_service_prefs"
}
}

View File

@@ -1,7 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext {
kotlin_version = '1.9.25'
}
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@@ -9,6 +12,7 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.13.0' classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4' classpath 'com.google.gms:google-services:4.4.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

@@ -0,0 +1,14 @@
# Copy this file to keystore.properties and fill in your actual values
# NEVER commit keystore.properties to git!
# Path to your keystore file (relative to android/ directory)
storeFile=paarrot-release.keystore
# Keystore password
storePassword=your_keystore_password_here
# Key alias
keyAlias=paarrot
# Key password
keyPassword=your_key_password_here

2
cinny

Submodule cinny updated: 0659f3c1b0...7b69ed6df8

View File

@@ -1,8 +1,3 @@
{ {
"dependencies": { "dependencies": {}
"@capacitor/android": "8.3.0",
"@capacitor/cli": "8.3.0",
"@capacitor/core": "8.3.0",
"@capacitor/local-notifications": "8.0.2"
}
} }

View File

@@ -36,6 +36,11 @@ import {
setupNotificationTapListener, setupNotificationTapListener,
} from '../../utils/tauri'; } from '../../utils/tauri';
import { setPaarrotNavigate, initPaarrotAPI } from '../../paarrot-api'; import { setPaarrotNavigate, initPaarrotAPI } from '../../paarrot-api';
import {
startBackgroundSync,
stopBackgroundSync,
setAppForegroundState,
} from '../../utils/backgroundSync';
/** /**
* Applies the selected emoji style font to the document. * Applies the selected emoji style font to the document.
@@ -413,6 +418,31 @@ function MessageNotifications() {
return null; return null;
} }
/**
* Configures native Android push-ping background sync on login, keeps it
* informed of foreground state so it doesn't double-fire notifications,
* and clears it cleanly on unmount (logout).
* Only active on Android Capacitor builds.
*/
function BackgroundSyncSetup() {
const mx = useMatrixClient();
useEffect(() => {
startBackgroundSync(mx);
const onVisibility = () => setAppForegroundState(!document.hidden);
document.addEventListener('visibilitychange', onVisibility);
setAppForegroundState(!document.hidden);
return () => {
document.removeEventListener('visibilitychange', onVisibility);
stopBackgroundSync();
};
}, [mx]);
return null;
}
/** /**
* Initializes the Paarrot API for Electron integration * Initializes the Paarrot API for Electron integration
* Registers the navigate function and sets up IPC handlers * Registers the navigate function and sets up IPC handlers
@@ -475,6 +505,7 @@ export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) {
<FaviconUpdater /> <FaviconUpdater />
<InviteNotifications /> <InviteNotifications />
<MessageNotifications /> <MessageNotifications />
<BackgroundSyncSetup />
<PaarrotAPIInitializer /> <PaarrotAPIInitializer />
<TaskbarFlashStopper /> <TaskbarFlashStopper />
{children} {children}

View File

@@ -0,0 +1,106 @@
import { Capacitor, registerPlugin } from '@capacitor/core';
import type { MatrixClient } from 'matrix-js-sdk';
interface MatrixBackgroundSyncPlugin {
/** Persist credentials used by push-triggered one-shot sync fetches. */
start(options: {
homeserverUrl: string;
accessToken: string;
userId: string;
deviceId: string;
}): Promise<void>;
/** Trigger a one-shot fetch as if a push ping arrived. */
triggerPing(options: { reason?: string }): Promise<void>;
/** Stop any in-flight fetch and clear persisted credentials. */
stop(): Promise<void>;
/**
* Notify the service whether the app UI is visible.
* When foreground is true, the service suppresses native notifications
* because the JS layer handles them via LocalNotifications.
*/
setAppForeground(options: { foreground: boolean }): Promise<void>;
/** Returns whether the sync service is currently running. */
getStatus(): Promise<{ running: boolean }>;
}
const MatrixBackgroundSync = registerPlugin<MatrixBackgroundSyncPlugin>('MatrixBackgroundSync');
/** Returns true when the current platform is Android Capacitor. */
export const isBackgroundSyncSupported = (): boolean =>
Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android';
/**
* Persists Matrix credentials for Android push-ping wake handling.
* Safe to call on every login.
* @param mx Authenticated Matrix client
*/
export const startBackgroundSync = async (mx: MatrixClient): Promise<void> => {
if (!isBackgroundSyncSupported()) return;
const homeserverUrl = mx.getHomeserverUrl();
const accessToken = mx.getAccessToken();
const userId = mx.getUserId();
const deviceId = mx.getDeviceId();
if (!homeserverUrl || !accessToken || !userId) {
console.warn('[BackgroundSync] Missing credentials, not starting');
return;
}
try {
await MatrixBackgroundSync.start({
homeserverUrl,
accessToken,
userId,
deviceId: deviceId ?? '',
});
console.log('[BackgroundSync] Push wake credentials configured');
} catch (err) {
console.warn('[BackgroundSync] Failed to start:', err);
}
};
/**
* Manually triggers a one-shot native fetch.
* Useful for diagnostics and bridge testing.
*/
export const triggerBackgroundSyncPing = async (reason?: string): Promise<void> => {
if (!isBackgroundSyncSupported()) return;
try {
await MatrixBackgroundSync.triggerPing({ reason });
} catch (err) {
console.warn('[BackgroundSync] triggerPing failed:', err);
}
};
/**
* Stops native background sync handling and clears persisted credentials.
* Call on logout.
*/
export const stopBackgroundSync = async (): Promise<void> => {
if (!isBackgroundSyncSupported()) return;
try {
await MatrixBackgroundSync.stop();
console.log('[BackgroundSync] Service stopped');
} catch (err) {
console.warn('[BackgroundSync] Failed to stop:', err);
}
};
/**
* Tells the native service whether the app UI is in the foreground.
* Call when document visibility changes so the service can suppress
* duplicate notifications while the JS layer is active.
* @param foreground true if the WebView UI is currently visible
*/
export const setAppForegroundState = async (foreground: boolean): Promise<void> => {
if (!isBackgroundSyncSupported()) return;
try {
await MatrixBackgroundSync.setAppForeground({ foreground });
} catch (err) {
console.warn('[BackgroundSync] setAppForeground failed:', err);
}
};

1063
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,19 @@
{ {
"name": "paarrot", "name": "paarrot",
"version": "4.11.77", "version": "4.11.88",
"description": "Paarrot - A Matrix client based on Cinny", "description": "Paarrot - A Matrix client based on Cinny",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"scripts": { "scripts": {
"android:prepare": "node scripts/apply-overlay.mjs && cd cinny && npm install && npm run build && npx cap sync android", "apply-overlay": "node scripts/apply-overlay.mjs",
"android:patch-settings:linux": "sed -i 's|../node_modules/|../cinny/node_modules/|g' android/capacitor.settings.gradle", "android:prepare": "cd cinny && npm run build && cd .. && npx cap sync android",
"android:restore": "node scripts/restore-cinny.mjs", "android:open": "npx cap open android",
"android:open": "cd cinny && npx cap open android", "android:run": "npx cap run android",
"android:run": "cd cinny && npx cap run android", "android:apk": "npm run apply-overlay && npm run android:prepare && cd android && gradlew.bat assembleDebug",
"android:apk": "npm run android:prepare && cd cinny/android && gradlew.bat assembleDebug && cd ../.. && node scripts/restore-cinny.mjs", "android:apk:release": "npm run apply-overlay && npm run android:prepare && cd android && gradlew.bat assembleRelease",
"android:apk:linux": "npm run android:prepare && npm run android:patch-settings:linux && cd cinny/android && ./gradlew assembleDebug && cd ../.. && node scripts/restore-cinny.mjs", "android:apk:linux": "cd cinny && npm run build && cd .. && npx cap sync android && cd android && ./gradlew assembleDebug",
"android:apk:release": "npm run android:prepare && cd cinny/android && gradlew.bat assembleRelease && cd ../.. && node scripts/restore-cinny.mjs", "android:aab": "npm run apply-overlay && npm run android:prepare && cd android && gradlew.bat bundleRelease"
"android:apk:release:linux": "npm run android:prepare && npm run android:patch-settings:linux && cd cinny/android && ./gradlew assembleRelease && cd ../.. && node scripts/restore-cinny.mjs",
"android:aab": "npm run android:prepare && cd cinny/android && gradlew.bat bundleRelease && cd ../.. && node scripts/restore-cinny.mjs",
"android:aab:linux": "npm run android:prepare && npm run android:patch-settings:linux && cd cinny/android && ./gradlew bundleRelease && cd ../.. && node scripts/restore-cinny.mjs"
}, },
"keywords": [], "keywords": [],
"author": { "author": {
@@ -24,6 +21,12 @@
"email": "mates.media@users.noreply.github.com" "email": "mates.media@users.noreply.github.com"
}, },
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": {
"@capacitor/android": "8.3.0",
"@capacitor/cli": "8.3.0",
"@capacitor/core": "8.3.0",
"@capacitor/local-notifications": "8.0.2"
},
"devDependencies": { "devDependencies": {
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"png2icons": "2.0.1", "png2icons": "2.0.1",

View File

@@ -120,7 +120,23 @@ if (!existsSync(androidJunction)) {
console.log(` android/ link already exists, skipping`); console.log(` android/ link already exists, skipping`);
} }
// 7. Write android/local.properties with the current machine's Android SDK path // 8. Sync version from root package.json into android/app/build.gradle
console.log('[apply-overlay] Syncing version into build.gradle...');
const rootPkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
const version = rootPkg.version ?? '1.0.0';
const parts = version.split('.').map(Number);
// versionCode: major*10000 + minor*100 + patch (e.g. 4.11.78 -> 41178)
const versionCode = (parts[0] ?? 0) * 10000 + (parts[1] ?? 0) * 100 + (parts[2] ?? 0);
const buildGradlePath = join(androidTarget, 'app', 'build.gradle');
let buildGradle = readFileSync(buildGradlePath, 'utf8');
buildGradle = buildGradle
.replace(/versionCode\s+\d+/, `versionCode ${versionCode}`)
.replace(/versionName\s+"[^"]+"/, `versionName "${version}"`);
writeFileSync(buildGradlePath, buildGradle, 'utf8');
console.log(` versionName "${version}", versionCode ${versionCode}`);
// 9. Write android/local.properties with the current machine's Android SDK path
const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT; const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
if (androidHome) { if (androidHome) {
console.log('[apply-overlay] Writing android/local.properties...'); console.log('[apply-overlay] Writing android/local.properties...');