release: 1.7.88
Some checks are pending
Deploy to GitHub Pages / deploy (push) Waiting to run

This commit is contained in:
SkateSpots 2026-11-02 17:43:04 +07:00
parent eebc9c06f4
commit c8428f2f78
9 changed files with 56 additions and 28 deletions

View file

@ -7,10 +7,10 @@ account is required.
- Forge: <https://git.skate-spots.org/skatespots/SkateSpots>
- Latest release: <https://git.skate-spots.org/skatespots/SkateSpots/releases/latest>
- Signed APK (1.7.87):
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.87/SkateSpots-1.7.87.apk>
- Signed APK (1.7.88):
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.88/SkateSpots-1.7.88.apk>
- SHA-256:
`269be76e9e7c67bd0d7167137c9a5767765a46c1f7e6459a126092ad9de5fe7e`
`d7e713618c4c57105fc690a42f3c0845536d05491bbf1b5f925d8d7b8d272450`
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

View file

@ -1,7 +1,7 @@
# SkateSpots QA matrix
Last updated: 2026-11-02
App release under test: **1.7.87** (versionCode 10787)
App release under test: **1.7.88** (versionCode 10788)
This is a best-effort test record, not a certification. “PASS” means the named
check was actually run in this environment. Physical-device and real-money
@ -22,11 +22,11 @@ checks are explicitly marked untested until someone runs them.
| Channel | Version | Result | Notes |
| --- | --- | --- | --- |
| Web | 1.7.87 | PASS | Production deploy from `06c1d64` |
| Zapstore | 1.7.87 | PASS | Release event + current icon on relay |
| Forgejo | 1.7.87 | PASS | APK + `.sha256` assets |
| F-Droid self-hosted repo | 1.7.87 | PASS | `index-v2.json` lists versionCode 10787 |
| Download page | dynamic | PASS | Live `/api/latest-release` returns 1.7.87; static fallback is 1.7.87 |
| Web | 1.7.88 | PASS | Production deploy from `06c1d64` |
| Zapstore | 1.7.88 | PASS | Release event + current icon on relay |
| Forgejo | 1.7.88 | PASS | APK + `.sha256` assets |
| F-Droid self-hosted repo | 1.7.88 | PASS | `index-v2.json` lists versionCode 10788 |
| Download page | dynamic | PASS | Live `/api/latest-release` returns 1.7.88; static fallback is 1.7.88 |
## Still requires a real device

View file

@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="referrer" content="strict-origin-when-cross-origin" />
<link rel="preconnect" href="https://a.tile.openstreetmap.org" crossorigin />
<link rel="preconnect" href="https://tile.openstreetmap.org" crossorigin />
<link rel="dns-prefetch" href="//tile.openstreetmap.org" />
<title>SkateSpots — Open-sourced Skate Community App</title>
<!--

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "skate-spots",
"version": "1.7.87",
"version": "1.7.88",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "skate-spots",
"version": "1.7.87",
"version": "1.7.88",
"license": "AGPL-3.0-only",
"dependencies": {
"@bitcoinerlab/secp256k1": "^1.2.0",

View file

@ -1,7 +1,7 @@
{
"name": "skate-spots",
"private": true,
"version": "1.7.87",
"version": "1.7.88",
"license": "AGPL-3.0-only",
"type": "module",
"homepage": "https://skate-spots.org",

View file

@ -99,16 +99,16 @@
<a
class="download"
id="download-link"
href="https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.87/SkateSpots-1.7.87.apk"
href="https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.88/SkateSpots-1.7.88.apk"
rel="noopener"
>
<span id="download-label">Download SkateSpots-1.7.87.apk · 60 MB</span>
<span id="download-label">Download SkateSpots-1.7.88.apk · 60 MB</span>
<span aria-hidden="true">↓</span>
</a>
<div class="meta">
<span>Requires Android 7.0 or newer.</span>
<span>SHA-256: <code id="download-sha256">269be76e9e7c67bd0d7167137c9a5767765a46c1f7e6459a126092ad9de5fe7e</code></span>
<span>SHA-256: <code id="download-sha256">d7e713618c4c57105fc690a42f3c0845536d05491bbf1b5f925d8d7b8d272450</code></span>
<span>Only install APKs from a source you trust. Android will ask you to allow installs from your browser.</span>
</div>

View file

@ -1,3 +1,11 @@
🛹 Faster open + community chat recovery
- Shortened the branded splash: reveal is capped at 0.48s and the previous artificial waits are gone.
- Added early connections to OpenStreetMap tiles and the SkateSpots relay so the map starts warming sooner.
- Cordn join/check/device-link actions now have a short local cooldown, so repeated taps cannot flood the coordinator or Amber and trigger rate limits.
- Restarted the community room bot; existing members who are adding a new phone should use "Link this device" from an already-joined device, or ask to join again once.
---
🛹 Channel and QA cleanup
- The download page now reads the latest Forgejo release automatically, so its APK link, version and SHA-256 cannot drift from the actual release.
- The self-hosted F-Droid repository is back in sync at 1.7.86.

View file

@ -23,11 +23,11 @@ import { useTranslation } from '@/contexts/language';
*/
const SESSION_KEY = 'splash-shown-session';
const MIN_MS = 380; // keep the brand beat, lose the artificial wait
const WARM_GRACE_MS = 180; // a short breath after relays connect
const MAX_MS = 900; // hard cap — reveal the shell even on a dead network
const FADE_MS = 280; // iris-close exit
const READY_HOLD_MS = 120; // show the ready check, then move
const MIN_MS = 140; // keep a brand beat, not an artificial wait
const WARM_GRACE_MS = 70; // a short breath after relays connect
const MAX_MS = 480; // hard cap — reveal the shell even on a dead network
const FADE_MS = 170; // iris-close exit
const READY_HOLD_MS = 0; // reveal immediately once ready
const SPARKS = [
{ sx: '-8px', sy: '18px', ex: '-54px', ey: '44px', delay: '0ms' },

View file

@ -212,6 +212,18 @@ export function CordnNativePanel({
const fileRef = useRef<HTMLInputElement>(null);
const checkingRef = useRef(false);
const loadedRequestsRef = useRef(false);
// A short local cooldown prevents a manual double-tap (or repeated retries)
// from flooding the coordinator/Amber and triggering rate limits.
const [cooldownUntil, setCooldownUntil] = useState(0);
const [, setCooldownTick] = useState(0);
useEffect(() => {
if (cooldownUntil <= Date.now()) return;
const id = window.setInterval(() => setCooldownTick(Date.now()), 1000);
return () => window.clearInterval(id);
}, [cooldownUntil]);
const coolingDown = cooldownUntil > Date.now();
const { mutateAsync: uploadFile, isPending: uploading } = useUploadFile();
const inviteUrl = useMemo(() => {
@ -575,10 +587,12 @@ export function CordnNativePanel({
}, [deviceLinkCode]);
const addScannedDevice = useCallback(async (raw: string) => {
if (coolingDown) return;
setCooldownUntil(Date.now() + 20_000);
setNotice(null);
const ok = await native.addDevice(raw);
setNotice(ok ? t('cordnUi.deviceLinked') : t('cordnUi.deviceLinkFailed'));
}, [native, t]);
}, [native, t, coolingDown]);
const checkNow = useCallback(async () => {
if (checkingRef.current) return false;
@ -591,12 +605,14 @@ export function CordnNativePanel({
}, [checkInviteNative]);
const handleRequestJoin = useCallback(async () => {
if (coolingDown) return;
setCooldownUntil(Date.now() + 20_000);
setNotice(null);
const requested = await native.requestJoin();
if (!requested || !user?.pubkey) return;
writePendingJoin(user.pubkey, native.groupId);
setAwaiting(true);
}, [native, user?.pubkey]);
}, [native, user?.pubkey, coolingDown]);
const handleApprove = useCallback(async (request: { pk: string; kp_ref: string; at: number }) => {
const ok = await native.approve(
@ -611,7 +627,8 @@ export function CordnNativePanel({
}, [native, t]);
const handleCheckInvite = useCallback(async () => {
if (native.busy) return;
if (native.busy || coolingDown) return;
setCooldownUntil(Date.now() + 10_000);
setNotice(null);
const joined = await checkNow();
if (joined) {
@ -620,7 +637,7 @@ export function CordnNativePanel({
return;
}
if (!awaiting) setNotice(t(noInviteKey));
}, [checkNow, awaiting, t, noInviteKey, native.busy, user?.pubkey, native.groupId]);
}, [checkNow, awaiting, t, noInviteKey, native.busy, user?.pubkey, native.groupId, coolingDown]);
// Keep the latest checker in a ref: the poll must not restart just because a
// parent re-render gave the hook callbacks new identities.
@ -939,7 +956,7 @@ export function CordnNativePanel({
<>
<Button
size="sm"
disabled={!native.ready || native.busy || awaiting}
disabled={!native.ready || native.busy || awaiting || coolingDown}
onClick={() => void handleRequestJoin()}
className="gap-1.5 bg-teal-500 font-semibold text-black hover:bg-teal-400"
>
@ -949,7 +966,7 @@ export function CordnNativePanel({
<Button
size="sm"
variant="outline"
disabled={!native.ready || native.busy}
disabled={!native.ready || native.busy || coolingDown}
onClick={() => void handleCheckInvite()}
className="gap-1.5"
>
@ -960,7 +977,7 @@ export function CordnNativePanel({
<Button
size="sm"
variant="outline"
disabled={!native.ready || native.busy}
disabled={!native.ready || native.busy || coolingDown}
onClick={() => void openDeviceLink()}
className="gap-1.5"
>
@ -989,7 +1006,7 @@ export function CordnNativePanel({
{awaiting && !deviceLinkPending && (
<button
type="button"
disabled={!native.ready || native.busy}
disabled={!native.ready || native.busy || coolingDown}
onClick={() => void handleRequestJoin()}
className="mx-auto block text-[11px] text-teal-300 underline underline-offset-2 hover:text-teal-200 disabled:opacity-50"
>