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> - Forge: <https://git.skate-spots.org/skatespots/SkateSpots>
- Latest release: <https://git.skate-spots.org/skatespots/SkateSpots/releases/latest> - Latest release: <https://git.skate-spots.org/skatespots/SkateSpots/releases/latest>
- Signed APK (1.7.87): - Signed APK (1.7.88):
<https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.87/SkateSpots-1.7.87.apk> <https://git.skate-spots.org/skatespots/SkateSpots/releases/download/v1.7.88/SkateSpots-1.7.88.apk>
- SHA-256: - SHA-256:
`269be76e9e7c67bd0d7167137c9a5767765a46c1f7e6459a126092ad9de5fe7e` `d7e713618c4c57105fc690a42f3c0845536d05491bbf1b5f925d8d7b8d272450`
The APK is signed with the same key as the Zapstore build. Installing an 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 F-Droid-signed build over it would require uninstalling SkateSpots and losing

View file

@ -1,7 +1,7 @@
# SkateSpots QA matrix # SkateSpots QA matrix
Last updated: 2026-11-02 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 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 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 | | Channel | Version | Result | Notes |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Web | 1.7.87 | PASS | Production deploy from `06c1d64` | | Web | 1.7.88 | PASS | Production deploy from `06c1d64` |
| Zapstore | 1.7.87 | PASS | Release event + current icon on relay | | Zapstore | 1.7.88 | PASS | Release event + current icon on relay |
| Forgejo | 1.7.87 | PASS | APK + `.sha256` assets | | Forgejo | 1.7.88 | PASS | APK + `.sha256` assets |
| F-Droid self-hosted repo | 1.7.87 | PASS | `index-v2.json` lists versionCode 10787 | | 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.87; static fallback is 1.7.87 | | Download page | dynamic | PASS | Live `/api/latest-release` returns 1.7.88; static fallback is 1.7.88 |
## Still requires a real device ## Still requires a real device

View file

@ -4,6 +4,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="referrer" content="strict-origin-when-cross-origin" /> <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> <title>SkateSpots — Open-sourced Skate Community App</title>
<!-- <!--

4
package-lock.json generated
View file

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

View file

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

View file

@ -99,16 +99,16 @@
<a <a
class="download" class="download"
id="download-link" 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" 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> <span aria-hidden="true">↓</span>
</a> </a>
<div class="meta"> <div class="meta">
<span>Requires Android 7.0 or newer.</span> <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> <span>Only install APKs from a source you trust. Android will ask you to allow installs from your browser.</span>
</div> </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 🛹 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 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. - 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 SESSION_KEY = 'splash-shown-session';
const MIN_MS = 380; // keep the brand beat, lose the artificial wait const MIN_MS = 140; // keep a brand beat, not an artificial wait
const WARM_GRACE_MS = 180; // a short breath after relays connect const WARM_GRACE_MS = 70; // a short breath after relays connect
const MAX_MS = 900; // hard cap — reveal the shell even on a dead network const MAX_MS = 480; // hard cap — reveal the shell even on a dead network
const FADE_MS = 280; // iris-close exit const FADE_MS = 170; // iris-close exit
const READY_HOLD_MS = 120; // show the ready check, then move const READY_HOLD_MS = 0; // reveal immediately once ready
const SPARKS = [ const SPARKS = [
{ sx: '-8px', sy: '18px', ex: '-54px', ey: '44px', delay: '0ms' }, { 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 fileRef = useRef<HTMLInputElement>(null);
const checkingRef = useRef(false); const checkingRef = useRef(false);
const loadedRequestsRef = 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 { mutateAsync: uploadFile, isPending: uploading } = useUploadFile();
const inviteUrl = useMemo(() => { const inviteUrl = useMemo(() => {
@ -575,10 +587,12 @@ export function CordnNativePanel({
}, [deviceLinkCode]); }, [deviceLinkCode]);
const addScannedDevice = useCallback(async (raw: string) => { const addScannedDevice = useCallback(async (raw: string) => {
if (coolingDown) return;
setCooldownUntil(Date.now() + 20_000);
setNotice(null); setNotice(null);
const ok = await native.addDevice(raw); const ok = await native.addDevice(raw);
setNotice(ok ? t('cordnUi.deviceLinked') : t('cordnUi.deviceLinkFailed')); setNotice(ok ? t('cordnUi.deviceLinked') : t('cordnUi.deviceLinkFailed'));
}, [native, t]); }, [native, t, coolingDown]);
const checkNow = useCallback(async () => { const checkNow = useCallback(async () => {
if (checkingRef.current) return false; if (checkingRef.current) return false;
@ -591,12 +605,14 @@ export function CordnNativePanel({
}, [checkInviteNative]); }, [checkInviteNative]);
const handleRequestJoin = useCallback(async () => { const handleRequestJoin = useCallback(async () => {
if (coolingDown) return;
setCooldownUntil(Date.now() + 20_000);
setNotice(null); setNotice(null);
const requested = await native.requestJoin(); const requested = await native.requestJoin();
if (!requested || !user?.pubkey) return; if (!requested || !user?.pubkey) return;
writePendingJoin(user.pubkey, native.groupId); writePendingJoin(user.pubkey, native.groupId);
setAwaiting(true); setAwaiting(true);
}, [native, user?.pubkey]); }, [native, user?.pubkey, coolingDown]);
const handleApprove = useCallback(async (request: { pk: string; kp_ref: string; at: number }) => { const handleApprove = useCallback(async (request: { pk: string; kp_ref: string; at: number }) => {
const ok = await native.approve( const ok = await native.approve(
@ -611,7 +627,8 @@ export function CordnNativePanel({
}, [native, t]); }, [native, t]);
const handleCheckInvite = useCallback(async () => { const handleCheckInvite = useCallback(async () => {
if (native.busy) return; if (native.busy || coolingDown) return;
setCooldownUntil(Date.now() + 10_000);
setNotice(null); setNotice(null);
const joined = await checkNow(); const joined = await checkNow();
if (joined) { if (joined) {
@ -620,7 +637,7 @@ export function CordnNativePanel({
return; return;
} }
if (!awaiting) setNotice(t(noInviteKey)); 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 // Keep the latest checker in a ref: the poll must not restart just because a
// parent re-render gave the hook callbacks new identities. // parent re-render gave the hook callbacks new identities.
@ -939,7 +956,7 @@ export function CordnNativePanel({
<> <>
<Button <Button
size="sm" size="sm"
disabled={!native.ready || native.busy || awaiting} disabled={!native.ready || native.busy || awaiting || coolingDown}
onClick={() => void handleRequestJoin()} onClick={() => void handleRequestJoin()}
className="gap-1.5 bg-teal-500 font-semibold text-black hover:bg-teal-400" className="gap-1.5 bg-teal-500 font-semibold text-black hover:bg-teal-400"
> >
@ -949,7 +966,7 @@ export function CordnNativePanel({
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
disabled={!native.ready || native.busy} disabled={!native.ready || native.busy || coolingDown}
onClick={() => void handleCheckInvite()} onClick={() => void handleCheckInvite()}
className="gap-1.5" className="gap-1.5"
> >
@ -960,7 +977,7 @@ export function CordnNativePanel({
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
disabled={!native.ready || native.busy} disabled={!native.ready || native.busy || coolingDown}
onClick={() => void openDeviceLink()} onClick={() => void openDeviceLink()}
className="gap-1.5" className="gap-1.5"
> >
@ -989,7 +1006,7 @@ export function CordnNativePanel({
{awaiting && !deviceLinkPending && ( {awaiting && !deviceLinkPending && (
<button <button
type="button" type="button"
disabled={!native.ready || native.busy} disabled={!native.ready || native.busy || coolingDown}
onClick={() => void handleRequestJoin()} onClick={() => void handleRequestJoin()}
className="mx-auto block text-[11px] text-teal-300 underline underline-offset-2 hover:text-teal-200 disabled:opacity-50" className="mx-auto block text-[11px] text-teal-300 underline underline-offset-2 hover:text-teal-200 disabled:opacity-50"
> >