Compare commits

114 Commits

Author SHA1 Message Date
GitHub Actions
afc4ba1087 chore: bump version to 4.11.191 [skip ci] 2026-08-24 14:03:10 +00:00
f022155aa1 Update responsive image attachments
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 16s
Build / build-windows (push) Successful in 7m8s
Build / build-linux (push) Successful in 3m10s
Build / finalize-release (push) Successful in 6s
Advance Cinny to the responsive message image fix.
2026-08-25 00:02:49 +10:00
GitHub Actions
f4ec39f229 chore: bump version to 4.11.190 [skip ci] 2026-08-24 13:58:49 +00:00
848283da42 Add TikTok player embed
Some checks failed
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-linux (push) Has been cancelled
Build / finalize-release (push) Has been cancelled
Build / build-windows (push) Has been cancelled
Use TikTok's supported iframe player and safely invoke yt-dlp for YouTube streams.
2026-08-24 23:58:35 +10:00
GitHub Actions
2cd9409e3d chore: bump version to 4.11.189 [skip ci] 2026-08-24 10:07:31 +00:00
b85774e911 Bump cinny for Capacitor HTTP.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m37s
Build / build-linux (push) Successful in 3m10s
Build / finalize-release (push) Successful in 4s
Include the native HTTP configuration required for mobile collectible catalog downloads.
2026-08-24 20:07:17 +10:00
GitHub Actions
73d31a2cb4 chore: bump version to 4.11.188 [skip ci] 2026-08-24 09:53:09 +00:00
f9bc1c7c2f Bump cinny for mobile collectibles support.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m44s
Build / build-linux (push) Successful in 3m6s
Build / finalize-release (push) Successful in 4s
Use the mobile catalog and CDN fallback for profile cosmetics outside Electron.
2026-08-24 19:52:54 +10:00
GitHub Actions
85995d0744 chore: bump version to 4.11.187 [skip ci] 2026-08-24 09:30:06 +00:00
9d7ae1d63b Use published Discord collectibles catalog.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m54s
Build / build-linux (push) Successful in 3m5s
Build / finalize-release (push) Successful in 4s
Fetch and persist the public release catalog locally while retiring client-side Discord token handling.
2026-08-24 19:29:53 +10:00
GitHub Actions
ce7b2104ad chore: bump version to 4.11.186 [skip ci] 2026-08-24 08:49:07 +00:00
60d65a29ff Bump cinny for configurable sidebar nameplate background.
Some checks failed
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m49s
Build / build-linux (push) Failing after 3m18s
Build / finalize-release (push) Successful in 4s
Include immediate collectible rendering updates and sidebar opacity controls.
2026-08-24 18:48:53 +10:00
GitHub Actions
ce29b24c28 chore: bump version to 4.11.185 [skip ci] 2026-08-23 14:55:15 +00:00
608cbaf682 Add Discord collectibles IPC and bump cinny for profile overlays.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 15s
Build / build-windows (push) Successful in 6m55s
Build / build-linux (push) Successful in 3m10s
Build / finalize-release (push) Successful in 4s
Wire electron-side catalog fetch and asset download, exposing collectibles APIs to the renderer for live Discord shop browsing and Matrix profile uploads.
2026-08-24 00:55:01 +10:00
GitHub Actions
8ca7be8b42 chore: bump version to 4.11.184 [skip ci] 2026-08-23 06:55:17 +00:00
3c55fe65a3 Bump cinny: What's New dialog fixes and readable dev class names.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 7m2s
Build / build-linux (push) Successful in 3m4s
Build / finalize-release (push) Successful in 4s
2026-08-23 16:55:04 +10:00
GitHub Actions
f99b3c85d4 chore: bump version to 4.11.183 [skip ci] 2026-08-23 06:18:56 +00:00
fc4f087c68 Bump cinny for Android About/updates fixes and overlay user color crash guard.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m40s
Build / build-linux (push) Successful in 3m3s
Build / finalize-release (push) Successful in 4s
2026-08-23 16:18:43 +10:00
GitHub Actions
6d347a6f6b chore: bump version to 4.11.182 [skip ci] 2026-08-22 22:38:44 +00:00
316a5514e6 Bump cinny for Android View updates dialog fix.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 15s
Build / build-windows (push) Successful in 6m44s
Build / build-linux (push) Successful in 3m2s
Build / finalize-release (push) Successful in 4s
2026-08-23 08:38:29 +10:00
GitHub Actions
8392e3f7f5 chore: bump version to 4.11.181 [skip ci] 2026-08-22 22:26:52 +00:00
61b3cbc6d8 Point desktop build at cinny-local update manifest script for mobile CI.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 15s
Build / build-windows (push) Successful in 6m46s
Build / build-linux (push) Successful in 3m1s
Build / finalize-release (push) Successful in 7s
2026-08-23 08:26:33 +10:00
GitHub Actions
9befaa14a5 chore: bump version to 4.11.180 [skip ci] 2026-08-22 22:23:43 +00:00
ea45c4551b Bump cinny for What's New auto-show on first launch after update.
Some checks failed
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 14s
Build / finalize-release (push) Has been cancelled
Build / build-windows (push) Has been cancelled
Build / build-linux (push) Has been cancelled
2026-08-23 08:23:23 +10:00
GitHub Actions
146927fc10 chore: bump version to 4.11.179 [skip ci] 2026-08-22 22:20:20 +00:00
8edbc1b19a Bump cinny for Android updates dialog fix and add release notes manifest workflow.
Some checks failed
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Has started running
Build / finalize-release (push) Has been cancelled
Build / build-linux (push) Has been cancelled
2026-08-23 08:20:07 +10:00
GitHub Actions
9700c645a3 chore: bump version to 4.11.178 [skip ci] 2026-08-22 22:07:17 +00:00
2828ae425c Add native media save dialog and bump cinny submodule.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 6m51s
Build / build-linux (push) Successful in 3m11s
Build / finalize-release (push) Successful in 5s
Wire Electron/Tauri save-file IPC for desktop downloads and update the cinny submodule with username colors, release notes, and account profile layout fixes.
2026-08-23 08:07:02 +10:00
GitHub Actions
5e06256973 chore: bump version to 4.11.177 [skip ci] 2026-08-09 08:35:32 +00:00
28c7dedf65 Bump cinny for fullscreen sick emoji confetti spew.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 7m35s
Build / build-linux (push) Successful in 3m13s
Build / finalize-release (push) Successful in 4s
2026-08-09 18:35:17 +10:00
GitHub Actions
ca10121796 chore: bump version to 4.11.176 [skip ci] 2026-08-09 05:59:10 +00:00
1ad1fc7b70 Bump cinny for shadowbox image viewer.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 15s
Build / build-windows (push) Successful in 7m31s
Build / build-linux (push) Successful in 3m16s
Build / finalize-release (push) Successful in 6s
2026-08-09 15:58:52 +10:00
GitHub Actions
f16b84a9f1 chore: bump version to 4.11.175 [skip ci] 2026-08-09 05:50:38 +00:00
e917193b0d Harden Linux clipboard image paste and bump cinny.
Some checks failed
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 15s
Build / build-windows (push) Successful in 7m28s
Build / finalize-release (push) Has been cancelled
Build / build-linux (push) Has been cancelled
Require a real image/* clipboard format before treating paste as an image, and pick up firework confetti plus URL-paste fixes from cinny.
2026-08-09 15:50:23 +10:00
GitHub Actions
fc5d498673 chore: bump version to 4.11.174 [skip ci] 2026-08-09 03:17:02 +00:00
c01bd9a43e Bump cinny for bot-driven room app UI.
All checks were successful
Build / increment-version (push) Successful in 14s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Successful in 8m3s
Build / build-linux (push) Successful in 3m22s
Build / finalize-release (push) Successful in 4s
2026-08-09 13:16:48 +10:00
GitHub Actions
3f4b1b3fc4 chore: bump version to 4.11.173 [skip ci] 2026-08-03 05:22:25 +00:00
24b59a454a Ensure wine binary exists for Windows NSIS cross-builds.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-linux (push) Successful in 2m58s
Build / build-windows (push) Successful in 5m46s
Build / finalize-release (push) Successful in 4s
2026-08-03 15:22:12 +10:00
GitHub Actions
fac9a49d56 chore: bump version to 4.11.172 [skip ci] 2026-08-03 05:16:00 +00:00
83993efc83 Run Windows Electron builds on Linux with Wine.
Some checks failed
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-linux (push) Successful in 3m2s
Build / build-windows (push) Failing after 4m57s
Build / finalize-release (push) Successful in 4s
2026-08-03 15:15:46 +10:00
GitHub Actions
44a9230df3 chore: bump version to 4.11.171 [skip ci] 2026-08-03 05:08:38 +00:00
658ff1215c Bump cinny: local API message and unread endpoints by scope.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 14s
Build / build-linux (push) Successful in 2m55s
Build / build-windows (push) Successful in 3m54s
Build / finalize-release (push) Successful in 5s
2026-08-03 15:08:25 +10:00
GitHub Actions
829e9b06cb chore: bump version to 4.11.170 [skip ci] 2026-07-29 10:41:07 +00:00
e64279db74 Bump cinny: emoji confetti bursts and component playground.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 13s
Build / build-linux (push) Successful in 3m13s
Build / build-windows (push) Successful in 6m11s
Build / finalize-release (push) Successful in 4s
Wire the playground npm script through the desktop wrapper.
2026-07-29 20:40:53 +10:00
GitHub Actions
85d95faf11 chore: bump version to 4.11.169 [skip ci] 2026-07-25 10:09:49 +00:00
61de658029 Bump cinny: fix avatar upload for JPEG/WebP/GIF without profile color.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 16s
Build / build-linux (push) Successful in 3m21s
Build / build-windows (push) Successful in 5m45s
Build / finalize-release (push) Successful in 4s
2026-07-25 20:09:35 +10:00
GitHub Actions
c8b57ba217 chore: bump version to 4.11.168 [skip ci] 2026-07-24 06:52:49 +00:00
8d8809d0c7 Bump cinny: open DMs while space parent maps catch up.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 14s
Build / build-linux (push) Successful in 2m44s
Build / build-windows (push) Successful in 3m45s
Build / finalize-release (push) Successful in 4s
2026-07-24 16:52:35 +10:00
GitHub Actions
55a0f4f566 chore: bump version to 4.11.167 [skip ci] 2026-07-24 06:24:58 +00:00
f488f32d88 Fix PowerShell mangling GitHub upload URLs with ?name=.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 13s
Build / build-linux (push) Successful in 2m45s
Build / build-windows (push) Successful in 3m43s
Build / finalize-release (push) Successful in 4s
2026-07-24 16:24:44 +10:00
GitHub Actions
9154929de9 chore: bump version to 4.11.166 [skip ci] 2026-07-24 06:20:05 +00:00
c2cc0d5ed5 Fix PowerShell origin URL trimming for submodule checkout.
Some checks failed
Build / increment-version (push) Successful in 13s
Build / prepare-release (push) Successful in 13s
Build / build-linux (push) Successful in 2m44s
Build / build-windows (push) Failing after 3m22s
Build / finalize-release (push) Successful in 8s
2026-07-24 16:19:51 +10:00
GitHub Actions
aa38348fdd chore: bump version to 4.11.165 [skip ci] 2026-07-24 06:17:38 +00:00
02ab544a37 Fix CI Windows upload shell and submodule clone host.
Some checks failed
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 14s
Build / build-windows (push) Failing after 11s
Build / finalize-release (push) Has been cancelled
Build / build-linux (push) Has been cancelled
Use PowerShell + curl.exe on the Windows runner (no WSL), and clone cinny via a relative/origin-derived URL so act runners do not hairpin through public HTTPS.
2026-07-24 16:17:23 +10:00
GitHub Actions
3707a6d38a chore: bump version to 4.11.164 [skip ci] 2026-07-24 06:09:58 +00:00
fe1f4abf2d Bypass flaky Actions artifacts for AppImage/exe uploads.
Some checks failed
Build / increment-version (push) Successful in 12s
Build / prepare-release (push) Successful in 13s
Build / build-windows (push) Failing after 3m27s
Build / finalize-release (push) Has been cancelled
Build / build-linux (push) Has been cancelled
Publish build outputs straight to Gitea/GitHub releases via the REST API with no HTTP timeout, since Gitea's artifact pipeline keeps timing out on ~130MB binaries.
2026-07-24 16:09:44 +10:00
147389724e Point cinny submodule at git.ruv.wtf. 2026-07-24 16:02:45 +10:00
GitHub Actions
d4d4c73c2b chore: bump version to 4.11.163 [skip ci] 2026-07-24 06:01:21 +00:00
73fbc1c77a Bump cinny: hide Jump to Latest when scrolled to live bottom.
Some checks failed
Build / increment-version (push) Successful in 12s
Build / build-windows (push) Successful in 3m30s
Build / build-linux (push) Failing after 7m44s
Build / create-release (push) Has been cancelled
2026-07-24 16:01:06 +10:00
GitHub Actions
3dc732521f chore: bump version to 4.11.162 [skip ci] 2026-07-23 05:18:40 +00:00
b76aa3fe4a Add mock auto-updater in dev and title-bar download progress.
All checks were successful
Build / increment-version (push) Successful in 11s
Build / build-linux (push) Successful in 1m54s
Build / build-windows (push) Successful in 3m36s
Build / create-release (push) Successful in 1m13s
2026-07-23 15:18:25 +10:00
GitHub Actions
c0d1e87b8d chore: bump version to 4.11.161 [skip ci] 2026-07-23 05:14:07 +00:00
04d4026e78 Fix Linux SSO return via paarrot://desktop and protocol handler repair.
Some checks failed
Build / increment-version (push) Successful in 12s
Build / build-linux (push) Successful in 1m55s
Build / build-windows (push) Successful in 3m35s
Build / create-release (push) Has been cancelled
2026-07-23 15:13:54 +10:00
GitHub Actions
cfbc59d6a9 chore: bump version to 4.11.160 [skip ci] 2026-07-23 04:58:11 +00:00
dfe42fa942 Sync cinny: remove editor autocorrect workaround.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / build-linux (push) Successful in 1m54s
Build / build-windows (push) Successful in 3m35s
Build / create-release (push) Successful in 1m12s
2026-07-23 14:57:58 +10:00
GitHub Actions
6a8397425e chore: bump version to 4.11.159 [skip ci] 2026-07-23 04:52:37 +00:00
641ec26142 Sync cinny: Shared Media drawer and same-room navigation fixes.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / build-linux (push) Successful in 1m59s
Build / build-windows (push) Successful in 3m47s
Build / create-release (push) Successful in 1m12s
2026-07-23 14:52:23 +10:00
GitHub Actions
1eaf246806 chore: bump version to 4.11.158 [skip ci] 2026-07-22 14:24:31 +00:00
0e97e4d3b6 Sync cinny: Stationery and Stationery Dark themes.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / build-linux (push) Successful in 1m58s
Build / build-windows (push) Successful in 3m37s
Build / create-release (push) Successful in 1m13s
2026-07-23 00:24:17 +10:00
GitHub Actions
1636a8d0c6 chore: bump version to 4.11.157 [skip ci] 2026-07-22 10:29:14 +00:00
ac236750aa Sync cinny: fix jumbo emoji regex for production builds.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / build-linux (push) Successful in 1m52s
Build / build-windows (push) Successful in 3m40s
Build / create-release (push) Successful in 1m12s
2026-07-22 20:29:00 +10:00
GitHub Actions
7e747e6c36 chore: bump version to 4.11.156 [skip ci] 2026-07-22 10:14:27 +00:00
1b2cae5b12 Sync cinny: fix jumbo emoji in production builds.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / build-linux (push) Successful in 1m56s
Build / build-windows (push) Successful in 3m42s
Build / create-release (push) Successful in 1m11s
2026-07-22 20:14:15 +10:00
GitHub Actions
76921c4e79 chore: bump version to 4.11.155 [skip ci] 2026-07-22 10:00:52 +00:00
688efff7c9 Sync cinny submodule and stop Vite from opening a browser on dev.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / build-linux (push) Successful in 1m55s
Build / build-windows (push) Successful in 3m36s
Build / create-release (push) Successful in 1m13s
Pull in timeline scroll memory, media height reservation, avatar caching, jumbo emoji, and Unicode 15 emoji font coverage.
2026-07-22 20:00:38 +10:00
GitHub Actions
9bc377b62a chore: bump version to 4.11.154 [skip ci] 2026-07-22 09:11:11 +00:00
836ccce7a3 Revert cinny to pre-jumbo emoji baseline.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / build-linux (push) Successful in 1m57s
Build / build-windows (push) Successful in 3m37s
Build / create-release (push) Successful in 1m13s
Roll back submodule to 154f4dfd before jumbo emoji and related styling regressions.
2026-07-22 19:10:58 +10:00
GitHub Actions
34c8e6e38c chore: bump version to 4.11.153 [skip ci] 2026-07-22 09:03:00 +00:00
e69bda3aa6 Bump cinny: fix production chat text color in Electron builds.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / build-linux (push) Successful in 1m51s
Build / build-windows (push) Successful in 3m43s
Build / create-release (push) Successful in 1m13s
2026-07-22 19:02:46 +10:00
GitHub Actions
5d55e5d826 chore: bump version to 4.11.152 [skip ci] 2026-07-22 08:37:39 +00:00
b5902b9fc3 Bump cinny: fix production CSS loading in Electron build.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / build-linux (push) Successful in 1m51s
Build / build-windows (push) Successful in 3m34s
Build / create-release (push) Successful in 1m12s
2026-07-22 18:37:26 +10:00
GitHub Actions
f60cf0bd3c chore: bump version to 4.11.151 [skip ci] 2026-07-22 08:21:32 +00:00
83dec0ef54 Bump cinny: fix black chat pane and jumbo emoji rendering.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / build-linux (push) Successful in 1m50s
Build / build-windows (push) Successful in 3m35s
Build / create-release (push) Successful in 1m12s
2026-07-22 18:21:18 +10:00
GitHub Actions
40a76f02cd chore: bump version to 4.11.150 [skip ci] 2026-07-22 08:02:08 +00:00
3def3cc456 Bump cinny: instant room switches, perf cleanup, members drawer prefs.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / build-linux (push) Successful in 1m53s
Build / build-windows (push) Successful in 3m42s
Build / create-release (push) Successful in 1m13s
2026-07-22 18:01:53 +10:00
GitHub Actions
30bd67d43a chore: bump version to 4.11.149 [skip ci] 2026-07-22 06:16:20 +00:00
ffeccee680 Bump cinny: media placeholders, avatar blob cache, jumbo emoji.
All checks were successful
Build / increment-version (push) Successful in 12s
Build / build-linux (push) Successful in 1m58s
Build / build-windows (push) Successful in 3m50s
Build / create-release (push) Successful in 1m13s
2026-07-22 14:42:19 +10:00
GitHub Actions
facbeee024 chore: bump version to 4.11.148 [skip ci] 2026-07-21 06:21:51 +00:00
09102c7903 Bump cinny: fix jumbo emoji actually rendering at 100px.
All checks were successful
Build / increment-version (push) Successful in 14s
Build / build-linux (push) Successful in 1m55s
Build / build-windows (push) Successful in 5m42s
Build / create-release (push) Successful in 1m13s
2026-07-21 16:21:37 +10:00
GitHub Actions
fa33ec7ac8 chore: bump version to 4.11.147 [skip ci] 2026-07-21 05:56:18 +00:00
c44f5cf14a Bump cinny: fix jumbo standalone emoji sizing.
All checks were successful
Build / increment-version (push) Successful in 13s
Build / build-linux (push) Successful in 1m57s
Build / build-windows (push) Successful in 5m21s
Build / create-release (push) Successful in 1m14s
2026-07-21 15:56:03 +10:00
GitHub Actions
27793d7c4f chore: bump version to 4.11.146 [skip ci] 2026-07-21 05:38:44 +00:00
32b2902ef7 Bump cinny: fix jumbo emoji vanilla-extract build.
All checks were successful
Build / increment-version (push) Successful in 14s
Build / build-linux (push) Successful in 2m2s
Build / build-windows (push) Successful in 3m33s
Build / create-release (push) Successful in 1m13s
2026-07-21 15:38:29 +10:00
GitHub Actions
5cdb318854 chore: bump version to 4.11.145 [skip ci] 2026-07-21 05:34:16 +00:00
a28f05bb88 Bump cinny: theme polish, jumbo emoji, and Windows autocorrect fix.
Some checks failed
Build / increment-version (push) Successful in 14s
Build / build-linux (push) Failing after 44s
Build / build-windows (push) Failing after 1m4s
Build / create-release (push) Has been skipped
2026-07-21 15:34:00 +10:00
GitHub Actions
cecfe73b2e chore: bump version to 4.11.144 [skip ci] 2026-07-14 17:31:07 +00:00
b458c7918c chore: bump cinny submodule for emoji font spacing fix
All checks were successful
Build / increment-version (push) Successful in 14s
Build / build-linux (push) Successful in 2m3s
Build / build-windows (push) Successful in 5m10s
Build / create-release (push) Successful in 1m13s
2026-07-15 03:30:53 +10:00
GitHub Actions
d8660f4822 chore: bump version to 4.11.143 [skip ci] 2026-07-12 16:40:22 +00:00
dad83773a6 Bump cinny: Twemoji digit fix and Slate emoji focus crash
All checks were successful
Build / increment-version (push) Successful in 13s
Build / build-linux (push) Successful in 1m55s
Build / build-windows (push) Successful in 4m48s
Build / create-release (push) Successful in 1m12s
2026-07-13 02:40:08 +10:00
GitHub Actions
d2d35de46b chore: bump version to 4.11.142 [skip ci] 2026-07-12 16:25:25 +00:00
c752ec0b38 Bump cinny: fix noise/echo cancellation settings
All checks were successful
Build / increment-version (push) Successful in 14s
Build / build-linux (push) Successful in 1m57s
Build / build-windows (push) Successful in 5m5s
Build / create-release (push) Successful in 1m13s
2026-07-13 02:25:10 +10:00
GitHub Actions
28752eb56e chore: bump version to 4.11.141 [skip ci] 2026-07-12 08:15:59 +00:00
4f243a0fcf Bump cinny: fix vanilla-extract presence CSS build failure.
All checks were successful
Build / increment-version (push) Successful in 14s
Build / build-linux (push) Successful in 1m57s
Build / build-windows (push) Successful in 4m48s
Build / create-release (push) Successful in 1m13s
2026-07-12 18:15:44 +10:00
GitHub Actions
9bce616f26 chore: bump version to 4.11.140 [skip ci] 2026-07-11 22:19:18 +00:00
f16ab73c9a Allow git deps in npm and bump cinny submodule.
Some checks failed
Build / increment-version (push) Successful in 13s
Build / build-linux (push) Failing after 42s
Build / build-windows (push) Failing after 1m47s
Build / create-release (push) Has been skipped
2026-07-12 08:19:03 +10:00
GitHub Actions
c29a15e073 chore: bump version to 4.11.139 [skip ci] 2026-07-11 21:52:03 +00:00
f5fe61e3eb hidden shit, and online status bars, search settings
Some checks failed
Build / increment-version (push) Successful in 14s
Build / build-linux (push) Failing after 42s
Build / build-windows (push) Failing after 1m29s
Build / create-release (push) Has been skipped
2026-07-12 07:51:47 +10:00
GitHub Actions
a1a752b0fb chore: bump version to 4.11.138 [skip ci] 2026-07-11 21:15:56 +00:00
7769250875 Removed invalid channels
All checks were successful
Build / increment-version (push) Successful in 14s
Build / build-linux (push) Successful in 1m56s
Build / build-windows (push) Successful in 4m36s
Build / create-release (push) Successful in 1m13s
2026-07-12 07:15:28 +10:00
GitHub Actions
b3af9f3185 chore: bump version to 4.11.137 [skip ci] 2026-07-10 11:40:45 +00:00
7906326790 update base
All checks were successful
Build / increment-version (push) Successful in 14s
Build / build-linux (push) Successful in 1m56s
Build / build-windows (push) Successful in 4m56s
Build / create-release (push) Successful in 1m13s
2026-07-10 21:40:27 +10:00
GitHub Actions
3b4babb9d7 chore: bump version to 4.11.136 [skip ci] 2026-07-08 18:22:44 +00:00
4b284ec747 Merge branch 'main' of http://synbox.ruv.wtf:8418/litruv/cinny-desktop
All checks were successful
Build / increment-version (push) Successful in 14s
Build / build-linux (push) Successful in 2m0s
Build / build-windows (push) Successful in 3m28s
Build / create-release (push) Successful in 1m12s
2026-07-09 04:22:28 +10:00
e1b4cfb9c8 chore: bump cinny submodule for compact nav and back routing fixes 2026-07-09 04:22:14 +10:00
16 changed files with 1497 additions and 299 deletions

View File

@@ -8,6 +8,9 @@ on:
tags:
- 'v*'
# Large AppImage/exe uploads time out on Gitea's actions artifact pipeline.
# Build jobs publish assets straight to Gitea/GitHub releases via the REST API.
jobs:
increment-version:
runs-on: ubuntu-latest
@@ -105,84 +108,101 @@ jobs:
echo "VERSION=${{ steps.decide.outputs.CURRENT_VERSION }}" >> $GITHUB_OUTPUT
fi
build-windows:
runs-on: windows
prepare-release:
runs-on: ubuntu-latest
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'
outputs:
version: ${{ steps.version.outputs.VERSION }}
tag: ${{ steps.version.outputs.TAG_NAME }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: false
fetch-depth: 0
ref: ${{ github.ref_name }}
- name: Checkout submodules manually
shell: powershell
run: |
git submodule sync --recursive
if (git submodule update --init --recursive) {
Write-Host "Submodules checked out at pinned commits."
exit 0
}
Write-Warning "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
git submodule deinit -f --all
git submodule update --init --recursive --remote
Write-Host "Resolved submodule commit:"
git -C cinny rev-parse HEAD
- name: Pull latest (after version bump)
if: github.ref == 'refs/heads/main'
shell: powershell
run: git pull origin main
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies (root)
run: npm ci --prefer-offline
- name: Install dependencies (cinny)
working-directory: ./cinny
run: npm ci --prefer-offline
- name: Clean previous builds
shell: powershell
- name: Resolve version
id: version
run: |
if (Test-Path cinny/dist) { Remove-Item -Recurse -Force cinny/dist }
if (Test-Path dist-electron) { Remove-Item -Recurse -Force dist-electron }
if [ -n "${{ needs.increment-version.outputs.version }}" ]; then
VERSION="${{ needs.increment-version.outputs.version }}"
else
VERSION=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
fi
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "TAG_NAME=v$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Build Electron app
run: npm run build:local -- --win
- name: Create and push tag
run: |
VERSION="${{ steps.version.outputs.VERSION }}"
TAG_NAME="v${VERSION}"
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
echo "Tag $TAG_NAME already exists"
else
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
git push origin "$TAG_NAME"
fi
- name: Ensure Gitea release
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.VERSION }}"
TAG_NAME="v${VERSION}"
API_BASE="${{ github.server_url }}/api/v1"
REPO="${{ github.repository }}"
RELEASE_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty' || true)
if [ -z "$RELEASE_ID" ]; then
echo "Creating Gitea release ${TAG_NAME}..."
RELEASE_ID=$(curl -fsS -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
"${API_BASE}/repos/${REPO}/releases" | jq -r '.id')
fi
echo "Gitea release id: ${RELEASE_ID}"
- name: Ensure GitHub release
env:
GH_TOKEN: ${{ secrets.GHTOKEN }}
- name: Upload NSIS Installer (x64)
uses: actions/upload-artifact@v3
with:
name: Paarrot-Setup-x64.exe
path: dist-electron/Paarrot-*-win-x64.exe
- name: Upload latest.yml
uses: actions/upload-artifact@v3
with:
name: latest.yml
path: dist-electron/latest.yml
- name: List build output
shell: powershell
run: |
Get-ChildItem -Path dist-electron -Recurse | Select-Object FullName
set -euo pipefail
VERSION="${{ steps.version.outputs.VERSION }}"
TAG_NAME="v${VERSION}"
GITHUB_REPO="Paarrot/Paarrot-Desktop"
build-linux:
RELEASE_ID=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty' || true)
if [ -z "$RELEASE_ID" ]; then
echo "Creating GitHub release ${TAG_NAME}..."
RELEASE_ID=$(curl -fsS -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')
fi
echo "GitHub release id: ${RELEASE_ID}"
build-windows:
runs-on: ubuntu-latest
needs: increment-version
if: always() && (needs.increment-version.outputs.should_build == 'true' || needs.increment-version.result == 'skipped')
needs: [increment-version, prepare-release]
if: always() && needs.prepare-release.result == 'success'
steps:
- name: Checkout repository
@@ -194,6 +214,14 @@ jobs:
- name: Checkout submodules manually
run: |
set -euo pipefail
# Use the same host/scheme the runner already used for origin (not public HTTPS).
ORIGIN_URL=$(git remote get-url origin)
ORIGIN_URL=${ORIGIN_URL%/}
ORIGIN_URL=${ORIGIN_URL%.git}
BASE_URL=${ORIGIN_URL%/*}
CINNY_URL="${BASE_URL}/cinny.git"
echo "Rewriting submodule URL to ${CINNY_URL}"
git config submodule.cinny.url "${CINNY_URL}"
git submodule sync --recursive
if git submodule update --init --recursive; then
@@ -202,7 +230,165 @@ jobs:
fi
echo "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
git submodule deinit -f --all
git submodule deinit -f --all || true
rm -rf cinny .git/modules/cinny
git config submodule.cinny.url "${CINNY_URL}"
git submodule sync --recursive
git submodule update --init --recursive --remote
echo "Resolved submodule commit:"
git -C cinny rev-parse HEAD
- name: Pull latest (after version bump)
if: github.ref == 'refs/heads/main'
run: git pull origin main
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Wine (NSIS cross-build)
run: |
set -euo pipefail
# electron-builder spawns `wine` by name; wine64 alone is not enough.
sudo dpkg --add-architecture i386
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
wine \
wine64 \
wine32 \
|| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends wine64
if ! command -v wine >/dev/null 2>&1; then
WINE64_BIN=$(command -v wine64)
echo "No wine binary; linking wine64 -> /usr/local/bin/wine (${WINE64_BIN})"
sudo ln -sf "${WINE64_BIN}" /usr/local/bin/wine
fi
command -v wine
wine --version
- name: Install dependencies (root)
run: npm ci --prefer-offline
- name: Install dependencies (cinny)
working-directory: ./cinny
run: npm ci --prefer-offline
- name: Clean previous builds
run: |
rm -rf cinny/dist dist-electron
- name: Build Electron app
run: npm run build:local -- --win
env:
GH_TOKEN: ${{ secrets.GHTOKEN }}
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
- name: Upload Windows assets to releases
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GHTOKEN }}
run: |
set -euo pipefail
TAG_NAME="${{ needs.prepare-release.outputs.tag }}"
API_BASE="${{ github.server_url }}/api/v1"
REPO="${{ github.repository }}"
GITHUB_REPO="Paarrot/Paarrot-Desktop"
GITEA_RELEASE_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id')
GH_RELEASE_ID=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id')
GH_UPLOAD_URL=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/${GH_RELEASE_ID}" | jq -r '.upload_url' | sed 's/{?name,label}//')
upload_one() {
local FILE="$1"
local FILENAME
FILENAME=$(basename "$FILE")
echo "Uploading ${FILENAME} ($(du -h "$FILE" | cut -f1))..."
OLD_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets" \
| jq -r --arg n "$FILENAME" '.[] | select(.name == $n) | .id' || true)
if [ -n "${OLD_ID:-}" ]; then
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets/${OLD_ID}" >/dev/null
fi
curl -fsS --max-time 0 --retry 5 --retry-delay 10 \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"${FILE}" \
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets?name=${FILENAME}" \
>/dev/null
echo " Gitea: ok"
OLD_GH=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/${GH_RELEASE_ID}/assets" \
| jq -r --arg n "$FILENAME" '.[] | select(.name == $n) | .id' || true)
if [ -n "${OLD_GH:-}" ]; then
curl -fsS -X DELETE -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/assets/${OLD_GH}" >/dev/null
fi
curl -fsS --max-time 0 --retry 5 --retry-delay 10 \
-X POST \
-H "Authorization: token ${GH_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"${FILE}" \
"${GH_UPLOAD_URL}?name=${FILENAME}" \
>/dev/null
echo " GitHub: ok"
}
shopt -s nullglob
for FILE in dist-electron/Paarrot-*-win-x64.exe dist-electron/latest.yml; do
upload_one "$FILE"
done
- name: List build output
run: |
find dist-electron -type f -printf '%p\n' | sort
build-linux:
runs-on: ubuntu-latest
needs: [increment-version, prepare-release]
if: always() && needs.prepare-release.result == 'success'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: false
ref: ${{ github.ref_name }}
- name: Checkout submodules manually
run: |
set -euo pipefail
# Use the same host/scheme the runner already used for origin (not public HTTPS).
ORIGIN_URL=$(git remote get-url origin)
ORIGIN_URL=${ORIGIN_URL%/}
ORIGIN_URL=${ORIGIN_URL%.git}
BASE_URL=${ORIGIN_URL%/*}
CINNY_URL="${BASE_URL}/cinny.git"
echo "Rewriting submodule URL to ${CINNY_URL}"
git config submodule.cinny.url "${CINNY_URL}"
git submodule sync --recursive
if git submodule update --init --recursive; then
echo "Submodules checked out at pinned commits."
exit 0
fi
echo "Pinned submodule commit is unavailable on remote. Falling back to remote submodule HEAD."
git submodule deinit -f --all || true
rm -rf cinny .git/modules/cinny
git config submodule.cinny.url "${CINNY_URL}"
git submodule sync --recursive
git submodule update --init --recursive --remote
echo "Resolved submodule commit:"
@@ -233,208 +419,126 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GHTOKEN }}
- name: Upload AppImage
uses: actions/upload-artifact@v3
with:
name: Paarrot-Linux-x64.AppImage
path: dist-electron/Paarrot-*.AppImage
- name: Upload Linux assets to releases
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GHTOKEN }}
run: |
set -euo pipefail
VERSION="${{ needs.prepare-release.outputs.version }}"
TAG_NAME="${{ needs.prepare-release.outputs.tag }}"
API_BASE="${{ github.server_url }}/api/v1"
REPO="${{ github.repository }}"
GITHUB_REPO="Paarrot/Paarrot-Desktop"
- name: Upload latest-linux.yml
uses: actions/upload-artifact@v3
with:
name: latest-linux.yml
path: dist-electron/latest-linux.yml
GITEA_RELEASE_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id')
GH_RELEASE_ID=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG_NAME}" | jq -r '.id')
GH_UPLOAD_URL=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/${GH_RELEASE_ID}" | jq -r '.upload_url' | sed 's/{?name,label}//')
create-release:
upload_one() {
local FILE="$1"
local FILENAME
FILENAME=$(basename "$FILE")
echo "Uploading ${FILENAME} ($(du -h "$FILE" | cut -f1))..."
OLD_ID=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets" \
| jq -r --arg n "$FILENAME" '.[] | select(.name == $n) | .id' || true)
if [ -n "${OLD_ID:-}" ]; then
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets/${OLD_ID}" >/dev/null
fi
curl -fsS --max-time 0 --retry 5 --retry-delay 10 \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"${FILE}" \
"${API_BASE}/repos/${REPO}/releases/${GITEA_RELEASE_ID}/assets?name=${FILENAME}" \
>/dev/null
echo " Gitea: ok"
OLD_GH=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/${GH_RELEASE_ID}/assets" \
| jq -r --arg n "$FILENAME" '.[] | select(.name == $n) | .id' || true)
if [ -n "${OLD_GH:-}" ]; then
curl -fsS -X DELETE -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/assets/${OLD_GH}" >/dev/null
fi
curl -fsS --max-time 0 --retry 5 --retry-delay 10 \
-X POST \
-H "Authorization: token ${GH_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"${FILE}" \
"${GH_UPLOAD_URL}?name=${FILENAME}" \
>/dev/null
echo " GitHub: ok"
}
shopt -s nullglob
for FILE in dist-electron/Paarrot-*.AppImage dist-electron/latest-linux.yml; do
upload_one "$FILE"
done
finalize-release:
runs-on: ubuntu-latest
needs: [increment-version, build-windows, build-linux]
if: always() && (needs.build-windows.result == 'success' || needs.build-linux.result == 'success')
needs: [increment-version, prepare-release, build-windows, build-linux]
if: always() && needs.prepare-release.result == 'success' && (needs.build-windows.result == 'success' || needs.build-linux.result == 'success')
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref_name }}
- name: Pull latest (after version bump)
if: github.ref == 'refs/heads/main'
run: git pull origin main
- name: Get version
id: version
run: |
# Use version from increment-version job if available, otherwise read from file
if [ -n "${{ needs.increment-version.outputs.version }}" ]; then
VERSION="${{ needs.increment-version.outputs.version }}"
else
VERSION=$(grep -Po '"version":\s*"\K[^"]+' package.json | head -1)
fi
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Check if tag exists
id: tag_check
run: |
VERSION="${{ steps.version.outputs.VERSION }}"
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "Tag v$VERSION already exists"
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "Tag v$VERSION does not exist"
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Create and push tag
if: steps.tag_check.outputs.exists == 'false'
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git tag -a "v${{ steps.version.outputs.VERSION }}" -m "Release v${{ steps.version.outputs.VERSION }}"
git push origin "v${{ steps.version.outputs.VERSION }}"
- name: Download all artifacts
uses: actions/download-artifact@v3
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release-files
find artifacts -type f \( -name "*.exe" -o -name "*.AppImage" -o -name "*.yml" \) -exec cp {} release-files/ \;
ls -la release-files/
- name: Create or Update Release
- name: Cleanup old Gitea releases
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.VERSION }}"
TAG_NAME="v${VERSION}"
API_BASE="http://synbox.ruv.wtf:8418/api/v1"
REPO="litruv/cinny-desktop"
set -euo pipefail
TAG_NAME="${{ needs.prepare-release.outputs.tag }}"
API_BASE="${{ github.server_url }}/api/v1"
REPO="${{ github.repository }}"
# Delete old releases (keep only current)
echo "Cleaning up old releases..."
OLD_RELEASES=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases" | jq -r '.[] | select(.tag_name != "'${TAG_NAME}'") | .id')
echo "Cleaning up old Gitea releases (keeping ${TAG_NAME})..."
OLD_RELEASES=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases" | jq -r --arg t "$TAG_NAME" '.[] | select(.tag_name != $t) | .id')
for OLD_ID in $OLD_RELEASES; do
echo "Deleting old release ${OLD_ID}..."
OLD_TAG=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
[ -n "$OLD_ID" ] || continue
OLD_TAG=$(curl -fsS -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}" | jq -r '.tag_name')
# Delete release
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}"
# Delete tag
if [ -n "$OLD_TAG" ]; then
echo "Deleting old tag ${OLD_TAG}..."
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/tags/${OLD_TAG}"
echo "Deleting Gitea release ${OLD_ID} (${OLD_TAG})..."
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${OLD_ID}" >/dev/null || true
if [ -n "$OLD_TAG" ] && [ "$OLD_TAG" != "null" ]; then
curl -fsS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/tags/${OLD_TAG}" >/dev/null || true
fi
done
# Check if release exists
RELEASE_ID=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/tags/${TAG_NAME}" | jq -r '.id // empty')
if [ -z "$RELEASE_ID" ]; then
echo "Creating new release for ${TAG_NAME}..."
RELEASE_ID=$(curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${TAG_NAME}\", \"body\": \"Release ${VERSION}\", \"draft\": false, \"prerelease\": false}" \
"${API_BASE}/repos/${REPO}/releases" | jq -r '.id')
echo "Created release with ID: ${RELEASE_ID}"
else
echo "Release exists with ID: ${RELEASE_ID}"
# Delete existing assets
echo "Deleting existing assets..."
ASSETS=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets" | jq -r '.[].id')
for ASSET_ID in $ASSETS; do
echo "Deleting asset ${ASSET_ID}..."
curl -s -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
done
fi
# Upload new assets
echo "Uploading assets..."
for FILE in release-files/*; do
FILENAME=$(basename "$FILE")
echo "Uploading ${FILENAME}..."
curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${FILE}" \
"${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
echo ""
done
echo "Release complete!"
- name: Create GitHub Release
- name: Cleanup old GitHub releases
env:
GH_TOKEN: ${{ secrets.GHTOKEN }}
run: |
VERSION="${{ steps.version.outputs.VERSION }}"
TAG_NAME="v${VERSION}"
set -euo pipefail
TAG_NAME="${{ needs.prepare-release.outputs.tag }}"
GITHUB_REPO="Paarrot/Paarrot-Desktop"
# Delete old GitHub releases (keep only current)
echo "Cleaning up old GitHub releases..."
OLD_RELEASES=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r '.[] | select(.tag_name != "'${TAG_NAME}'") | .id')
echo "Cleaning up old GitHub releases (keeping ${TAG_NAME})..."
OLD_RELEASES=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases" | jq -r --arg t "$TAG_NAME" '.[] | select(.tag_name != $t) | .id')
for OLD_ID in $OLD_RELEASES; do
echo "Deleting old GitHub release ${OLD_ID}..."
OLD_TAG=$(curl -s -H "Authorization: token ${GH_TOKEN}" \
[ -n "$OLD_ID" ] || continue
OLD_TAG=$(curl -fsS -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}" | jq -r '.tag_name')
# Delete release
curl -s -X DELETE -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}"
# Delete tag
if [ -n "$OLD_TAG" ]; then
echo "Deleting old GitHub tag ${OLD_TAG}..."
curl -s -X DELETE -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/git/refs/tags/${OLD_TAG}"
echo "Deleting GitHub release ${OLD_ID} (${OLD_TAG})..."
curl -fsS -X DELETE -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/releases/${OLD_ID}" >/dev/null || true
if [ -n "$OLD_TAG" ] && [ "$OLD_TAG" != "null" ]; then
curl -fsS -X DELETE -H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${GITHUB_REPO}/git/refs/tags/${OLD_TAG}" >/dev/null || true
fi
done
# Check if release exists on GitHub
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
# Upload assets to GitHub
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=$(basename "$FILE")
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!"
echo "Release finalize complete for ${TAG_NAME}"

35
.github/workflows/update-manifest.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Update release notes manifest
on:
push:
paths:
- 'cinny/public/update/*.md'
- 'cinny/scripts/generate-update-manifest.mjs'
permissions:
contents: write
jobs:
manifest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Generate manifest.json
run: node cinny/scripts/generate-update-manifest.mjs
- name: Commit manifest if changed
run: |
if git diff --quiet -- cinny/public/update/manifest.json; then
echo "manifest.json unchanged"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add cinny/public/update/manifest.json
git commit -m "chore: regenerate update manifest.json"
git push

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "cinny"]
path = cinny
url = http://synbox.ruv.wtf:8418/litruv/cinny.git
url = ../cinny.git

1
.npmrc
View File

@@ -1 +1,2 @@
save-exact=true
allow-git=all

2
cinny

Submodule cinny updated: b4258278d8...47979718eb

View File

@@ -153,6 +153,72 @@ class PaarrotAPIServer {
}
});
// Recent messages across group channels (non-DMs)
this.app.get('/messages/groups', async (req, res) => {
try {
const result = await this.executeAction('get-messages-groups', {
limit: req.query.limit,
});
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Recent messages across DMs
this.app.get('/messages/dms', async (req, res) => {
try {
const result = await this.executeAction('get-messages-dms', {
limit: req.query.limit,
});
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Recent messages across groups + DMs
this.app.get('/messages/combined', async (req, res) => {
try {
const result = await this.executeAction('get-messages-combined', {
limit: req.query.limit,
});
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Unread conversations across group channels (non-DMs)
this.app.get('/unreads/groups', async (req, res) => {
try {
const result = await this.executeAction('get-unreads-groups');
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Unread conversations across DMs
this.app.get('/unreads/dms', async (req, res) => {
try {
const result = await this.executeAction('get-unreads-dms');
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// Unread conversations across groups + DMs
this.app.get('/unreads/combined', async (req, res) => {
try {
const result = await this.executeAction('get-unreads-combined');
res.json({ success: true, data: result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 404 handler
this.app.use((req, res) => {
res.status(404).json({
@@ -169,7 +235,13 @@ class PaarrotAPIServer {
'GET /channels',
'POST /message',
'POST /message/current',
'GET /room/current'
'GET /room/current',
'GET /messages/groups',
'GET /messages/dms',
'GET /messages/combined',
'GET /unreads/groups',
'GET /unreads/dms',
'GET /unreads/combined'
]
});
});
@@ -237,6 +309,12 @@ class PaarrotAPIServer {
console.log(' POST /message');
console.log(' POST /message/current');
console.log(' GET /room/current');
console.log(' GET /messages/groups');
console.log(' GET /messages/dms');
console.log(' GET /messages/combined');
console.log(' GET /unreads/groups');
console.log(' GET /unreads/dms');
console.log(' GET /unreads/combined');
resolve(port);
}).on('error', (err) => {
if (err.code === 'EADDRINUSE') {

View File

@@ -0,0 +1,381 @@
/**
* Discord shop collectibles: fetch a published catalog and download selected assets from Discord CDN.
*/
const ITEM_TYPE_FOLDER = {
0: 'avatar-decorations',
1: 'profile-effects',
2: 'nameplates',
};
const ITEM_TYPE_KEY = {
0: 'avatar_decoration',
1: 'profile_effect',
2: 'nameplate',
};
const CDN_BASE = 'https://cdn.discordapp.com';
const CDN_HOST_RE = /(^|\.)(discordapp\.com|discordapp\.net|discord\.com)$/i;
const DEFAULT_USER_AGENT =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) discord/1.0 Chrome/120 Electron/28 Safari/537.36';
const PUBLISHED_CATALOG_URLS = [
'https://github.com/litruv/discord-collectibles/releases/download/latest/profileeffects.json',
'https://github.com/litruv/discord-collectibles/releases/download/latest/nameplate.json',
'https://github.com/litruv/discord-collectibles/releases/download/latest/avatardecorations.json',
];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function isCdnUrl(value) {
if (typeof value !== 'string' || !value.startsWith('http')) return false;
try {
return CDN_HOST_RE.test(new URL(value).hostname);
} catch {
return false;
}
}
function basenameFromUrl(url) {
try {
const { pathname } = new URL(url);
const last = decodeURIComponent(pathname.split('/').filter(Boolean).pop() || '');
return last || 'asset';
} catch {
return 'asset';
}
}
function guessMimeType(filename) {
const lower = String(filename).toLowerCase();
if (lower.endsWith('.png')) return 'image/png';
if (lower.endsWith('.webp')) return 'image/webp';
if (lower.endsWith('.webm')) return 'video/webm';
if (lower.endsWith('.gif')) return 'image/gif';
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
return 'application/octet-stream';
}
function deepCollectUrls(node, acc = new Set()) {
if (node == null) return acc;
if (typeof node === 'string') {
if (isCdnUrl(node)) acc.add(node);
return acc;
}
if (Array.isArray(node)) {
for (const v of node) deepCollectUrls(v, acc);
return acc;
}
if (typeof node === 'object') {
for (const v of Object.values(node)) deepCollectUrls(v, acc);
}
return acc;
}
function urlsForItem(item) {
const out = [];
const seen = new Set();
function push(url, filename, role) {
if (!url || seen.has(url)) return;
seen.add(url);
out.push({ url, filename, role: role || filename });
}
for (const url of deepCollectUrls(item)) {
let role = basenameFromUrl(url);
if (item.thumbnailPreviewSrc === url) role = 'thumbnail';
else if (item.reducedMotionSrc === url) role = 'reduced_motion';
else if (Array.isArray(item.effects)) {
const effectIndex = item.effects.findIndex((effect) => effect?.src === url);
if (effectIndex >= 0) role = `effect_${effectIndex}`;
}
push(url, basenameFromUrl(url), role);
}
if (item.type === 0 && item.asset) {
const base = `${CDN_BASE}/avatar-decoration-presets/${item.asset}.png`;
push(`${base}?passthrough=true`, 'animated.png', 'animated');
push(`${base}?passthrough=false`, 'static.png', 'static');
}
if (item.type === 2 && item.asset) {
const assetPath = String(item.asset).replace(/^\/+/, '');
push(`${CDN_BASE}/assets/collectibles/${assetPath}static.png`, 'static.png', 'static');
push(`${CDN_BASE}/assets/collectibles/${assetPath}asset.webm`, 'asset.webm', 'animated');
}
return out;
}
function decimalRgbToCss(color) {
const value = Number(color) >>> 0;
const r = (value >> 16) & 0xff;
const g = (value >> 8) & 0xff;
const b = value & 0xff;
return `rgb(${r}, ${g}, ${b})`;
}
function gradientFromColors(colors) {
if (!Array.isArray(colors) || colors.length === 0) return undefined;
if (colors.length === 1) return decimalRgbToCss(colors[0]);
return `linear-gradient(135deg, ${decimalRgbToCss(colors[0])}, ${decimalRgbToCss(colors[1])})`;
}
function previewAspectRatioForItem(item) {
if (item.type === 0) return 1;
if (item.type === 2) return 448 / 84;
const firstEffect = item.effects?.[0];
if (firstEffect?.width && firstEffect?.height) {
return firstEffect.width / firstEffect.height;
}
return 450 / 880;
}
function formatPaletteName(palette) {
if (!palette) return undefined;
return String(palette)
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
const NAMEPLATE_PALETTE_GRADIENTS = {
crimson: 'linear-gradient(135deg, #5c0a1c, #dc143c)',
berry: 'linear-gradient(135deg, #4a1030, #c42d78)',
sky: 'linear-gradient(135deg, #0a2a5c, #3b8eed)',
teal: 'linear-gradient(135deg, #0a3d3d, #2dd4bf)',
forest: 'linear-gradient(135deg, #0a2e1a, #22c55e)',
bubble_gum: 'linear-gradient(135deg, #4a1038, #f472b6)',
violet: 'linear-gradient(135deg, #2d1050, #8b5cf6)',
cobalt: 'linear-gradient(135deg, #0a1448, #3b5bdb)',
clover: 'linear-gradient(135deg, #0a3d20, #4ade80)',
lemon: 'linear-gradient(135deg, #4a3d0a, #fbbf24)',
white: 'linear-gradient(135deg, #888888, #f0f0f0)',
black: 'linear-gradient(135deg, #1a1a1a, #404040)',
};
function nameplatePaletteGradient(palette) {
if (!palette) return undefined;
return NAMEPLATE_PALETTE_GRADIENTS[palette];
}
function thumbnailForItem(item) {
if (item.thumbnailPreviewSrc && isCdnUrl(item.thumbnailPreviewSrc)) {
return item.thumbnailPreviewSrc;
}
if (item.type === 0 && item.asset) {
return `${CDN_BASE}/avatar-decoration-presets/${item.asset}.png?passthrough=false`;
}
if (item.type === 2 && item.asset) {
const assetPath = String(item.asset).replace(/^\/+/, '');
return `${CDN_BASE}/assets/collectibles/${assetPath}static.png`;
}
const assets = urlsForItem(item);
return assets[0]?.url;
}
function* walkProduct(product, category) {
yield { product, category };
for (const bundled of product.bundled_products ?? []) {
yield* walkProduct(bundled, category);
}
for (const variant of product.variants ?? []) {
yield* walkProduct(variant, category);
}
}
function* walkProducts(categories) {
for (const category of categories ?? []) {
for (const product of category.products ?? []) {
yield* walkProduct(product, category);
}
}
}
function extractCollectibleItems(categories) {
const items = [];
const seenItems = new Set();
for (const { product, category } of walkProducts(categories)) {
const categoryName = category?.name || 'Unknown';
for (const item of product.items ?? []) {
if (![0, 1, 2].includes(item.type)) continue;
const skuId = String(item.sku_id ?? item.id ?? product.sku_id ?? 'unknown');
const itemKey = `${item.type}:${skuId}:${item.id ?? ''}`;
if (seenItems.has(itemKey)) continue;
seenItems.add(itemKey);
const assets = urlsForItem(item);
if (assets.length === 0) continue;
const productName = product.name || item.title || item.label || skuId;
const typeKey = ITEM_TYPE_KEY[item.type];
const entry = {
id: `${typeKey}:${skuId}`,
skuId,
name: productName,
type: typeKey,
category: categoryName,
label: item.label ?? item.title ?? productName,
thumbnailUrl: thumbnailForItem(item),
previewAspectRatio: previewAspectRatioForItem(item),
previewColors: product.styles?.background_colors,
palette: item.type === 2 ? item.palette : undefined,
paletteLabel: item.type === 2 ? formatPaletteName(item.palette) : undefined,
previewGradient: item.type === 2 ? nameplatePaletteGradient(item.palette) : undefined,
assets: assets.map((a) => ({
role: a.role,
url: a.url,
filename: a.filename,
mimeType: guessMimeType(a.filename),
})),
};
if (item.type === 1) {
entry.effect = {
animationType: item.animationType,
thumbnailPreviewSrc: item.thumbnailPreviewSrc,
reducedMotionSrc: item.reducedMotionSrc,
effects: item.effects,
};
}
items.push(entry);
}
}
return items;
}
function isPublishedCatalog(document) {
return (
document &&
document.schema_version === 1 &&
typeof document.type === 'string' &&
Array.isArray(document.items) &&
document.items.every(
(item) =>
item &&
typeof item.id === 'string' &&
typeof item.skuId === 'string' &&
typeof item.type === 'string' &&
Array.isArray(item.assets)
)
);
}
async function fetchPublishedCatalog() {
const documents = await Promise.all(
PUBLISHED_CATALOG_URLS.map(async (url) => {
const response = await fetch(url, { headers: { Accept: 'application/json' } });
if (!response.ok) {
throw new Error(`Published collectibles catalog request failed with HTTP ${response.status}.`);
}
const document = await response.json();
if (!isPublishedCatalog(document)) {
throw new Error('Published collectibles catalog has an unsupported schema.');
}
return document;
})
);
const items = documents.flatMap((document) => document.items);
if (items.length === 0) {
throw new Error('Published collectibles catalog is empty.');
}
return items;
}
async function downloadAsset(url, { retries = 4 } = {}) {
for (let attempt = 0; ; attempt++) {
let res;
try {
res = await fetch(url, {
headers: { 'User-Agent': DEFAULT_USER_AGENT, Accept: '*/*' },
});
} catch (err) {
if (attempt >= retries) throw err;
await sleep(500 * 2 ** attempt);
continue;
}
if (res.status === 429) {
const retryAfter = Number(res.headers.get('retry-after')) || 1;
await sleep((retryAfter + 0.5) * 1000);
continue;
}
if (!res.ok) {
if (attempt >= retries) {
throw new Error(`Failed to download asset: ${res.status} ${res.statusText}`);
}
await sleep(500 * 2 ** attempt);
continue;
}
const buf = Buffer.from(await res.arrayBuffer());
return buf;
}
}
function createDiscordCollectiblesService(store) {
let catalogCache = store.get('discordCollectiblesCatalog') || null;
if (
!catalogCache ||
!Array.isArray(catalogCache.items) ||
typeof catalogCache.fetchedAt !== 'string'
) {
catalogCache = null;
}
let catalogCacheAt = catalogCache?.fetchedAt ? Date.parse(catalogCache.fetchedAt) : 0;
const CATALOG_TTL_MS = 6 * 60 * 60 * 1000;
store.delete('discordCollectiblesToken');
async function getCatalog({ force = false } = {}) {
const now = Date.now();
if (!force && catalogCache && now - catalogCacheAt < CATALOG_TTL_MS) {
return { success: true, data: catalogCache };
}
try {
const items = await fetchPublishedCatalog();
catalogCache = { items, fetchedAt: new Date().toISOString() };
catalogCacheAt = now;
store.set('discordCollectiblesCatalog', catalogCache);
return { success: true, data: catalogCache };
} catch (err) {
if (catalogCache) {
return {
success: true,
data: { ...catalogCache, stale: true },
};
}
return { success: false, error: err instanceof Error ? err.message : String(err) };
}
}
async function downloadAssets(assetUrls) {
const results = [];
for (const asset of assetUrls) {
const buffer = await downloadAsset(asset.url);
results.push({
role: asset.role,
url: asset.url,
filename: asset.filename,
mimeType: asset.mimeType || guessMimeType(asset.filename),
data: buffer,
});
}
return results;
}
return {
getCatalog,
downloadAssets,
ITEM_TYPE_FOLDER,
};
}
module.exports = { createDiscordCollectiblesService, extractCollectibleItems, urlsForItem };

View File

@@ -1,4 +1,4 @@
const { app, BrowserWindow, ipcMain, shell, Tray, Menu, nativeImage, clipboard, session, desktopCapturer } = require('electron');
const { app, BrowserWindow, ipcMain, shell, Tray, Menu, nativeImage, clipboard, session, desktopCapturer, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
const { exec, execFile, execFileSync } = require('child_process');
@@ -8,6 +8,7 @@ const openPkg = require('open');
const open = openPkg.default;
const openApps = openPkg.apps;
const PaarrotAPIServer = require('./api-server');
const { createDiscordCollectiblesService } = require('./discord-collectibles');
const https = require('https');
const http = require('http');
const AdmZip = require('adm-zip');
@@ -104,6 +105,7 @@ function isAppOwnedUrl(targetUrl, currentUrl) {
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
const store = new Store();
const discordCollectibles = createDiscordCollectiblesService(store);
const PROTOCOL_SCHEME = 'paarrot';
let mainWindow = null;
@@ -247,6 +249,10 @@ function extractDeepLinkFromArgv(argv) {
/**
* Converts a paarrot:// deep link into an in-app hash route.
* Supports:
* - Hash routes: paarrot://anything/#/login/server/
* - Path routes (SSO-safe): paarrot://desktop/login/server/?loginToken=...
* Preserves SSO loginToken from the URL query (Synapse inserts it before `#`).
* @param {string} deepLink - The incoming protocol URL.
* @returns {string|null} Hash route value without leading #.
*/
@@ -259,17 +265,63 @@ function deepLinkToHashRoute(deepLink) {
return null;
}
let hashRoute = null;
let loginToken = null;
try {
const parsed = new URL(deepLink);
loginToken = parsed.searchParams.get('loginToken');
if (parsed.hash && parsed.hash.length > 1) {
const hashValue = parsed.hash.slice(1);
hashRoute = hashValue.startsWith('/') ? hashValue : `/${hashValue}`;
} else if (parsed.pathname && parsed.pathname !== '/') {
// paarrot://desktop/login/... → /login/...
hashRoute = parsed.pathname;
if (parsed.search && parsed.search.length > 1) {
// Keep non-loginToken query on the route; loginToken merged below
const params = new URLSearchParams(parsed.search);
params.delete('loginToken');
const rest = params.toString();
if (rest) {
hashRoute += `?${rest}`;
}
}
}
} catch {
const hashIndex = deepLink.indexOf('#');
if (hashIndex === -1) {
return null;
}
if (hashIndex !== -1) {
const hashValue = deepLink.slice(hashIndex + 1);
if (!hashValue) {
return null;
if (hashValue) {
hashRoute = hashValue.startsWith('/') ? hashValue : `/${hashValue}`;
}
}
return hashValue.startsWith('/') ? hashValue : `/${hashValue}`;
const match = deepLink.match(/[?&]loginToken=([^&#]+)/i);
if (match) {
try {
loginToken = decodeURIComponent(match[1]);
} catch {
loginToken = match[1];
}
}
}
if (loginToken) {
if (!hashRoute) {
hashRoute = '/';
}
const queryIndex = hashRoute.indexOf('?');
const pathPart = queryIndex === -1 ? hashRoute : hashRoute.slice(0, queryIndex);
const params = new URLSearchParams(queryIndex === -1 ? '' : hashRoute.slice(queryIndex + 1));
if (!params.has('loginToken')) {
params.set('loginToken', loginToken);
}
const query = params.toString();
hashRoute = query ? `${pathPart}?${query}` : pathPart;
}
return hashRoute;
}
/**
@@ -389,6 +441,129 @@ function syncProtocolRegistrationState() {
console.log(`[Protocol] ${PROTOCOL_SCHEME}:// before=${protocolRegistrationState.before} registerCall=${protocolRegistrationState.registerCall} after=${protocolRegistrationState.after}${protocolRegistrationState.error ? ` error=${protocolRegistrationState.error}` : ''}`);
}
/**
* Refresh Linux xdg-mime association for paarrot:// to the current app desktop file.
* AppImage upgrades often leave a stale desktop id in mimeapps.list.
*/
async function ensureLinuxProtocolHandler() {
const home = app.getPath('home');
const searchDirs = [
path.join(home, '.local', 'share', 'applications'),
'/usr/share/applications',
'/var/lib/flatpak/exports/share/applications',
];
let desktopId = null;
for (const dir of searchDirs) {
try {
if (!fs.existsSync(dir)) continue;
const entries = fs.readdirSync(dir);
// Prefer the newest AppImageLauncher desktop entry when several exist.
const matches = entries
.filter((name) => /paarrot/i.test(name) && name.endsWith('.desktop'))
.map((name) => {
try {
return { name, mtime: fs.statSync(path.join(dir, name)).mtimeMs };
} catch {
return { name, mtime: 0 };
}
})
.sort((a, b) => b.mtime - a.mtime);
if (matches.length > 0) {
desktopId = matches[0].name;
break;
}
} catch {
// keep looking
}
}
if (!desktopId) {
desktopId = 'paarrot.desktop';
}
// Rewrite mimeapps.list so stale AppImage ids cannot win over xdg-mime default.
try {
const mimeappsPath = path.join(home, '.config', 'mimeapps.list');
let content = '';
try {
content = fs.readFileSync(mimeappsPath, 'utf8');
} catch {
content = '';
}
const schemeLine = `x-scheme-handler/${PROTOCOL_SCHEME}=${desktopId}`;
const lines = content ? content.split(/\r?\n/) : [];
let section = null;
const next = [];
let wroteDefault = false;
let wroteAdded = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === '[Default Applications]' || trimmed === '[Added Associations]') {
section = trimmed;
next.push(line);
continue;
}
if (trimmed.startsWith('x-scheme-handler/paarrot=')) {
if (section === '[Default Applications]') {
next.push(schemeLine);
wroteDefault = true;
} else if (section === '[Added Associations]') {
next.push(`${schemeLine};`);
wroteAdded = true;
}
continue;
}
next.push(line);
}
if (!next.some((l) => l.trim() === '[Default Applications]')) {
next.push('', '[Default Applications]');
}
if (!wroteDefault) {
const idx = next.findIndex((l) => l.trim() === '[Default Applications]');
next.splice(idx + 1, 0, schemeLine);
}
if (!next.some((l) => l.trim() === '[Added Associations]')) {
next.unshift('[Added Associations]');
}
if (!wroteAdded) {
const idx = next.findIndex((l) => l.trim() === '[Added Associations]');
next.splice(idx + 1, 0, `${schemeLine};`);
}
fs.mkdirSync(path.dirname(mimeappsPath), { recursive: true });
fs.writeFileSync(mimeappsPath, `${next.join('\n').replace(/\n{3,}/g, '\n\n').trim()}\n`);
console.log(`[Protocol] mimeapps.list → ${desktopId} for x-scheme-handler/${PROTOCOL_SCHEME}`);
} catch (error) {
console.warn('[Protocol] Failed to rewrite mimeapps.list:', error);
}
await new Promise((resolve) => {
execFile(
'xdg-mime',
['default', desktopId, `x-scheme-handler/${PROTOCOL_SCHEME}`],
{ timeout: 5000 },
(error, _stdout, stderr) => {
if (error) {
console.warn(`[Protocol] xdg-mime default failed (${desktopId}):`, error.message, stderr);
} else {
console.log(`[Protocol] xdg-mime default → ${desktopId} for x-scheme-handler/${PROTOCOL_SCHEME}`);
}
resolve();
}
);
});
await new Promise((resolve) => {
execFile('update-desktop-database', [path.join(home, '.local', 'share', 'applications')], { timeout: 5000 }, () => {
resolve();
});
});
}
/**
* Reads a named Windows registry value.
* @param {string} keyPath - Registry key path.
@@ -920,6 +1095,12 @@ app.whenReady().then(() => {
syncProtocolRegistrationState();
if (process.platform === 'linux') {
ensureLinuxProtocolHandler().catch((error) => {
console.warn('[Protocol] Startup Linux protocol ensure failed:', error);
});
}
createWindow();
createTray();
@@ -933,18 +1114,27 @@ app.whenReady().then(() => {
console.error('Failed to start API server:', err);
});
// Check for updates on startup (not in development mode)
if (!isDev) {
// Check for updates on start (after 3 seconds)
// Check for updates on startup
setTimeout(() => {
autoUpdater.checkForUpdates().catch(err => {
if (isDev) {
// Exercise the title-bar updater UI with a mock update.
sendUpdaterEvent('update-available', {
version: '99.0.0-dev',
releaseNotes: 'Mock update for development — download to preview the title-bar progress UI.',
releaseDate: new Date().toISOString(),
mock: true,
});
return;
}
autoUpdater.checkForUpdates().catch((err) => {
console.error('Failed to check for updates:', err);
});
}, 3000);
}, 2500);
if (!isDev) {
// Check for updates every 6 hours
setInterval(() => {
autoUpdater.checkForUpdates().catch(err => {
autoUpdater.checkForUpdates().catch((err) => {
console.error('Failed to check for updates:', err);
});
}, 6 * 60 * 60 * 1000);
@@ -1053,6 +1243,14 @@ ipcMain.handle('protocol:repair', async () => {
syncProtocolRegistrationState();
if (process.platform === 'linux') {
try {
await ensureLinuxProtocolHandler();
} catch (error) {
console.warn('[Protocol] Failed to refresh Linux protocol handler:', error);
}
}
const status = await getProtocolStatusPayload();
return {
success: true,
@@ -1079,14 +1277,104 @@ ipcMain.handle('open-external-url', async (event, url) => {
}
});
/**
* Save a media blob from the renderer via a native Save dialog.
* Payload: { filename, mimeType?, data: Uint8Array | ArrayBuffer | number[] }
*/
ipcMain.handle('media:save-file', async (event, payload = {}) => {
try {
const filename = typeof payload.filename === 'string' && payload.filename.trim()
? path.basename(payload.filename.trim()) || 'download'
: 'download';
const mimeType = typeof payload.mimeType === 'string' ? payload.mimeType : '';
const raw = payload.data;
if (!raw) {
return { success: false, error: 'Missing file data' };
}
const buffer = Buffer.from(
raw instanceof ArrayBuffer
? new Uint8Array(raw)
: ArrayBuffer.isView(raw)
? new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength)
: raw
);
const ext = path.extname(filename).replace(/^\./, '').toLowerCase();
const filters = (() => {
if (mimeType.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'].includes(ext)) {
return [
{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'] },
{ name: 'All Files', extensions: ['*'] },
];
}
if (mimeType.startsWith('video/') || ['mp4', 'webm', 'mkv', 'mov'].includes(ext)) {
return [
{ name: 'Videos', extensions: ['mp4', 'webm', 'mkv', 'mov'] },
{ name: 'All Files', extensions: ['*'] },
];
}
if (mimeType.startsWith('audio/') || ['mp3', 'ogg', 'wav', 'm4a', 'flac'].includes(ext)) {
return [
{ name: 'Audio', extensions: ['mp3', 'ogg', 'wav', 'm4a', 'flac'] },
{ name: 'All Files', extensions: ['*'] },
];
}
if (mimeType === 'application/pdf' || ext === 'pdf') {
return [
{ name: 'PDF', extensions: ['pdf'] },
{ name: 'All Files', extensions: ['*'] },
];
}
if (ext) {
return [
{ name: ext.toUpperCase(), extensions: [ext] },
{ name: 'All Files', extensions: ['*'] },
];
}
return [{ name: 'All Files', extensions: ['*'] }];
})();
const win = BrowserWindow.fromWebContents(event.sender) || mainWindow;
const result = await dialog.showSaveDialog(win || undefined, {
title: 'Save file',
defaultPath: filename,
filters,
});
if (result.canceled || !result.filePath) {
return { success: true, canceled: true };
}
await fs.promises.writeFile(result.filePath, buffer);
return { success: true, path: result.filePath };
} catch (error) {
console.error('[media:save-file] failed:', error);
return { success: false, error: error.message || String(error) };
}
});
// Read clipboard image
ipcMain.handle('read-clipboard-image', async () => {
try {
// Extra Things: only trust an image when the clipboard actually advertises one.
// On Linux, readImage() can return a stale bitmap after copying plain text/URLs.
const formats = clipboard.availableFormats();
const hasImageFormat = formats.some((format) => format.startsWith('image/'));
if (!hasImageFormat) {
return { success: true, data: null };
}
const image = clipboard.readImage();
if (image.isEmpty()) {
return { success: true, data: null };
}
const { width, height } = image.getSize();
if (width < 2 || height < 2) {
return { success: true, data: null };
}
// Convert to PNG base64
const pngBuffer = image.toPNG();
const base64 = pngBuffer.toString('base64');
@@ -1137,33 +1425,33 @@ ipcMain.handle('play-notification-sound', async (event, soundType = 'message') =
}
});
// Get YouTube stream using yt-dlp
ipcMain.handle('get-youtube-stream', async (event, url) => {
// Get a direct YouTube stream using yt-dlp.
async function getYouTubeStream(url) {
try {
// Check if yt-dlp is available
try {
await execAsync('yt-dlp --version');
await execFileAsync('yt-dlp', ['--version']);
} catch {
return {
success: false,
error: 'yt-dlp is not installed. Please install yt-dlp to use YouTube features.'
error: 'yt-dlp is not installed. Please install yt-dlp to use YouTube features.',
};
}
// Get title
let title = 'YouTube Video';
try {
const titleResult = await execAsync(`yt-dlp --get-title "${url}"`);
const titleResult = await execFileAsync('yt-dlp', ['--no-playlist', '--get-title', url]);
title = titleResult.stdout.trim();
} catch (e) {
console.warn('Failed to get video title:', e.message);
}
// Get video URL
const result = await execAsync(
`yt-dlp -g -f "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best" "${url}"`
);
const result = await execFileAsync('yt-dlp', [
'--no-playlist',
'-g',
'-f',
'best[height<=1080]/bestvideo[height<=1080]+bestaudio/best',
url,
]);
const videoUrl = result.stdout.trim().split('\n')[0];
if (!videoUrl) {
@@ -1172,14 +1460,19 @@ ipcMain.handle('get-youtube-stream', async (event, url) => {
return {
success: true,
data: { video_url: videoUrl, title }
data: { video_url: videoUrl, title },
};
} catch (error) {
return {
success: false,
error: `yt-dlp error: ${error.message}`
error: `yt-dlp error: ${error.message}`,
};
}
}
// Get YouTube stream using yt-dlp
ipcMain.handle('get-youtube-stream', async (event, url) => {
return getYouTubeStream(url);
});
// Background sync stubs (desktop doesn't need it)
@@ -1195,10 +1488,61 @@ ipcMain.handle('get-background-sync-state', async () => {
return { success: true, data: 'NotApplicable' };
});
// Auto-updater IPC handlers
// Auto-updater IPC handlers (mock in development so title-bar UI can be exercised)
let mockDownloadTimer = null;
let mockDownloadPercent = 0;
function sendUpdaterEvent(channel, payload) {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(channel, payload);
}
}
function startMockDownload() {
if (mockDownloadTimer) {
clearInterval(mockDownloadTimer);
}
mockDownloadPercent = 0;
sendUpdaterEvent('update-download-progress', {
percent: 0,
transferred: 0,
total: 100,
mock: true,
});
mockDownloadTimer = setInterval(() => {
mockDownloadPercent = Math.min(100, mockDownloadPercent + 4 + Math.random() * 6);
const percent = Math.round(mockDownloadPercent);
sendUpdaterEvent('update-download-progress', {
percent,
transferred: percent,
total: 100,
mock: true,
});
if (percent >= 100) {
clearInterval(mockDownloadTimer);
mockDownloadTimer = null;
sendUpdaterEvent('update-downloaded', {
version: '99.0.0-dev',
releaseNotes: 'Mock update for development.',
mock: true,
});
}
}, 180);
}
ipcMain.handle('check-for-updates', async () => {
if (isDev) {
return { success: false, error: 'Updates are not available in development mode' };
sendUpdaterEvent('update-checking', { mock: true });
await new Promise((resolve) => setTimeout(resolve, 600));
sendUpdaterEvent('update-available', {
version: '99.0.0-dev',
releaseNotes: 'Mock update for development — download to preview the title-bar progress UI.',
releaseDate: new Date().toISOString(),
mock: true,
});
return { success: true, data: { updateInfo: { version: '99.0.0-dev' }, mock: true } };
}
try {
const result = await autoUpdater.checkForUpdates();
@@ -1210,7 +1554,8 @@ ipcMain.handle('check-for-updates', async () => {
ipcMain.handle('download-update', async () => {
if (isDev) {
return { success: false, error: 'Updates are not available in development mode' };
startMockDownload();
return { success: true, data: { mock: true } };
}
try {
await autoUpdater.downloadUpdate();
@@ -1222,7 +1567,12 @@ ipcMain.handle('download-update', async () => {
ipcMain.handle('install-update', () => {
if (isDev) {
return { success: false, error: 'Updates are not available in development mode' };
sendUpdaterEvent('update-not-available', {
version: app.getVersion(),
mock: true,
message: 'Mock install — restart skipped in development.',
});
return { success: true, data: { mock: true } };
}
autoUpdater.quitAndInstall();
return { success: true };
@@ -1230,7 +1580,8 @@ ipcMain.handle('install-update', () => {
ipcMain.handle('download-and-install-update', async () => {
if (isDev) {
return { success: false, error: 'Updates are not available in development mode' };
startMockDownload();
return { success: true, data: { mock: true } };
}
try {
await autoUpdater.downloadUpdate();
@@ -1241,6 +1592,8 @@ ipcMain.handle('download-and-install-update', async () => {
}
});
ipcMain.handle('updater-is-mock', () => ({ success: true, data: { mock: isDev } }));
// Screen capture / desktopCapturer handler
ipcMain.handle('get-desktop-sources', async (event, opts) => {
try {
@@ -1555,6 +1908,35 @@ ipcMain.handle('plugin:read-code', async (event, pluginId) => {
}
});
ipcMain.handle('discord-collectibles:fetch-catalog', async (event, { force } = {}) => {
try {
return await discordCollectibles.getCatalog({ force: Boolean(force) });
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('discord-collectibles:download-assets', async (event, { assets }) => {
try {
if (!Array.isArray(assets) || assets.length === 0) {
return { success: false, error: 'No assets to download' };
}
const downloaded = await discordCollectibles.downloadAssets(assets);
return {
success: true,
data: downloaded.map((item) => ({
role: item.role,
url: item.url,
filename: item.filename,
mimeType: item.mimeType,
data: item.data,
})),
};
} catch (error) {
return { success: false, error: error.message };
}
});
console.log('Paarrot Electron main process started');
console.log('Development mode:', isDev);
console.log('Platform:', process.platform);

View File

@@ -104,6 +104,12 @@ contextBridge.exposeInMainWorld('electron', {
readImage: () => ipcRenderer.invoke('read-clipboard-image'),
writeText: (text) => ipcRenderer.invoke('write-clipboard-text', text)
},
// Media save (native Save dialog for images / videos / files)
media: {
saveFile: ({ filename, mimeType, data }) =>
ipcRenderer.invoke('media:save-file', { filename, mimeType, data }),
},
// Audio
audio: {
playNotificationSound: (soundType = 'message') => ipcRenderer.invoke('play-notification-sound', soundType),
@@ -130,6 +136,7 @@ contextBridge.exposeInMainWorld('electron', {
updater: {
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
downloadAndInstallUpdate: () => ipcRenderer.invoke('download-and-install-update'),
isMock: () => ipcRenderer.invoke('updater-is-mock'),
onUpdateAvailable: (callback) => {
ipcRenderer.on('update-available', (_, info) => callback(info));
},
@@ -139,6 +146,9 @@ contextBridge.exposeInMainWorld('electron', {
onUpdateDownloaded: (callback) => {
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
},
onUpdateNotAvailable: (callback) => {
ipcRenderer.on('update-not-available', (_, info) => callback(info));
},
downloadUpdate: () => ipcRenderer.invoke('download-update'),
installUpdate: () => ipcRenderer.invoke('install-update'),
},
@@ -183,7 +193,13 @@ contextBridge.exposeInMainWorld('electron', {
protocol: {
getStatus: () => ipcRenderer.invoke('protocol:get-status'),
repair: () => ipcRenderer.invoke('protocol:repair')
}
},
// Discord shop collectibles (published catalog + on-demand CDN download)
discordCollectibles: {
fetchCatalog: (force = false) => ipcRenderer.invoke('discord-collectibles:fetch-catalog', { force }),
downloadAssets: (assets) => ipcRenderer.invoke('discord-collectibles:download-assets', { assets }),
},
});
// Preload script ready

View File

@@ -208,6 +208,81 @@
{
"name": "Messaging",
"item": [
{
"name": "Get Group Messages",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/messages/groups?limit=10",
"host": [
"{{baseUrl}}"
],
"path": [
"messages",
"groups"
],
"query": [
{
"key": "limit",
"value": "10",
"description": "Number of recent messages (1-100, default 10)"
}
]
},
"description": "Get the most recent messages across joined group channels (non-DMs)."
}
},
{
"name": "Get DM Messages",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/messages/dms?limit=10",
"host": [
"{{baseUrl}}"
],
"path": [
"messages",
"dms"
],
"query": [
{
"key": "limit",
"value": "10",
"description": "Number of recent messages (1-100, default 10)"
}
]
},
"description": "Get the most recent messages across joined direct messages."
}
},
{
"name": "Get Combined Messages",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/messages/combined?limit=10",
"host": [
"{{baseUrl}}"
],
"path": [
"messages",
"combined"
],
"query": [
{
"key": "limit",
"value": "10",
"description": "Number of recent messages (1-100, default 10)"
}
]
},
"description": "Get the most recent messages across groups and DMs together."
}
},
{
"name": "Send Message to Room",
"request": {

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "paarrot",
"version": "4.11.133",
"version": "4.11.137",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "paarrot",
"version": "4.11.133",
"version": "4.11.137",
"license": "AGPL-3.0-only",
"dependencies": {
"adm-zip": "^0.5.18",

View File

@@ -1,6 +1,6 @@
{
"name": "paarrot",
"version": "4.11.135",
"version": "4.11.191",
"description": "Paarrot - A Matrix client based on Cinny",
"homepage": "https://github.com/Paarrot/Paarrot-Desktop",
"repository": {
@@ -13,13 +13,15 @@
},
"scripts": {
"dev": "concurrently \"npm run dev:vite\" \"npm run dev:electron\"",
"dev:vite": "cd cinny && npm start",
"dev:vite": "cross-env BROWSER=none sh -c 'cd cinny && npm start'",
"dev:electron": "wait-on http://localhost:38347 && cross-env NODE_ENV=development electron .",
"build": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder --publish always",
"build:local": "node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder",
"build": "node scripts/generate-update-manifest.mjs && node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder --publish always",
"build:local": "node scripts/generate-update-manifest.mjs && node -e \"require('fs').copyFileSync('config.json', 'cinny/config.json')\" && cd cinny && npm run build && cd .. && electron-builder",
"build:linux": "npm run build -- --linux",
"build:win": "npm run build -- --win",
"postman:generate": "node scripts/generate-postman-collection.js"
"postman:generate": "node scripts/generate-postman-collection.js",
"generate:update-manifest": "node scripts/generate-update-manifest.mjs",
"playground": "npm --prefix cinny run playground"
},
"keywords": [],
"author": {
@@ -47,5 +49,9 @@
"png2icons": "2.0.1",
"sharp": "0.35.3",
"wait-on": "9.0.10"
},
"allowScripts": {
"electron-winstaller@5.4.0": true,
"electron@43.1.0": true
}
}

View File

@@ -164,6 +164,87 @@ const collection = {
{
name: 'Messaging',
item: [
{
name: 'Get Group Messages',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/messages/groups?limit=10',
host: ['{{baseUrl}}'],
path: ['messages', 'groups'],
query: [{ key: 'limit', value: '10', description: 'Number of recent messages (1-100, default 10)' }],
},
description: 'Get the most recent messages across joined group channels (non-DMs).',
},
},
{
name: 'Get DM Messages',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/messages/dms?limit=10',
host: ['{{baseUrl}}'],
path: ['messages', 'dms'],
query: [{ key: 'limit', value: '10', description: 'Number of recent messages (1-100, default 10)' }],
},
description: 'Get the most recent messages across joined direct messages.',
},
},
{
name: 'Get Combined Messages',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/messages/combined?limit=10',
host: ['{{baseUrl}}'],
path: ['messages', 'combined'],
query: [{ key: 'limit', value: '10', description: 'Number of recent messages (1-100, default 10)' }],
},
description: 'Get the most recent messages across groups and DMs together.',
},
},
{
name: 'Get Group Unreads',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/unreads/groups',
host: ['{{baseUrl}}'],
path: ['unreads', 'groups'],
},
description: 'List unread group conversations (non-DMs), including counts and latest message preview.',
},
},
{
name: 'Get DM Unreads',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/unreads/dms',
host: ['{{baseUrl}}'],
path: ['unreads', 'dms'],
},
description: 'List unread DM conversations, including counts and latest message preview.',
},
},
{
name: 'Get Combined Unreads',
request: {
method: 'GET',
header: [],
url: {
raw: '{{baseUrl}}/unreads/combined',
host: ['{{baseUrl}}'],
path: ['unreads', 'combined'],
},
description: 'List unread conversations across groups and DMs.',
},
},
{
name: 'Send Message to Room',
request: {

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env node
/**
* Desktop repo wrapper — manifest generator lives in cinny/scripts/.
*/
import { spawnSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
const script = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'cinny',
'scripts',
'generate-update-manifest.mjs'
);
const result = spawnSync(process.execPath, [script], { stdio: 'inherit' });
process.exit(result.status ?? 1);

View File

@@ -18,6 +18,17 @@
"updater:allow-download",
"updater:allow-download-and-install",
"dialog:default",
"fs:default",
"fs:allow-write-file",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$HOME/**" },
{ "path": "$DOWNLOAD/**" },
{ "path": "$DESKTOP/**" },
{ "path": "$DOCUMENT/**" }
]
},
"process:allow-restart",
{
"identifier": "http:default",

View File

@@ -61,6 +61,16 @@ async function runTests() {
// Get current room
await testEndpoint('Get Current Room', 'GET', '/room/current');
// Recent messages by scope
await testEndpoint('Get Group Messages', 'GET', '/messages/groups');
await testEndpoint('Get DM Messages', 'GET', '/messages/dms');
await testEndpoint('Get Combined Messages', 'GET', '/messages/combined?limit=10');
// Unread conversations by scope
await testEndpoint('Get Group Unreads', 'GET', '/unreads/groups');
await testEndpoint('Get DM Unreads', 'GET', '/unreads/dms');
await testEndpoint('Get Combined Unreads', 'GET', '/unreads/combined');
// Toggle mute
await testEndpoint('Toggle Mute', 'POST', '/mute/toggle');