@www.hyperlinks.space/program-kit 1.2.3 → 1.2.91881
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/app/_layout.tsx +5 -6
- package/app/index.tsx +1 -1
- package/assets/images/PreviewImage.png +0 -0
- package/database/start.ts +1 -3
- package/docs/build_and_install.md +6 -6
- package/docs/npm-release.md +46 -0
- package/docs/releases.md +5 -5
- package/docs/releases_github_actions.md +5 -5
- package/docs/security_plan_raw.md +4 -4
- package/docs/security_raw.md +2 -2
- package/docs/timing_raw.md +7 -7
- package/docs/update.md +11 -11
- package/docs/wallet_telegram_standalone_multichain_proposal.md +192 -0
- package/docs/wallets_hosting_architecture.md +1 -1
- package/fullREADME.md +101 -81
- package/npmReadMe.md +5 -5
- package/package.json +2 -2
- package/scripts/load-env.ts +1 -3
- package/scripts/test-api-base.ts +1 -1
- package/tsconfig.json +1 -1
- package/windows/build.cjs +1 -1
- package/windows/cleanup-legacy-appdata-installs.ps1 +1 -1
- package/windows/product-brand.cjs +1 -1
- package/app/components/GlobalBottomBar.tsx +0 -447
- package/app/components/GlobalBottomBarWeb.tsx +0 -362
- package/app/components/GlobalLogoBar.tsx +0 -108
- package/app/components/GlobalLogoBarFallback.tsx +0 -66
- package/app/components/GlobalLogoBarWithFallback.tsx +0 -24
- package/app/components/HyperlinksSpaceLogo.tsx +0 -29
- package/app/components/Telegram.tsx +0 -648
- package/app/components/telegramWebApp.ts +0 -359
- package/app/fonts.ts +0 -12
- package/app/theme.ts +0 -117
- package/docs/backlogs/medium_term_backlog.md +0 -26
- package/docs/backlogs/short_term_backlog.md +0 -39
- package/eslint.config.js +0 -10
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Web-only GlobalBottomBar implementation using a native <textarea>.
|
|
3
|
-
* Mirrors GlobalBottomBar behaviour (auto-resize 1–8 lines, custom scroll
|
|
4
|
-
* indicator, arrow icon) so we can compare web rendering more directly.
|
|
5
|
-
*/
|
|
6
|
-
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
7
|
-
import { View, Text, StyleSheet, Pressable } from "react-native";
|
|
8
|
-
import Svg, { Path } from "react-native-svg";
|
|
9
|
-
import { useTelegram } from "./Telegram";
|
|
10
|
-
import { getPrimaryTextColorFromLaunch } from "./telegramWebApp";
|
|
11
|
-
import { WEB_UI_SANS_STACK } from "../fonts";
|
|
12
|
-
import { layout, icons, useColors } from "../theme";
|
|
13
|
-
|
|
14
|
-
const { maxContentWidth } = layout;
|
|
15
|
-
const {
|
|
16
|
-
lineHeight: LINE_HEIGHT,
|
|
17
|
-
verticalPadding: VERTICAL_PADDING,
|
|
18
|
-
horizontalPadding: HORIZONTAL_PADDING,
|
|
19
|
-
applyIconBottom: APPLY_ICON_BOTTOM,
|
|
20
|
-
maxLinesBeforeScroll: MAX_LINES_BEFORE_SCROLL,
|
|
21
|
-
maxBarHeight: MAX_BAR_HEIGHT,
|
|
22
|
-
} = layout.bottomBar;
|
|
23
|
-
const INNER_PADDING = 20; // gap above first line and below last line
|
|
24
|
-
const AUTO_SCROLL_THRESHOLD = 30;
|
|
25
|
-
const MAX_INPUT_HEIGHT = (MAX_LINES_BEFORE_SCROLL + 1) * LINE_HEIGHT; // 8 lines = 160 (text-only height)
|
|
26
|
-
|
|
27
|
-
export function GlobalBottomBarWeb() {
|
|
28
|
-
const colors = useColors();
|
|
29
|
-
const { themeBgReady } = useTelegram();
|
|
30
|
-
const backgroundColor = themeBgReady ? colors.background : "transparent";
|
|
31
|
-
const [value, setValue] = useState("");
|
|
32
|
-
const [isFocused, setIsFocused] = useState(false);
|
|
33
|
-
const [scrollY, setScrollY] = useState(0);
|
|
34
|
-
const [domScrollRange, setDomScrollRange] = useState(0);
|
|
35
|
-
const [contentHeight, setContentHeight] = useState(LINE_HEIGHT);
|
|
36
|
-
// Height of a DOM-based mirror used to drive both growth and shrink on web.
|
|
37
|
-
const [domMirrorHeight, setDomMirrorHeight] = useState<number | null>(null);
|
|
38
|
-
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
39
|
-
const domMirrorRef = useRef<HTMLDivElement | null>(null);
|
|
40
|
-
const wasNearBottomBeforeInputRef = useRef(true);
|
|
41
|
-
|
|
42
|
-
// Until Telegram theme is fully applied in React, use launch hash colors (sync, no grey flash).
|
|
43
|
-
const launchPrimary =
|
|
44
|
-
typeof window !== "undefined" ? getPrimaryTextColorFromLaunch() : null;
|
|
45
|
-
const inputColor = themeBgReady ? colors.primary : (launchPrimary ?? colors.primary);
|
|
46
|
-
|
|
47
|
-
const measureAndResize = useCallback(() => {
|
|
48
|
-
const el = textareaRef.current;
|
|
49
|
-
if (!el) return;
|
|
50
|
-
// Let React control the height via dynamicHeight; we only measure the
|
|
51
|
-
// intrinsic content height (including inner padding).
|
|
52
|
-
el.style.height = "auto";
|
|
53
|
-
const fullScrollHeight = el.scrollHeight;
|
|
54
|
-
setContentHeight(fullScrollHeight);
|
|
55
|
-
}, []);
|
|
56
|
-
|
|
57
|
-
const handleInput = useCallback(
|
|
58
|
-
(e: React.FormEvent<HTMLTextAreaElement>) => {
|
|
59
|
-
const target = e.target as HTMLTextAreaElement;
|
|
60
|
-
const range = Math.max(0, target.scrollHeight - target.clientHeight);
|
|
61
|
-
const isNearBottom =
|
|
62
|
-
range <= 0 || target.scrollTop >= range - AUTO_SCROLL_THRESHOLD;
|
|
63
|
-
wasNearBottomBeforeInputRef.current = isNearBottom;
|
|
64
|
-
setValue(target.value);
|
|
65
|
-
requestAnimationFrame(measureAndResize);
|
|
66
|
-
},
|
|
67
|
-
[measureAndResize]
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
const el = textareaRef.current;
|
|
72
|
-
if (!el) return;
|
|
73
|
-
const onScroll = () => {
|
|
74
|
-
const scrollTop = el.scrollTop;
|
|
75
|
-
const contentH = el.scrollHeight;
|
|
76
|
-
const clientH = el.clientHeight;
|
|
77
|
-
const range = Math.max(0, contentH - clientH);
|
|
78
|
-
wasNearBottomBeforeInputRef.current =
|
|
79
|
-
range <= 0 || scrollTop >= range - AUTO_SCROLL_THRESHOLD;
|
|
80
|
-
setScrollY(scrollTop);
|
|
81
|
-
setDomScrollRange(range);
|
|
82
|
-
};
|
|
83
|
-
el.addEventListener("scroll", onScroll, { passive: true });
|
|
84
|
-
return () => el.removeEventListener("scroll", onScroll);
|
|
85
|
-
}, [value]);
|
|
86
|
-
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
const id = requestAnimationFrame(() => measureAndResize());
|
|
89
|
-
return () => cancelAnimationFrame(id);
|
|
90
|
-
}, [measureAndResize]);
|
|
91
|
-
|
|
92
|
-
// Web-only DOM mirror: measure real wrapped text height (including INNER_PADDING),
|
|
93
|
-
// using a pixel-identical clone of the textarea's computed styles.
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
if (typeof document === "undefined") return;
|
|
96
|
-
|
|
97
|
-
let el = domMirrorRef.current;
|
|
98
|
-
if (!el) {
|
|
99
|
-
el = document.createElement("div");
|
|
100
|
-
domMirrorRef.current = el;
|
|
101
|
-
el.style.position = "absolute";
|
|
102
|
-
el.style.visibility = "hidden";
|
|
103
|
-
el.style.pointerEvents = "none";
|
|
104
|
-
el.style.whiteSpace = "pre-wrap";
|
|
105
|
-
el.style.wordBreak = "break-word";
|
|
106
|
-
el.style.left = "-9999px";
|
|
107
|
-
el.style.top = "-9999px";
|
|
108
|
-
document.body.appendChild(el);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const host = textareaRef.current;
|
|
112
|
-
if (host) {
|
|
113
|
-
const rect = host.getBoundingClientRect();
|
|
114
|
-
const cs = window.getComputedStyle(host);
|
|
115
|
-
|
|
116
|
-
// Match width and all layout‑critical styles.
|
|
117
|
-
el.style.width = `${rect.width}px`;
|
|
118
|
-
el.style.boxSizing = cs.boxSizing;
|
|
119
|
-
el.style.paddingTop = cs.paddingTop;
|
|
120
|
-
el.style.paddingBottom = cs.paddingBottom;
|
|
121
|
-
el.style.paddingLeft = cs.paddingLeft;
|
|
122
|
-
el.style.paddingRight = cs.paddingRight;
|
|
123
|
-
el.style.border = cs.border;
|
|
124
|
-
el.style.outline = cs.outline;
|
|
125
|
-
el.style.fontFamily = cs.fontFamily;
|
|
126
|
-
el.style.fontSize = cs.fontSize;
|
|
127
|
-
el.style.fontWeight = cs.fontWeight as string;
|
|
128
|
-
el.style.lineHeight = cs.lineHeight;
|
|
129
|
-
el.style.letterSpacing = cs.letterSpacing;
|
|
130
|
-
el.style.textTransform = cs.textTransform;
|
|
131
|
-
el.style.direction = cs.direction;
|
|
132
|
-
el.style.textAlign = cs.textAlign;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
el.textContent = value || " ";
|
|
136
|
-
const h = el.getBoundingClientRect().height;
|
|
137
|
-
setDomMirrorHeight(Number.isFinite(h) && h > 0 ? h : null);
|
|
138
|
-
}, [value]);
|
|
139
|
-
|
|
140
|
-
// Intrinsic text height (without inner gaps). Prefer the DOM mirror
|
|
141
|
-
// measurement when available so shrink-on-erase works reliably.
|
|
142
|
-
const baseHeight =
|
|
143
|
-
domMirrorHeight != null ? domMirrorHeight : contentHeight;
|
|
144
|
-
const effectiveTextHeight = Math.max(0, baseHeight - INNER_PADDING * 2);
|
|
145
|
-
// Use a threshold so we only switch to the next line once most of the
|
|
146
|
-
// next 20px slot is actually used (avoids early jumps on a few pixels).
|
|
147
|
-
const rawLines = Math.max(
|
|
148
|
-
1,
|
|
149
|
-
Math.floor((effectiveTextHeight + LINE_HEIGHT * 0.2) / LINE_HEIGHT),
|
|
150
|
-
);
|
|
151
|
-
// Visually we allow up to 7 full lines; the 8th+ line uses scroll.
|
|
152
|
-
const visibleLines = Math.min(rawLines, MAX_LINES_BEFORE_SCROLL);
|
|
153
|
-
// Final height: 2 * INNER_PADDING for gaps + visibleLines * lineHeight,
|
|
154
|
-
// clamped between 60 and 180px.
|
|
155
|
-
const dynamicHeight = Math.max(
|
|
156
|
-
60,
|
|
157
|
-
Math.min(
|
|
158
|
-
MAX_BAR_HEIGHT,
|
|
159
|
-
INNER_PADDING * 2 + visibleLines * LINE_HEIGHT,
|
|
160
|
-
),
|
|
161
|
-
);
|
|
162
|
-
const rowHeight = dynamicHeight;
|
|
163
|
-
const viewportHeight = rowHeight;
|
|
164
|
-
// Use height that includes 20px top + bottom gaps for scroll math (same as dynamicHeight formula).
|
|
165
|
-
const contentHeightWithGaps = baseHeight;
|
|
166
|
-
const scrollRange = Math.max(contentHeightWithGaps - viewportHeight, 0);
|
|
167
|
-
// Enter scroll mode as soon as content is taller than the visible viewport (i.e. from the 8th line).
|
|
168
|
-
const isScrollMode = contentHeightWithGaps > viewportHeight && scrollRange > 0;
|
|
169
|
-
const showScrollbar = isScrollMode;
|
|
170
|
-
// The scrollbar track always matches the visible input height.
|
|
171
|
-
const barHeight = rowHeight;
|
|
172
|
-
|
|
173
|
-
// Debug: log bar and textarea heights whenever they change so we can
|
|
174
|
-
// verify that they stay perfectly in sync (e.g. 3 lines → 100px).
|
|
175
|
-
useEffect(() => {
|
|
176
|
-
const el = textareaRef.current;
|
|
177
|
-
const domClient = el?.clientHeight ?? null;
|
|
178
|
-
const domScroll = el?.scrollHeight ?? null;
|
|
179
|
-
// eslint-disable-next-line no-console
|
|
180
|
-
console.log("[GlobalBottomBarWeb] heights", {
|
|
181
|
-
lines: rawLines,
|
|
182
|
-
dynamicHeight,
|
|
183
|
-
barHeight,
|
|
184
|
-
viewportHeight,
|
|
185
|
-
contentHeight,
|
|
186
|
-
domMirrorHeight,
|
|
187
|
-
domClient,
|
|
188
|
-
domScroll,
|
|
189
|
-
});
|
|
190
|
-
}, [rawLines, dynamicHeight, barHeight, viewportHeight, contentHeight, domMirrorHeight]);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
// When the 7th line first appears (i.e. we reach the max bar height but do
|
|
194
|
-
// not yet need scroll), shift the text up by one inner padding so the last
|
|
195
|
-
// visible line sits perfectly against the arrow baseline.
|
|
196
|
-
useEffect(() => {
|
|
197
|
-
if (typeof document === "undefined") return;
|
|
198
|
-
const el = textareaRef.current;
|
|
199
|
-
if (!el) return;
|
|
200
|
-
const isAtMaxHeight = dynamicHeight >= MAX_BAR_HEIGHT;
|
|
201
|
-
if (
|
|
202
|
-
rawLines === 7 &&
|
|
203
|
-
isAtMaxHeight &&
|
|
204
|
-
el.scrollTop === 0 &&
|
|
205
|
-
wasNearBottomBeforeInputRef.current
|
|
206
|
-
) {
|
|
207
|
-
el.scrollTop = INNER_PADDING;
|
|
208
|
-
}
|
|
209
|
-
}, [rawLines, dynamicHeight]);
|
|
210
|
-
|
|
211
|
-
// In scroll mode, keep the textarea scrolled to the bottom so the last line
|
|
212
|
-
// and the 20px bottom gap are visible, and the scroll indicator stays at bottom.
|
|
213
|
-
useEffect(() => {
|
|
214
|
-
if (typeof document === "undefined") return;
|
|
215
|
-
const el = textareaRef.current;
|
|
216
|
-
if (!el || !isScrollMode) return;
|
|
217
|
-
if (!wasNearBottomBeforeInputRef.current) return;
|
|
218
|
-
const range = el.scrollHeight - el.clientHeight;
|
|
219
|
-
if (range <= 0) return;
|
|
220
|
-
const raf = requestAnimationFrame(() => {
|
|
221
|
-
el.scrollTop = range;
|
|
222
|
-
setScrollY(range);
|
|
223
|
-
setDomScrollRange(range);
|
|
224
|
-
});
|
|
225
|
-
return () => cancelAnimationFrame(raf);
|
|
226
|
-
}, [value, isScrollMode]);
|
|
227
|
-
|
|
228
|
-
let indicatorHeight = 0;
|
|
229
|
-
let topPosition = 0;
|
|
230
|
-
const effectiveScrollRange = domScrollRange > 0 ? domScrollRange : scrollRange;
|
|
231
|
-
if (showScrollbar && effectiveScrollRange > 0 && contentHeightWithGaps > 0 && barHeight != null) {
|
|
232
|
-
const indicatorHeightRatio = Math.min(1, Math.max(0, viewportHeight / contentHeightWithGaps));
|
|
233
|
-
indicatorHeight = Math.min(barHeight, Math.max(0, barHeight * indicatorHeightRatio));
|
|
234
|
-
const scrollPosition = Math.min(1, Math.max(0, scrollY / effectiveScrollRange));
|
|
235
|
-
const availableSpace = Math.min(barHeight, Math.max(0, barHeight - indicatorHeight));
|
|
236
|
-
topPosition = Math.min(barHeight, Math.max(0, scrollPosition * availableSpace));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const handleSend = useCallback(() => {
|
|
240
|
-
const text = value.trim();
|
|
241
|
-
if (!text) return;
|
|
242
|
-
setValue("");
|
|
243
|
-
// Optional: navigate to AI with prompt like GlobalBottomBar
|
|
244
|
-
}, [value]);
|
|
245
|
-
|
|
246
|
-
return (
|
|
247
|
-
<View style={[styles.wrapper, { backgroundColor }]}>
|
|
248
|
-
<View style={[styles.block, { backgroundColor }]}>
|
|
249
|
-
<View style={[styles.row, { height: rowHeight }]}>
|
|
250
|
-
<View style={styles.inputWrap}>
|
|
251
|
-
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
|
|
252
|
-
<textarea
|
|
253
|
-
ref={textareaRef}
|
|
254
|
-
data-global-bottom-bar-web
|
|
255
|
-
value={value}
|
|
256
|
-
onInput={handleInput}
|
|
257
|
-
onFocus={() => setIsFocused(true)}
|
|
258
|
-
onBlur={() => setIsFocused(false)}
|
|
259
|
-
rows={1}
|
|
260
|
-
style={{
|
|
261
|
-
width: "100%",
|
|
262
|
-
// Keep the DOM min/height/max in sync with our computed
|
|
263
|
-
// dynamicHeight so there is no intermediate smaller box
|
|
264
|
-
// (e.g. 60px when we expect 80px on 3 lines).
|
|
265
|
-
minHeight: dynamicHeight,
|
|
266
|
-
height: dynamicHeight,
|
|
267
|
-
maxHeight: dynamicHeight,
|
|
268
|
-
fontSize: 15,
|
|
269
|
-
lineHeight: `${LINE_HEIGHT}px`,
|
|
270
|
-
paddingTop: INNER_PADDING,
|
|
271
|
-
paddingBottom: INNER_PADDING,
|
|
272
|
-
paddingRight: 36,
|
|
273
|
-
boxSizing: "border-box",
|
|
274
|
-
resize: "none",
|
|
275
|
-
border: "none",
|
|
276
|
-
outline: "none",
|
|
277
|
-
color: inputColor,
|
|
278
|
-
backgroundColor: "transparent",
|
|
279
|
-
caretColor: inputColor,
|
|
280
|
-
["--ai-placeholder-color" as string]: inputColor,
|
|
281
|
-
fontFamily: WEB_UI_SANS_STACK,
|
|
282
|
-
// Allow scroll exactly when content exceeds the visible viewport height.
|
|
283
|
-
overflow: contentHeightWithGaps > viewportHeight ? "auto" : "hidden",
|
|
284
|
-
}}
|
|
285
|
-
placeholder={isFocused ? "" : "AI and search"}
|
|
286
|
-
/>
|
|
287
|
-
</View>
|
|
288
|
-
<Pressable style={styles.arrowWrap} onPress={handleSend} accessibilityRole="button" accessibilityLabel="Send">
|
|
289
|
-
<Svg width={icons.apply.width} height={icons.apply.height} viewBox="0 0 15 10">
|
|
290
|
-
<Path
|
|
291
|
-
d="M1 5H10M6 1L10 5L6 9"
|
|
292
|
-
stroke={inputColor}
|
|
293
|
-
strokeWidth={1.5}
|
|
294
|
-
strokeLinecap="round"
|
|
295
|
-
strokeLinejoin="round"
|
|
296
|
-
/>
|
|
297
|
-
</Svg>
|
|
298
|
-
</Pressable>
|
|
299
|
-
</View>
|
|
300
|
-
</View>
|
|
301
|
-
{showScrollbar && indicatorHeight > 0 && barHeight != null && (
|
|
302
|
-
<View style={[styles.scrollbarContainer, { height: barHeight }]}>
|
|
303
|
-
<View
|
|
304
|
-
style={[
|
|
305
|
-
styles.scrollbarIndicator,
|
|
306
|
-
{ height: indicatorHeight, marginTop: topPosition, backgroundColor: colors.secondary },
|
|
307
|
-
]}
|
|
308
|
-
/>
|
|
309
|
-
</View>
|
|
310
|
-
)}
|
|
311
|
-
</View>
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const SCROLLBAR_INSET = 5;
|
|
316
|
-
|
|
317
|
-
const styles = StyleSheet.create({
|
|
318
|
-
wrapper: {
|
|
319
|
-
width: "100%",
|
|
320
|
-
position: "relative",
|
|
321
|
-
},
|
|
322
|
-
block: {
|
|
323
|
-
width: "100%",
|
|
324
|
-
maxWidth: maxContentWidth,
|
|
325
|
-
alignSelf: "center",
|
|
326
|
-
paddingHorizontal: HORIZONTAL_PADDING,
|
|
327
|
-
// No vertical gap outside the input; the textarea occupies the full bar height.
|
|
328
|
-
paddingTop: 0,
|
|
329
|
-
paddingBottom: 0,
|
|
330
|
-
},
|
|
331
|
-
row: {
|
|
332
|
-
flexDirection: "row",
|
|
333
|
-
// Let children fill the full bar height; vertical positioning is handled
|
|
334
|
-
// inside each child (textarea via INNER_PADDING, arrow via paddingBottom).
|
|
335
|
-
alignItems: "stretch",
|
|
336
|
-
gap: 5,
|
|
337
|
-
position: "relative",
|
|
338
|
-
},
|
|
339
|
-
inputWrap: {
|
|
340
|
-
flex: 1,
|
|
341
|
-
position: "relative",
|
|
342
|
-
// Keep the textarea stuck to the top of the bar; vertical gaps are
|
|
343
|
-
// handled via INNER_PADDING inside the textarea itself.
|
|
344
|
-
justifyContent: "flex-start",
|
|
345
|
-
},
|
|
346
|
-
arrowWrap: {
|
|
347
|
-
// Stick the arrow icon to the bottom edge of the bar with 25px padding.
|
|
348
|
-
justifyContent: "flex-end",
|
|
349
|
-
alignItems: "center",
|
|
350
|
-
paddingBottom: 25,
|
|
351
|
-
},
|
|
352
|
-
scrollbarContainer: {
|
|
353
|
-
position: "absolute",
|
|
354
|
-
right: SCROLLBAR_INSET,
|
|
355
|
-
top: 0,
|
|
356
|
-
alignItems: "flex-start",
|
|
357
|
-
justifyContent: "flex-start",
|
|
358
|
-
},
|
|
359
|
-
scrollbarIndicator: {
|
|
360
|
-
width: 1,
|
|
361
|
-
},
|
|
362
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global logo bar: same layout and behaviour as Dart GlobalLogoBar.
|
|
3
|
-
* All Telegram data from useTelegram() (single source: Telegram.ts / Telegram.tsx).
|
|
4
|
-
*/
|
|
5
|
-
import React, { useMemo } from "react";
|
|
6
|
-
import { View, Pressable, StyleSheet } from "react-native";
|
|
7
|
-
import { useRouter } from "expo-router";
|
|
8
|
-
import { useTelegram } from "./Telegram";
|
|
9
|
-
import { HyperlinksSpaceLogo } from "./HyperlinksSpaceLogo";
|
|
10
|
-
import { useColors } from "../theme";
|
|
11
|
-
|
|
12
|
-
const LOGO_HEIGHT = 32;
|
|
13
|
-
const BOTTOM_PADDING = 10;
|
|
14
|
-
const HORIZONTAL_PADDING = 15;
|
|
15
|
-
const BROWSER_FALLBACK_TOP_PADDING = 30;
|
|
16
|
-
|
|
17
|
-
function useLogoTopPadding(
|
|
18
|
-
safeAreaInsetTop: number,
|
|
19
|
-
contentSafeAreaInsetTop: number
|
|
20
|
-
): number {
|
|
21
|
-
return useMemo(() => {
|
|
22
|
-
if (safeAreaInsetTop === 0 && contentSafeAreaInsetTop === 0) {
|
|
23
|
-
return BROWSER_FALLBACK_TOP_PADDING;
|
|
24
|
-
}
|
|
25
|
-
const value = safeAreaInsetTop + contentSafeAreaInsetTop / 2 - 16;
|
|
26
|
-
return Number.isFinite(value) ? value : BROWSER_FALLBACK_TOP_PADDING;
|
|
27
|
-
}, [safeAreaInsetTop, contentSafeAreaInsetTop]);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function GlobalLogoBar() {
|
|
31
|
-
const router = useRouter();
|
|
32
|
-
const colors = useColors();
|
|
33
|
-
const {
|
|
34
|
-
isInTelegram,
|
|
35
|
-
triggerHaptic,
|
|
36
|
-
safeAreaInsetTop,
|
|
37
|
-
contentSafeAreaInsetTop,
|
|
38
|
-
isFullscreen,
|
|
39
|
-
themeBgReady,
|
|
40
|
-
} = useTelegram();
|
|
41
|
-
|
|
42
|
-
const backgroundColor = themeBgReady ? colors.background : "transparent";
|
|
43
|
-
|
|
44
|
-
const topPadding = useLogoTopPadding(safeAreaInsetTop, contentSafeAreaInsetTop);
|
|
45
|
-
const blockHeight = topPadding + LOGO_HEIGHT + BOTTOM_PADDING;
|
|
46
|
-
|
|
47
|
-
const shouldShow = useMemo(() => {
|
|
48
|
-
if (!isInTelegram) return true;
|
|
49
|
-
return isFullscreen;
|
|
50
|
-
}, [isInTelegram, isFullscreen]);
|
|
51
|
-
|
|
52
|
-
const onPress = () => {
|
|
53
|
-
triggerHaptic("light");
|
|
54
|
-
router.replace("/");
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
if (!shouldShow) {
|
|
58
|
-
return <View style={[styles.container, { height: 0, backgroundColor }]} />;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<View style={[styles.container, { height: blockHeight, backgroundColor }]}>
|
|
63
|
-
<View
|
|
64
|
-
style={[
|
|
65
|
-
styles.inner,
|
|
66
|
-
{
|
|
67
|
-
paddingTop: topPadding,
|
|
68
|
-
paddingBottom: BOTTOM_PADDING,
|
|
69
|
-
paddingHorizontal: HORIZONTAL_PADDING,
|
|
70
|
-
},
|
|
71
|
-
]}
|
|
72
|
-
>
|
|
73
|
-
<Pressable
|
|
74
|
-
onPress={onPress}
|
|
75
|
-
style={styles.logoWrap}
|
|
76
|
-
accessibilityRole="button"
|
|
77
|
-
accessibilityLabel="Go to home"
|
|
78
|
-
>
|
|
79
|
-
<View style={styles.logoBox}>
|
|
80
|
-
<HyperlinksSpaceLogo width={LOGO_HEIGHT} height={LOGO_HEIGHT} />
|
|
81
|
-
</View>
|
|
82
|
-
</Pressable>
|
|
83
|
-
</View>
|
|
84
|
-
</View>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const styles = StyleSheet.create({
|
|
89
|
-
container: {
|
|
90
|
-
width: "100%",
|
|
91
|
-
backgroundColor: "transparent",
|
|
92
|
-
flexShrink: 0, /* keep header fixed height when keyboard opens (flex layout, no shift) */
|
|
93
|
-
},
|
|
94
|
-
inner: {
|
|
95
|
-
width: "100%",
|
|
96
|
-
alignItems: "center",
|
|
97
|
-
justifyContent: "center",
|
|
98
|
-
},
|
|
99
|
-
logoWrap: {
|
|
100
|
-
maxWidth: 600,
|
|
101
|
-
alignItems: "center",
|
|
102
|
-
justifyContent: "center",
|
|
103
|
-
},
|
|
104
|
-
logoBox: {
|
|
105
|
-
width: LOGO_HEIGHT,
|
|
106
|
-
height: LOGO_HEIGHT,
|
|
107
|
-
},
|
|
108
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fallback logo bar when TMA SDK is not available (e.g. browser).
|
|
3
|
-
* Same layout: 30px top padding, 32px logo, 10px bottom, 15px horizontal.
|
|
4
|
-
*/
|
|
5
|
-
import React from "react";
|
|
6
|
-
import { View, Pressable, StyleSheet } from "react-native";
|
|
7
|
-
import { useRouter } from "expo-router";
|
|
8
|
-
import { HyperlinksSpaceLogo } from "./HyperlinksSpaceLogo";
|
|
9
|
-
|
|
10
|
-
const LOGO_HEIGHT = 32;
|
|
11
|
-
const BOTTOM_PADDING = 10;
|
|
12
|
-
const HORIZONTAL_PADDING = 15;
|
|
13
|
-
const BROWSER_FALLBACK_TOP_PADDING = 30;
|
|
14
|
-
const BLOCK_HEIGHT =
|
|
15
|
-
BROWSER_FALLBACK_TOP_PADDING + LOGO_HEIGHT + BOTTOM_PADDING;
|
|
16
|
-
|
|
17
|
-
export function GlobalLogoBarFallback() {
|
|
18
|
-
const router = useRouter();
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<View style={[styles.container, { height: BLOCK_HEIGHT }]}>
|
|
22
|
-
<View
|
|
23
|
-
style={[
|
|
24
|
-
styles.inner,
|
|
25
|
-
{
|
|
26
|
-
paddingTop: BROWSER_FALLBACK_TOP_PADDING,
|
|
27
|
-
paddingBottom: BOTTOM_PADDING,
|
|
28
|
-
paddingHorizontal: HORIZONTAL_PADDING,
|
|
29
|
-
},
|
|
30
|
-
]}
|
|
31
|
-
>
|
|
32
|
-
<Pressable
|
|
33
|
-
onPress={() => router.replace("/")}
|
|
34
|
-
style={styles.logoWrap}
|
|
35
|
-
accessibilityRole="button"
|
|
36
|
-
accessibilityLabel="Go to home"
|
|
37
|
-
>
|
|
38
|
-
<View style={styles.logoBox}>
|
|
39
|
-
<HyperlinksSpaceLogo width={LOGO_HEIGHT} height={LOGO_HEIGHT} />
|
|
40
|
-
</View>
|
|
41
|
-
</Pressable>
|
|
42
|
-
</View>
|
|
43
|
-
</View>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const styles = StyleSheet.create({
|
|
48
|
-
container: {
|
|
49
|
-
width: "100%",
|
|
50
|
-
backgroundColor: "transparent",
|
|
51
|
-
},
|
|
52
|
-
inner: {
|
|
53
|
-
width: "100%",
|
|
54
|
-
alignItems: "center",
|
|
55
|
-
justifyContent: "center",
|
|
56
|
-
},
|
|
57
|
-
logoWrap: {
|
|
58
|
-
maxWidth: 600,
|
|
59
|
-
alignItems: "center",
|
|
60
|
-
justifyContent: "center",
|
|
61
|
-
},
|
|
62
|
-
logoBox: {
|
|
63
|
-
width: LOGO_HEIGHT,
|
|
64
|
-
height: LOGO_HEIGHT,
|
|
65
|
-
},
|
|
66
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Renders GlobalLogoBar; on SDK error (e.g. browser) renders GlobalLogoBarFallback.
|
|
3
|
-
*/
|
|
4
|
-
import React, { Component, type ReactNode } from "react";
|
|
5
|
-
import { GlobalLogoBar } from "./GlobalLogoBar";
|
|
6
|
-
import { GlobalLogoBarFallback } from "./GlobalLogoBarFallback";
|
|
7
|
-
|
|
8
|
-
type Props = Record<string, never>;
|
|
9
|
-
type State = { hasError: boolean };
|
|
10
|
-
|
|
11
|
-
export class GlobalLogoBarWithFallback extends Component<Props, State> {
|
|
12
|
-
state: State = { hasError: false };
|
|
13
|
-
|
|
14
|
-
static getDerivedStateFromError(): State {
|
|
15
|
-
return { hasError: true };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
render(): ReactNode {
|
|
19
|
-
if (this.state.hasError) {
|
|
20
|
-
return <GlobalLogoBarFallback />;
|
|
21
|
-
}
|
|
22
|
-
return <GlobalLogoBar />;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 32×32 logo matching Dart GlobalLogoBar asset (HyperlinksSpace.svg).
|
|
3
|
-
* Inline SVG paths to avoid asset transformer; fill #1AAA11.
|
|
4
|
-
*/
|
|
5
|
-
import React from "react";
|
|
6
|
-
import Svg, { Path } from "react-native-svg";
|
|
7
|
-
|
|
8
|
-
const LOGO_SIZE = 32;
|
|
9
|
-
|
|
10
|
-
export function HyperlinksSpaceLogo({
|
|
11
|
-
width = LOGO_SIZE,
|
|
12
|
-
height = LOGO_SIZE,
|
|
13
|
-
}: {
|
|
14
|
-
width?: number;
|
|
15
|
-
height?: number;
|
|
16
|
-
}) {
|
|
17
|
-
return (
|
|
18
|
-
<Svg width={width} height={height} viewBox="0 0 24 24" fill="none">
|
|
19
|
-
<Path
|
|
20
|
-
d="M6 24L13.2 19.2L17.28 24H24V0H22.8V22.8H18L6 7.2V24Z"
|
|
21
|
-
fill="#1AAA11"
|
|
22
|
-
/>
|
|
23
|
-
<Path
|
|
24
|
-
d="M18 0L10.8 4.8L6.72 0H0V24H1.2V1.2H6L18 16.8V0Z"
|
|
25
|
-
fill="#1AAA11"
|
|
26
|
-
/>
|
|
27
|
-
</Svg>
|
|
28
|
-
);
|
|
29
|
-
}
|