Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ce57bedf9 | ||
|
|
bfc777c18b | ||
|
|
a2b732317e | ||
|
|
5ebf741427 | ||
|
|
4ae30ee8e6 | ||
|
|
72e5f89b49 | ||
|
|
d003748bb1 | ||
|
|
6048eafc0d | ||
|
|
562a7de7c0 | ||
|
|
8b12001fa6 | ||
|
|
383ff8a5cf | ||
|
|
65914144d1 | ||
|
|
a8780d5236 | ||
|
|
2dcf5a2aad | ||
|
|
50c95d0ff7 | ||
|
|
4ff736734d | ||
|
|
fd1713f6b1 | ||
|
|
6c9b6fab02 | ||
|
|
0ae4c59630 | ||
|
|
ab11b0d3ed |
21 changed files with 588 additions and 15 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -52,7 +52,8 @@ stats.json
|
|||
|
||||
# Local/generated assets & incidental lockfiles (do not commit)
|
||||
/assets/
|
||||
/resources/
|
||||
/resources/*
|
||||
!/resources/splash.png
|
||||
/bun.lock
|
||||
cvm-server/bun.lock
|
||||
push-service/bun.lock
|
||||
|
|
|
|||
84
AGENTS.md
84
AGENTS.md
|
|
@ -2228,7 +2228,7 @@ session with **no other memory of this project**. So:
|
|||
instead of showing the error boundary.
|
||||
- **Baselines** (verified 2026-10-29): `npm test` → **0 errors, 0 warnings**,
|
||||
**156 tests**, build ok. The gate also type-checks the CVM server
|
||||
(`tsc -p cvm-server/tsconfig.json`). `package.json` version **1.7.58**.
|
||||
(`tsc -p cvm-server/tsconfig.json`). `package.json` version **1.7.61**.
|
||||
- ✅ Lint is **clean**: `no-explicit-any` = 0 · `react-hooks/exhaustive-deps` = 0
|
||||
· `no-unused-vars` = 0 · `react-refresh` = 0. Non-component exports were
|
||||
**split into their own modules** (`contexts/language.ts`, `lib/sortOptions.ts`,
|
||||
|
|
@ -2415,6 +2415,75 @@ Rules:
|
|||
- Any time the deploy output isn't verified by the smoke test, run
|
||||
`node scripts/smoke-test.mjs` manually before shipping.
|
||||
|
||||
### Distribution without GitHub
|
||||
> **Owner decision (2026-10-30): no external identities.** Distribution must
|
||||
> stay self-hosted and Nostr-based. Do **not** create or ask for GitLab,
|
||||
> Codeberg, GitHub, email or F-Droid-main accounts. The local Forgejo admin
|
||||
> account and the self-hosted F-Droid repo key are infrastructure credentials,
|
||||
> not third-party identities. F-Droid main and IzzyOnDroid are therefore
|
||||
> **out of scope**.
|
||||
|
||||
- **Self-hosted Forgejo:** `https://git.skate-spots.org/skatespots/SkateSpots`
|
||||
(public source, registration disabled, admin credentials only on the relay
|
||||
host). It runs on `185.18.221.2` behind Caddy; do not commit its credentials.
|
||||
- **Latest release (Obtanium HTML source):**
|
||||
`https://git.skate-spots.org/skatespots/SkateSpots/releases/latest`.
|
||||
The page exposes one signed `.apk`; Obtanium may need **Override Source →
|
||||
HTML** because the host is self-hosted, not Codeberg.
|
||||
- **Static download page:** `public/download/index.html` → `https://skate-spots.org/download/`.
|
||||
- **Forgejo release helper:** `scripts/publish-forgejo.sh` (uses `FORGEJO_TOKEN`
|
||||
or `FORGEJO_PASSWORD`; `scripts/release.sh` calls it automatically when either
|
||||
is set). It uploads the APK and checksum, but does not push git history.
|
||||
- **Self-hosted F-Droid repo:** `https://fdroid.skate-spots.org/repo`
|
||||
(`org.skatespots.app`; currently 1.7.61). It is a **third-party repo**, not
|
||||
F-Droid main. Repo signing fingerprint:
|
||||
`88:B1:71:1C:24:02:89:FB:B2:A7:FC:C1:B3:1F:BB:9D:60:F5:7F:19:18:E0:3D:62:2D:35:F2:3D:E1:84:46:75`.
|
||||
The developer APK signature is unchanged; the repo key only signs indexes.
|
||||
A backup of the repo keystore/config is in
|
||||
`~/.config/skatespots/fdroid-repo/` (mode 600).
|
||||
- **F-Droid main/IzzyOnDroid:** **out of scope by owner decision.** Both require
|
||||
external GitLab/Codeberg identities and review; do not submit, create accounts,
|
||||
or present them as live. The self-hosted F-Droid repo above is the supported
|
||||
F-Droid-family channel.
|
||||
- **No GitHub required.** Keep Zapstore, Forgejo, the self-hosted F-Droid repo,
|
||||
and the static page in sync for each release.
|
||||
|
||||
### Future release / distribution checklist
|
||||
1. Bump `package.json`; update the top of `release-notes.txt`; run
|
||||
`scripts/release.sh patch --yes --deploy`.
|
||||
2. Set `FORGEJO_TOKEN` (or `FORGEJO_PASSWORD`) to upload the APK/checksum to the
|
||||
latest Forgejo release; the helper is `scripts/publish-forgejo.sh`.
|
||||
3. **Push source/tags to Forgejo.** The local machine's DNS for
|
||||
`git.skate-spots.org` is intermittent, so the reliable path is: create a git
|
||||
bundle locally, `scp` it to the relay, clone there, and push to
|
||||
`http://127.0.0.1:3000/skatespots/SkateSpots.git` with the host admin
|
||||
credentials. Do not put the password in a committed remote URL.
|
||||
4. Add the APK to `/home/ubuntu/fdroid/repo/`, update the F-Droid metadata
|
||||
changelog, run `fdroid update` in `/home/ubuntu/fdroid`, then rsync `repo/`
|
||||
to `/var/www/fdroid/repo/`.
|
||||
5. Update `public/download/index.html` with the new version/checksum and deploy
|
||||
the web page.
|
||||
6. Verify: latest Forgejo release exposes one `.apk`; static page 200;
|
||||
`https://fdroid.skate-spots.org/repo/index-v2.json` lists the new
|
||||
`versionCode`; Obtanium still resolves the latest release.
|
||||
7. Keep Forgejo/F-Droid credentials and the F-Droid repo keystore out of git.
|
||||
Local backups: `~/.config/skatespots/forgejo-admin.txt` and
|
||||
`~/.config/skatespots/fdroid-repo/` (mode 600).
|
||||
|
||||
### Nostr-only distribution (no external identities)
|
||||
- **Zapstore** is the Nostr-native app store/update channel; keep publishing
|
||||
release events there.
|
||||
- **Obtanium** is URL-based, not an account: users add the latest Forgejo
|
||||
release URL (HTML source if needed) or the self-hosted F-Droid repo.
|
||||
- **F-Droid main and IzzyOnDroid do not support Nostr identities.** They are
|
||||
intentionally not pursued. Do not create or request GitLab/Codeberg/email
|
||||
accounts, and do not claim those stores are live.
|
||||
- The local Forgejo admin account, the relay, and the F-Droid repo signing key
|
||||
are self-hosted infrastructure, not third-party identities.
|
||||
- The F-Droid recipe remains in `distribution/fdroiddata/` only as an unused
|
||||
technical reference. It was locally test-built at `5ebf741`, but must not be
|
||||
submitted.
|
||||
|
||||
### Deploy
|
||||
- **Web (auto)**: a systemd user timer (`skatespots-deploy.timer`) runs `scripts/auto-deploy.sh` every ~3 min. When `origin/main` changes it builds and runs `netlify deploy --prod --dir=dist`. It deploys **only committed code** (skips if tracked files are modified) and never mutates a dirty tree. **Doc/scripts-only commits are skipped** (matches `AGENTS.md`/`*.md`/`scripts/`/`cvm-server/`/`skills/`) — Netlify bills **15 credits per production deploy**, so don't spend them on non-site changes. One-time setup: `netlify login` + `netlify link`. Logs: `journalctl --user -u skatespots-deploy`. Manual run: `scripts/auto-deploy.sh`.
|
||||
- ⚠️ **Deploy throttle (2026-09-28):** production deploys are capped to one per **24h** by default (after a week of 68 deploys ≈ **1,020 credits** — a third of the monthly allowance). Batching a day's commits into one 15-credit deploy is the single biggest credit lever. Override with `SKATESPOTS_DEPLOY_MIN_INTERVAL=<seconds>` (`0` disables); deploy on demand with **`scripts/auto-deploy.sh --force`**. A throttled tick deliberately does **not** record the deployed SHA, so the pending change still deploys on a later tick; a **failed** deploy doesn't set the clock, so it retries immediately.
|
||||
|
|
@ -2789,6 +2858,19 @@ Rules:
|
|||
signed-in user’s profile on mobile and desktop; signed out it still opens
|
||||
the map. Bottom Spots / desktop Map remains the home shortcut. APK sha256
|
||||
`0d7bdad7…`, versionCode 10758. Web live at `74bd140`.
|
||||
- **1.7.59 (2026-10-30):** owner/admin Delete in spot/shop card menus; mobile
|
||||
Back arrow on every non-map screen (history fallback to map); avatar account
|
||||
menu trimmed to status/profile/settings/accounts/logout; brand wordmark
|
||||
hidden on sub-pages for narrow-header fit. APK sha256 `0fd4a149…`,
|
||||
versionCode 10759. Web live at `0355c10`.
|
||||
- **1.7.60 (2026-10-30):** Cordn header shows last successful check; sellers
|
||||
see payment-readiness in Create Listing + offers; uploads resize photos to
|
||||
1600px WebP when smaller (GIF/undecodable formats untouched), cutting
|
||||
quota/storage/upload time. APK sha256 `8f11c229…`, versionCode 10760.
|
||||
Web live at `6c9b6fa`.
|
||||
- **1.7.61 (2026-10-30):** Cordn setup timeline now checks prepared device
|
||||
/sent request and spins on the approval step while waiting. APK sha256
|
||||
`db0279f7…`, versionCode 10761. Web live at `50c95d0`.
|
||||
|
||||
### Relay archive (dataset backup)
|
||||
- **Transfer content between keys (`scripts/transfer-content.mjs`, 2026-10-04):**
|
||||
|
|
|
|||
97
distribution/README.md
Normal file
97
distribution/README.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Distribution without GitHub
|
||||
|
||||
SkateSpots is published from a self-hosted **Forgejo** instance. No GitHub
|
||||
account is required.
|
||||
|
||||
## Current release source
|
||||
|
||||
- Forge: <https://git.skate-spots.org/skatespots/SkateSpots>
|
||||
- Latest release: <https://git.skate-spots.org/skatespots/SkateSpots/releases/latest>
|
||||
- Signed APK (1.7.61):
|
||||
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.61/SkateSpots-1.7.61.apk>
|
||||
- SHA-256:
|
||||
`db0279f759efd9fa05d9d1bed57a3f5856777e6ccb208d3ed78b15398f266fba`
|
||||
|
||||
The APK is signed with the same key as the Zapstore build. Installing an
|
||||
F-Droid-signed build over it would require uninstalling SkateSpots and losing
|
||||
the local key/settings. Obtanium and direct APK updates keep the install.
|
||||
|
||||
## Obtanium
|
||||
|
||||
Obtanium cannot use a Zapstore URL as a source. Use the self-hosted Forgejo
|
||||
release page instead:
|
||||
|
||||
1. Open Obtanium → **Add App**.
|
||||
2. Paste:
|
||||
`https://git.skate-spots.org/skatespots/SkateSpots/releases/latest`
|
||||
3. If Obtanium does not recognise the self-hosted host, select **Override
|
||||
Source → HTML**.
|
||||
4. Set version extraction if desired:
|
||||
`SkateSpots-([0-9.]+)\.apk`, match group `1`.
|
||||
|
||||
The release page contains exactly one `.apk`, so Obtanium can follow the latest
|
||||
release without a custom store listing.
|
||||
|
||||
## F-Droid family
|
||||
|
||||
### Self-hosted F-Droid repo (live now)
|
||||
|
||||
A signed third-party F-Droid repository is published here:
|
||||
|
||||
- Repo: <https://fdroid.skate-spots.org/repo>
|
||||
- App ID: `org.skatespots.app`
|
||||
- Repo signing fingerprint:
|
||||
`88:B1:71:1C:24:02:89:FB:B2:A7:FC:C1:B3:1F:BB:9D:60:F5:7F:19:18:E0:3D:62:2D:35:F2:3D:E1:84:46:75`
|
||||
|
||||
Users can add this URL in F-Droid, or add it in Obtanium as an **F-Droid Third
|
||||
Party Repo**. This is not F-Droid main: it is a self-hosted, signed repository
|
||||
of the developer-signed APK. It keeps the existing install and signing key.
|
||||
|
||||
The repository metadata and APK are served from the relay host. The repo index
|
||||
is signed with a repository key stored on that host; the APK itself keeps the
|
||||
SkateSpots developer signature.
|
||||
|
||||
### No external store identities
|
||||
|
||||
**Owner decision:** distribution stays self-hosted and Nostr-based. No GitLab,
|
||||
Codeberg, GitHub, email or F-Droid-main accounts are created or requested.
|
||||
|
||||
F-Droid main and IzzyOnDroid both require external identities and review, so
|
||||
they are intentionally out of scope. The self-hosted repo above is the supported
|
||||
F-Droid-family channel.
|
||||
|
||||
`distribution/fdroiddata/` and `fastlane/` are kept as an unused technical
|
||||
reference only. A fresh local clone at `5ebf741` built the unsigned APK through
|
||||
the recipe's `npm ci → android:init → android:sync → assembleRelease` path, but
|
||||
the recipe must not be submitted.
|
||||
|
||||
## Release checklist
|
||||
|
||||
1. `npm test`
|
||||
2. `scripts/release.sh patch --yes --deploy` (Zapstore + web)
|
||||
3. Push the release tag and `main` to Forgejo.
|
||||
4. Upload the APK through the helper:
|
||||
`FORGEJO_TOKEN=... scripts/publish-forgejo.sh <version>`
|
||||
(or set `FORGEJO_TOKEN` before `release.sh`; the APK upload is then automatic)
|
||||
5. Copy the signed APK into the F-Droid repo on the relay host, add a
|
||||
`metadata/org.skatespots.app.yml` changelog entry, run `fdroid update`, and
|
||||
sync `repo/` to `/var/www/fdroid/repo/`:
|
||||
|
||||
```bash
|
||||
cd /home/ubuntu/fdroid
|
||||
cp /path/to/SkateSpots-<version>.apk repo/
|
||||
. /home/ubuntu/fdroid-venv/bin/activate
|
||||
fdroid update
|
||||
sudo rsync -a --delete repo/ /var/www/fdroid/repo/
|
||||
sudo chown -R caddy:caddy /var/www/fdroid
|
||||
```
|
||||
|
||||
6. Verify the public latest-release page contains the new `.apk`, and that
|
||||
`https://fdroid.skate-spots.org/repo/index-v2.json` lists the new
|
||||
`versionCode`.
|
||||
7. Update the static download page if the displayed version/checksum changes:
|
||||
`public/download/index.html`.
|
||||
|
||||
The Forgejo instance runs on the same host as the relay. It exposes public
|
||||
read access and HTTPS through Caddy; registration is disabled and administrative
|
||||
credentials are kept on the host, not in this repository.
|
||||
51
distribution/fdroiddata/org.skatespots.app.yml
Normal file
51
distribution/fdroiddata/org.skatespots.app.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
Categories:
|
||||
- Sports & Health
|
||||
License: AGPL-3.0-only
|
||||
AuthorName: SkateSpots
|
||||
WebSite: https://skate-spots.org
|
||||
SourceCode: https://git.skate-spots.org/skatespots/SkateSpots
|
||||
IssueTracker: https://git.skate-spots.org/skatespots/SkateSpots/issues
|
||||
Changelog: https://git.skate-spots.org/skatespots/SkateSpots/releases
|
||||
|
||||
AutoName: SkateSpots
|
||||
Summary: Local-first skate map, crews, events and marketplace
|
||||
Description: |-
|
||||
SkateSpots is a local-first skating map for finding spots, crews, events and
|
||||
gear. Your keys stay on your device, your messages stay in encrypted crew
|
||||
chats and the app does not use analytics or sell your data.
|
||||
|
||||
Find skate spots on the map, add new ones, organise sessions with your crew,
|
||||
follow events and browse the community marketplace. The Android app bundles
|
||||
the same web app for fast shell startup and native camera, location, push and
|
||||
NFC support where available.
|
||||
|
||||
SkateSpots is self-hosted and built on open protocols. You can run your own
|
||||
relay and Blossom storage instead of relying on a central service.
|
||||
|
||||
RepoType: git
|
||||
Repo: https://git.skate-spots.org/skatespots/SkateSpots.git
|
||||
|
||||
Builds:
|
||||
- versionName: 1.7.61
|
||||
versionCode: 10761
|
||||
commit: 5ebf741427303be0b0feeff5ce1d8420e2c0ddea
|
||||
subdir: .
|
||||
prebuild:
|
||||
- npm ci
|
||||
- npm run android:init
|
||||
- npm run android:sync
|
||||
# F-Droid signs the APK itself. Remove the developer signing config so
|
||||
# Gradle emits an unsigned release APK instead of requiring the keystore.
|
||||
- sed -i '/signingConfig signingConfigs.release/d' android/app/build.gradle
|
||||
build:
|
||||
- cd android && ./gradlew assembleRelease --no-daemon
|
||||
output: android/app/build/outputs/apk/release/app-release-unsigned.apk
|
||||
# The generated Android project is intentionally not committed. This
|
||||
# recipe recreates it with Capacitor before Gradle runs. F-Droid will sign
|
||||
# the resulting APK with its own key. The F-Droid buildserver supplies the
|
||||
# JDK/Android SDK; do not install packages with sudo in a recipe.
|
||||
|
||||
AutoUpdateMode: Version
|
||||
UpdateCheckMode: Tags
|
||||
CurrentVersion: 1.7.61
|
||||
CurrentVersionCode: 10761
|
||||
1
fastlane/metadata/android/en-US/changelogs/10761.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/10761.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Cordn setup now shows visible progress: the device-key step is checked once prepared, and the approval step shows a spinner while waiting. The APK uses the same signing key as the Zapstore release.
|
||||
15
fastlane/metadata/android/en-US/full_description.txt
Normal file
15
fastlane/metadata/android/en-US/full_description.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
SkateSpots is a local-first skating map for finding spots, crews, events and
|
||||
gear. Your keys stay on your device, your messages stay in encrypted crew chats
|
||||
and the app does not use analytics or sell your data.
|
||||
|
||||
Find skate spots on the map, add new ones, organise sessions with your crew,
|
||||
follow events and browse the community marketplace. The Android app bundles the
|
||||
same web app for fast offline shell startup and native camera, location, push
|
||||
and NFC support where available.
|
||||
|
||||
SkateSpots is self-hosted and built on open protocols. You can run your own
|
||||
relay and Blossom storage instead of relying on a central service.
|
||||
|
||||
Lightning payments are optional and self-custodial. Transport and driver
|
||||
profiles are self-attested; SkateSpots does not verify a licence, insurance or
|
||||
vehicle roadworthiness.
|
||||
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Local-first skate map, crews, events and marketplace.
|
||||
1
fastlane/metadata/android/en-US/title.txt
Normal file
1
fastlane/metadata/android/en-US/title.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
SkateSpots
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"version": "1.7.59",
|
||||
"version": "1.7.61",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "skate-spots",
|
||||
"version": "1.7.59",
|
||||
"version": "1.7.61",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@bitcoinerlab/secp256k1": "^1.2.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"private": true,
|
||||
"version": "1.7.59",
|
||||
"version": "1.7.61",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"homepage": "https://skate-spots.org",
|
||||
|
|
|
|||
127
public/download/index.html
Normal file
127
public/download/index.html
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>SkateSpots APK</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Download the signed SkateSpots Android APK. No account, no store, no tracking."
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #090b0c;
|
||||
--panel: #121719;
|
||||
--line: #273033;
|
||||
--text: #f2f5f4;
|
||||
--muted: #9aa7a5;
|
||||
--teal: #56e0c0;
|
||||
--amber: #f5b83d;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(circle at 20% 0%, rgba(86, 224, 192, 0.12), transparent 42%),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
}
|
||||
main {
|
||||
width: min(100%, 560px);
|
||||
padding: 28px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
background: color-mix(in srgb, var(--panel) 92%, transparent);
|
||||
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.eyebrow {
|
||||
margin: 0 0 10px;
|
||||
color: var(--teal);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
h1 { margin: 0; font-size: clamp(28px, 7vw, 38px); line-height: 1.02; }
|
||||
p { margin: 14px 0 0; color: var(--muted); }
|
||||
a.download {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 12px;
|
||||
background: var(--teal);
|
||||
color: #06201b;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.download:focus-visible { outline: 3px solid var(--amber); outline-offset: 3px; }
|
||||
a.download span:last-child { font-size: 20px; }
|
||||
.meta {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
code {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--text);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.links { display: flex; flex-wrap: wrap; gap: 10px 18px; margin-top: 16px; }
|
||||
.links a { color: var(--amber); font-weight: 700; text-decoration: none; }
|
||||
.links a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<p class="eyebrow">SkateSpots · Android</p>
|
||||
<h1>Get the signed APK.</h1>
|
||||
<p>
|
||||
Local-first skating map, crews, events and marketplace. No store account
|
||||
required. This is the same signing key as the Zapstore and Obtanium
|
||||
releases, so it updates over an existing install.
|
||||
</p>
|
||||
|
||||
<a
|
||||
class="download"
|
||||
href="https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.61/SkateSpots-1.7.61.apk"
|
||||
rel="noopener"
|
||||
>
|
||||
<span>Download SkateSpots-1.7.61.apk · 56 MB</span>
|
||||
<span aria-hidden="true">↓</span>
|
||||
</a>
|
||||
|
||||
<div class="meta">
|
||||
<span>Requires Android 7.0 or newer.</span>
|
||||
<span>SHA-256: <code>db0279f759efd9fa05d9d1bed57a3f5856777e6ccb208d3ed78b15398f266fba</code></span>
|
||||
<span>Only install APKs from a source you trust. Android will ask you to allow installs from your browser.</span>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<a href="https://git.skate-spots.org/skatespots/SkateSpots/releases">All releases</a>
|
||||
<a href="https://git.skate-spots.org/skatespots/SkateSpots">Source code</a>
|
||||
<a href="https://skate-spots.org">Open the web app</a>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<span><strong>Obtanium:</strong> add the latest release page above as the app source, or select <em>Override Source → HTML</em> if it is not recognised.</span>
|
||||
<span><strong>F-Droid repo:</strong> <a href="https://fdroid.skate-spots.org/repo">https://fdroid.skate-spots.org/repo</a> (third-party self-hosted repo; app ID <code>org.skatespots.app</code>).</span>
|
||||
<span>Repo signing fingerprint: <code>88:B1:71:1C:24:02:89:FB:B2:A7:FC:C1:B3:1F:BB:9D:60:F5:7F:19:18:E0:3D:62:2D:35:F2:3D:E1:84:46:75</code></span>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,3 +1,22 @@
|
|||
🧩 Cordn setup is easier to follow
|
||||
- The setup steps now show progress: preparing this device is checked once the
|
||||
key package is ready, and the approval step shows a spinner while waiting.
|
||||
- The private-chat header already shows the last successful check, so “live” and
|
||||
“still waiting” are distinguishable at a glance.
|
||||
|
||||
---
|
||||
|
||||
⚡ Cordn status, payment readiness, lighter photos
|
||||
- Cordn panels now show the last successful check next to the connection status,
|
||||
so “waiting” and “live” are easier to tell apart.
|
||||
- Sellers see a payment-readiness card in listing creation and on incoming
|
||||
offers, with a direct link to add a Lightning address before accepting a deal.
|
||||
- Photos are resized to max 1600px and converted to WebP before upload when that
|
||||
makes them smaller. GIFs and formats the WebView cannot decode are left alone.
|
||||
This cuts upload time, quota use and Blossom storage.
|
||||
|
||||
---
|
||||
|
||||
🧹 Spot owners can delete from the card menu
|
||||
- Owner/admin members now get a **Delete** action in the three-dot menu on
|
||||
spot and shop cards, opening the existing confirmation dialog.
|
||||
|
|
|
|||
BIN
resources/splash.png
Normal file
BIN
resources/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
108
scripts/publish-forgejo.sh
Executable file
108
scripts/publish-forgejo.sh
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Publish a built APK to the self-hosted Forgejo release page.
|
||||
#
|
||||
# Usage:
|
||||
# FORGEJO_TOKEN=... scripts/publish-forgejo.sh 1.7.62 [path/to.apk]
|
||||
# FORGEJO_PASSWORD=... FORGEJO_USER=skatespots scripts/publish-forgejo.sh 1.7.62
|
||||
#
|
||||
# This uploads the APK and its .sha256 file. It does not push the git tag or
|
||||
# source; do that separately (the Forgejo repo is a mirror, not an auto-push
|
||||
# remote). Keep credentials in your shell/environment, never in this repo.
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:?usage: publish-forgejo.sh <version> [apk]}"
|
||||
APK="${2:-android/app/build/outputs/apk/release/app-release.apk}"
|
||||
TAG="${FORGEJO_TAG:-v$VERSION}"
|
||||
BASE="${FORGEJO_URL:-https://git.skate-spots.org}"
|
||||
OWNER="${FORGEJO_OWNER:-skatespots}"
|
||||
REPO="${FORGEJO_REPO:-SkateSpots}"
|
||||
API="$BASE/api/v1/repos/$OWNER/$REPO"
|
||||
|
||||
command -v curl >/dev/null || { echo "curl is required" >&2; exit 1; }
|
||||
command -v python3 >/dev/null || { echo "python3 is required" >&2; exit 1; }
|
||||
[ -f "$APK" ] || { echo "APK not found: $APK" >&2; exit 1; }
|
||||
|
||||
AUTH=()
|
||||
if [ -n "${FORGEJO_TOKEN:-}" ]; then
|
||||
AUTH=(-H "Authorization: token $FORGEJO_TOKEN")
|
||||
elif [ -n "${FORGEJO_PASSWORD:-}" ]; then
|
||||
AUTH=(-u "${FORGEJO_USER:-skatespots}:$FORGEJO_PASSWORD")
|
||||
else
|
||||
echo "set FORGEJO_TOKEN (preferred) or FORGEJO_PASSWORD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
sha="$(sha256sum "$APK" | awk '{print $1}')"
|
||||
body="$tmpdir/release.md"
|
||||
cat >"$body" <<EOF
|
||||
## SkateSpots $VERSION
|
||||
|
||||
Signed Android release. SHA-256: \`$sha\`
|
||||
|
||||
- Source: $BASE/$OWNER/$REPO
|
||||
- APK signing key: same as the Zapstore release.
|
||||
EOF
|
||||
|
||||
python3 - "$TAG" "$VERSION" "$body" >"$tmpdir/payload.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
tag, version, body_path = sys.argv[1:]
|
||||
print(json.dumps({
|
||||
"tag_name": tag,
|
||||
"name": version,
|
||||
"body": Path(body_path).read_text(),
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
}))
|
||||
PY
|
||||
|
||||
create_code="$(curl -sS -o "$tmpdir/release.json" -w '%{http_code}' \
|
||||
"${AUTH[@]}" -H 'Content-Type: application/json' \
|
||||
-X POST "$API/releases" --data-binary @"$tmpdir/payload.json" || true)"
|
||||
|
||||
if [ "$create_code" = "201" ]; then
|
||||
release_id="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$tmpdir/release.json")"
|
||||
elif [ "$create_code" = "409" ] || [ "$create_code" = "422" ]; then
|
||||
curl -fsS "${AUTH[@]}" "$API/releases/tags/$TAG" -o "$tmpdir/release.json"
|
||||
release_id="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$tmpdir/release.json")"
|
||||
else
|
||||
echo "Forgejo release creation failed (HTTP $create_code)" >&2
|
||||
cat "$tmpdir/release.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sha_file="$tmpdir/SkateSpots-$VERSION.apk.sha256"
|
||||
printf '%s SkateSpots-%s.apk\n' "$sha" "$VERSION" >"$sha_file"
|
||||
|
||||
existing_assets="$(python3 - "$tmpdir/release.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
data = json.load(open(sys.argv[1]))
|
||||
print("\n".join(asset["name"] for asset in data.get("assets", [])))
|
||||
PY
|
||||
)"
|
||||
|
||||
for file in "$APK" "$sha_file"; do
|
||||
name="$(basename "$file")"
|
||||
if printf '%s\n' "$existing_assets" | grep -Fqx "$name"; then
|
||||
echo "already present: $name"
|
||||
continue
|
||||
fi
|
||||
code="$(curl -sS -o "$tmpdir/asset.json" -w '%{http_code}' \
|
||||
"${AUTH[@]}" -X POST "$API/releases/$release_id/assets?name=$name" \
|
||||
-F "attachment=@$file" || true)"
|
||||
case "$code" in
|
||||
201|202) echo "uploaded $name" ;;
|
||||
*) echo "warning: upload of $name returned HTTP $code" >&2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "release: $BASE/$OWNER/$REPO/releases/tag/$TAG"
|
||||
echo "latest: $BASE/$OWNER/$REPO/releases/latest"
|
||||
|
|
@ -126,6 +126,14 @@ for _ in 1 2 3 4 5 6; do
|
|||
done
|
||||
[ "$FOUND" -eq 1 ] || log "warning: the release event did not appear on the relay yet (it usually propagates shortly)"
|
||||
|
||||
if [ -n "${FORGEJO_TOKEN:-}" ] || [ -n "${FORGEJO_PASSWORD:-}" ]; then
|
||||
log "publishing the APK to the self-hosted Forgejo release page…"
|
||||
scripts/publish-forgejo.sh "$VERSION" "$APK" || \
|
||||
log "warning: Forgejo upload failed — the Zapstore release is still valid"
|
||||
else
|
||||
log "Forgejo: skipped (set FORGEJO_TOKEN or FORGEJO_PASSWORD to publish there)"
|
||||
fi
|
||||
|
||||
log "committing the release…"
|
||||
git add package.json package-lock.json release-notes.txt
|
||||
git commit -m "release: $VERSION" >/dev/null
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { genUserName } from '@/lib/genUserName';
|
|||
import type { MarketplaceItem } from '@/lib/types';
|
||||
import { PayWithSats } from '@/components/PayWithSats';
|
||||
import { MarkSoldDialog } from '@/components/MarkSoldDialog';
|
||||
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
||||
import { Handshake, CheckCircle, X, Loader2, ArrowLeftRight, Undo2, Zap } from 'lucide-react';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
|
|
@ -183,6 +184,8 @@ export function ListingOffersPanel({ item, isOwner }: { item: MarketplaceItem; i
|
|||
<Handshake className="h-4 w-4" /> {isOwner ? t('marketUi.offers.titleOwner', { count: mine.length }) : t('marketUi.offers.titleBuyer')}
|
||||
</h3>
|
||||
|
||||
{isOwner && <PaymentReadinessNote />}
|
||||
|
||||
{mine.map((thread) => {
|
||||
const latest = thread.latest;
|
||||
const other = latest.author === user.pubkey
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export function PaymentReadinessNote() {
|
|||
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-yellow-400" />
|
||||
<span>
|
||||
{t('marketUi.paymentReadiness.notReadyBefore')}{' '}
|
||||
<Link to="/settings" className="text-yellow-400 underline hover:text-yellow-300">
|
||||
<Link to="/settings?section=profile" className="text-yellow-400 underline hover:text-yellow-300">
|
||||
{t('marketUi.paymentReadiness.nostrProfile')}
|
||||
</Link>{' '}
|
||||
{t('marketUi.paymentReadiness.notReadyAfter')}
|
||||
|
|
|
|||
|
|
@ -814,6 +814,11 @@ export function CordnNativePanel({
|
|||
</div>
|
||||
</div>
|
||||
<p className="pt-1 text-xs text-gray-500">{copy.subtitle}</p>
|
||||
{native.group && native.lastCheckedAt && (
|
||||
<p className="pt-0.5 text-[10px] text-gray-600">
|
||||
{t('cordnUi.lastChecked')} {formatDistanceToNow(new Date(native.lastCheckedAt), { addSuffix: true })}
|
||||
</p>
|
||||
)}
|
||||
{!isCommunity && (
|
||||
<details className="group mt-1 rounded-md border border-white/10 bg-black/20 px-2.5 py-1.5">
|
||||
<summary className="flex cursor-pointer list-none items-center gap-1 text-[11px] text-gray-400">
|
||||
|
|
@ -870,14 +875,29 @@ export function CordnNativePanel({
|
|||
</p>
|
||||
|
||||
<ol className="mx-auto max-w-sm space-y-2 text-left">
|
||||
{steps.map((step, index) => (
|
||||
<li key={step} className="flex items-start gap-2 text-xs text-gray-400">
|
||||
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-teal-500/15 text-[10px] font-bold text-teal-300">
|
||||
{index + 1}
|
||||
{steps.map((step, index) => {
|
||||
// Members see the setup move visibly: device key prepared →
|
||||
// request sent → waiting for an admin. Founders start at step 1.
|
||||
const stepDone = !canCreate && index === 0 && (awaiting || native.keyPackageReady);
|
||||
const stepWaiting = !canCreate && index === 1 && awaiting;
|
||||
return (
|
||||
<li
|
||||
key={step}
|
||||
className={cn(
|
||||
'flex items-start gap-2 text-xs',
|
||||
stepDone || stepWaiting ? 'text-teal-200' : 'text-gray-400',
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
'mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] font-bold',
|
||||
stepDone ? 'bg-teal-500/20 text-teal-300' : 'bg-teal-500/15 text-teal-300',
|
||||
)}>
|
||||
{stepDone ? <Check className="h-2.5 w-2.5" /> : stepWaiting ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : index + 1}
|
||||
</span>
|
||||
<span>{step}</span>
|
||||
</li>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
|
||||
<p className="mx-auto max-w-sm text-[11px] text-gray-500">
|
||||
|
|
|
|||
|
|
@ -47,6 +47,41 @@ function validateImage(file: File): void {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize/compress camera photos before upload. Most phone photos are far larger
|
||||
* than the app ever displays; 1600px WebP cuts upload time and Blossom storage
|
||||
* without touching GIFs or formats the WebView cannot decode.
|
||||
*/
|
||||
async function prepareImage(file: File): Promise<File> {
|
||||
const type = (file.type || '').toLowerCase();
|
||||
if (type === 'image/gif' || type === 'image/heic' || type === 'image/heif') return file;
|
||||
if (typeof createImageBitmap !== 'function' || typeof document === 'undefined') return file;
|
||||
try {
|
||||
const bitmap = await createImageBitmap(file);
|
||||
const scale = Math.min(1, 1600 / Math.max(bitmap.width, bitmap.height));
|
||||
// Already small enough: keep the original bytes/format.
|
||||
if (scale === 1 && file.size <= 1_200_000) {
|
||||
bitmap.close();
|
||||
return file;
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
||||
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) { bitmap.close(); return file; }
|
||||
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||
bitmap.close();
|
||||
const blob = await new Promise<Blob | null>((resolve) => {
|
||||
canvas.toBlob(resolve, 'image/webp', 0.84);
|
||||
});
|
||||
if (!blob || blob.size >= file.size) return file;
|
||||
const base = file.name.replace(/\.[^.]+$/, '') || 'image';
|
||||
return new File([blob], `${base}.webp`, { type: 'image/webp', lastModified: file.lastModified });
|
||||
} catch {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
function hostOf(url: string): string {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
|
|
@ -65,6 +100,7 @@ export function useUploadFile() {
|
|||
}
|
||||
|
||||
validateImage(file);
|
||||
const prepared = await prepareImage(file);
|
||||
|
||||
const uploader = new BlossomUploader({
|
||||
servers: UPLOAD_SERVERS,
|
||||
|
|
@ -73,7 +109,7 @@ export function useUploadFile() {
|
|||
|
||||
let tags;
|
||||
try {
|
||||
tags = await uploader.upload(file);
|
||||
tags = await uploader.upload(prepared);
|
||||
} catch (error) {
|
||||
throw new Error(uploadErrorMessage(error));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { useListingRestrictions, recordListingCreation } from '@/hooks/useListin
|
|||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
import { Nip05Required } from '@/components/Nip05Required';
|
||||
import { SpamFeePayment } from '@/components/SpamFeePayment';
|
||||
import { PaymentReadinessNote } from '@/components/PaymentReadinessNote';
|
||||
import { MARKETPLACE_ITEM_KIND, GEAR_CATEGORIES, GEAR_CONDITIONS, PRICE_TYPES, SIZE_OPTIONS, TRUCK_HEIGHTS, WHEEL_HARDNESS, EXPIRATION_OPTIONS, CURRENCY_OPTIONS, CATEGORY_REQUIRED_FIELDS } from '@/lib/types';
|
||||
import { GEAR_CATEGORY_KEYS, GEAR_CONDITION_KEYS, GEAR_CONDITION_DESC_KEYS, PRICE_TYPE_KEYS, PRICE_TYPE_DESC_KEYS } from '@/lib/labelKeys';
|
||||
import { ShoppingBag, Upload, X, Loader2, ArrowLeft, MapPin, AlertTriangle, CheckCircle, Search, Clock, Shield, Zap } from 'lucide-react';
|
||||
|
|
@ -857,6 +858,8 @@ export default function CreateListingPage() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{listingType === 'selling' && <PaymentReadinessNote />}
|
||||
|
||||
{priceType === 'fixed' && (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 space-y-2">
|
||||
|
|
|
|||
Loading…
Reference in a new issue