Skip to content
This repository was archived by the owner on May 22, 2026. It is now read-only.

Commit ec2b70b

Browse files
NihilDigitclaude
andcommitted
chore(relay): clarify settings copy, isolate countdown, drop FCM-token row
clarify + distill (settings): - intro paragraph drops the "13 MB / FCM / 高优先级" implementation jargon and the dead "下方" reference (which only made sense after a code was already showing). Two sentences: what the problem is, what this fixes. - "每次只能绑一台" was misleading — multi-device is supported via the per-user FCM Hash. Replaced with "每个码单次有效", which describes the actual constraint. extract: - pair-code countdown moves into <PairCodeCard> with its own remaining-state. Settings's 9-key store subscription is no longer re-evaluated every 250 ms while a code is live. typeset (compose): - BindScreen / BoundScreen titles drop from displayMedium (44sp, emotional-peak tier) to headlineLarge (32sp). Setup is a quiet ritual, not a celebration moment — those slots stay reserved for settle / journey peaks. - StatusRow's "USER" English label conflicted with surrounding Chinese body. Becomes "用户". The FCM-token-prefix row is removed outright: useful only for self-debug during bring-up, reads as engineering noise to a user who already saw the user-id confirm. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent edc5fb7 commit ec2b70b

3 files changed

Lines changed: 85 additions & 51 deletions

File tree

‎android-relay/app/src/main/java/dev/nihildigit/tokmato/MainActivity.kt‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ private fun BoundScreen(userId: String, onRebind: () -> Unit) {
131131
)
132132
Text(
133133
text = stringResource(R.string.bound_title),
134-
style = MaterialTheme.typography.displayMedium,
134+
style = MaterialTheme.typography.headlineLarge,
135135
color = MaterialTheme.colorScheme.onSurface,
136136
)
137137
Text(
@@ -142,8 +142,10 @@ private fun BoundScreen(userId: String, onRebind: () -> Unit) {
142142

143143
Spacer(Modifier.height(8.dp))
144144

145-
StatusRow(label = "USER", value = userId)
146-
StatusRow(label = "FCM", value = AppPrefs(ctx).fcmToken?.take(24)?.plus("…") ?: "—")
145+
// FCM token row removed: it was useful for self-debug while
146+
// bringing relay up but reads as engineering noise to a user
147+
// who's already seen the user-id row confirm the bind.
148+
StatusRow(label = "用户", value = userId)
147149

148150
Spacer(Modifier.height(16.dp))
149151

‎android-relay/app/src/main/java/dev/nihildigit/tokmato/ui/BindScreen.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ fun BindScreen(onBound: (userId: String) -> Unit) {
107107
)
108108
Text(
109109
text = stringResource(R.string.bind_title),
110-
style = MaterialTheme.typography.displayMedium,
110+
style = MaterialTheme.typography.headlineLarge,
111111
color = MaterialTheme.colorScheme.onSurface,
112112
)
113113
Text(

‎app/settings/page.tsx‎

Lines changed: 79 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -111,35 +111,21 @@ export default function SettingsPage() {
111111
const [pushBusy, setPushBusy] = useState(false);
112112
const [pushMsg, setPushMsg] = useState<string>("");
113113

114-
// tokmato-relay (Android FCM helper) pair-code state.
114+
// tokmato-relay (Android FCM helper) pair-code state. The countdown
115+
// tick lives inside <PairCodeCard> so its 4 Hz setState doesn't
116+
// ripple through this page's 9-selector store subscription every
117+
// 250 ms.
115118
const [pairCode, setPairCode] = useState<string | null>(null);
116119
const [pairExpiresAt, setPairExpiresAt] = useState(0);
117120
const [pairBusy, setPairBusy] = useState(false);
118121
const [pairMsg, setPairMsg] = useState("");
119-
const [pairTick, setPairTick] = useState(0);
120122

121123
useEffect(() => {
122124
setPushSupported(isPushSupported());
123125
if (!isPushSupported()) return;
124126
void getCurrentSubscription().then((sub) => setPushOn(Boolean(sub)));
125127
}, []);
126128

127-
// Drive the pair-code countdown. The interval only runs while a code
128-
// is live, so on idle we don't burn ticks.
129-
useEffect(() => {
130-
if (!pairCode || pairExpiresAt === 0) return;
131-
const id = setInterval(() => {
132-
const remaining = Math.max(0, pairExpiresAt - Date.now());
133-
setPairTick(remaining);
134-
if (remaining === 0) {
135-
setPairCode(null);
136-
setPairExpiresAt(0);
137-
clearInterval(id);
138-
}
139-
}, 250);
140-
return () => clearInterval(id);
141-
}, [pairCode, pairExpiresAt]);
142-
143129
const handleCreatePairCode = async () => {
144130
setPairBusy(true);
145131
setPairMsg("");
@@ -166,14 +152,18 @@ export default function SettingsPage() {
166152
}
167153
setPairCode(json.code);
168154
setPairExpiresAt(json.expiresAt);
169-
setPairTick(json.expiresAt - Date.now());
170155
} catch {
171156
setPairMsg("网络问题,再试一次");
172157
} finally {
173158
setPairBusy(false);
174159
}
175160
};
176161

162+
const clearPairCode = () => {
163+
setPairCode(null);
164+
setPairExpiresAt(0);
165+
};
166+
177167
const handleEnablePush = async () => {
178168
setPushBusy(true);
179169
setPushMsg("");
@@ -477,40 +467,20 @@ export default function SettingsPage() {
477467
<Section title="Android 推送增强器">
478468
<div className="flex flex-col gap-3">
479469
<p className="text-sm text-ink-3 leading-relaxed">
480-
Android Chrome 上的 Web Push 受 Doze 节流,锁屏几小时后通知就漏掉了。
481-
tokmato-relay 是个 13 MB 的薄壳 APK,只接 FCM 高优先级推送、点击跳本站。
482-
输入下方 4 位码完成绑定。
470+
Android Chrome 的 Web Push 在锁屏几小时后会被节流,通知容易漏。
471+
装一个原生 APK 接管这条通道。
483472
</p>
484473
{pairCode ? (
485-
<output
486-
aria-label={`配对码 ${pairCode.split("").join(" ")},${Math.ceil(pairTick / 1000)} 秒后失效`}
487-
className="flex flex-col gap-3 rounded-lg border border-rule bg-paper-2 px-4 py-4"
488-
>
489-
<div className="flex items-baseline gap-3" aria-hidden="true">
490-
<span className="smallcaps text-ink-3">配对码</span>
491-
<span className="ml-auto text-xs text-ink-3">
492-
{Math.ceil(pairTick / 1000)}s 后失效
493-
</span>
494-
</div>
495-
<div
496-
className="flex justify-between gap-2 font-mono"
497-
aria-hidden="true"
498-
>
499-
{pairCode.split("").map((d, i) => (
500-
<span
501-
key={i}
502-
className="flex-1 rounded-md border border-rule bg-paper py-3 text-center text-h2 text-ink"
503-
>
504-
{d}
505-
</span>
506-
))}
507-
</div>
508-
</output>
474+
<PairCodeCard
475+
code={pairCode}
476+
expiresAt={pairExpiresAt}
477+
onExpire={clearPairCode}
478+
/>
509479
) : (
510480
<GhostRow
511481
Icon={Smartphone}
512482
title="生成配对码"
513-
sub="60 秒内输到 APK 里。每次只能绑一台。"
483+
sub="60 秒内输到 APK 里。每个码单次有效。"
514484
disabled={pairBusy}
515485
onClick={handleCreatePairCode}
516486
/>
@@ -777,6 +747,68 @@ export default function SettingsPage() {
777747

778748
/* ───────────────────────── helpers ───────────────────────── */
779749

750+
function PairCodeCard({
751+
code,
752+
expiresAt,
753+
onExpire,
754+
}: {
755+
code: string;
756+
expiresAt: number;
757+
onExpire: () => void;
758+
}) {
759+
// Tick lives here so the 4 Hz setState only re-renders this card,
760+
// not the entire SettingsPage tree (which carries a 9-key store
761+
// subscription).
762+
const [remaining, setRemaining] = useState(() =>
763+
Math.max(0, expiresAt - Date.now()),
764+
);
765+
766+
useEffect(() => {
767+
let active = true;
768+
const id = setInterval(() => {
769+
if (!active) return;
770+
const r = Math.max(0, expiresAt - Date.now());
771+
setRemaining(r);
772+
if (r === 0) {
773+
active = false;
774+
clearInterval(id);
775+
onExpire();
776+
}
777+
}, 250);
778+
return () => {
779+
active = false;
780+
clearInterval(id);
781+
};
782+
}, [expiresAt, onExpire]);
783+
784+
const seconds = Math.ceil(remaining / 1000);
785+
786+
return (
787+
<output
788+
aria-label={`配对码 ${code.split("").join(" ")},${seconds} 秒后失效`}
789+
className="flex flex-col gap-3 rounded-lg border border-rule bg-paper-2 px-4 py-4"
790+
>
791+
<div className="flex items-baseline gap-3" aria-hidden="true">
792+
<span className="smallcaps text-ink-3">配对码</span>
793+
<span className="ml-auto text-xs text-ink-3">{seconds}s 后失效</span>
794+
</div>
795+
<div
796+
className="flex justify-between gap-2 font-mono"
797+
aria-hidden="true"
798+
>
799+
{code.split("").map((d, i) => (
800+
<span
801+
key={i}
802+
className="flex-1 rounded-md border border-rule bg-paper py-3 text-center text-h2 text-ink"
803+
>
804+
{d}
805+
</span>
806+
))}
807+
</div>
808+
</output>
809+
);
810+
}
811+
780812
function Hairline() {
781813
return <div className="h-px bg-rule" />;
782814
}

0 commit comments

Comments
 (0)