@www.hyperlinks.space/program-kit 18.18.18 → 123.123.123

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.
Files changed (52) hide show
  1. package/.eas/workflows/create-development-builds.yml +21 -0
  2. package/.eas/workflows/create-draft.yml +15 -0
  3. package/.eas/workflows/deploy-to-production.yml +68 -0
  4. package/.gitattributes +48 -0
  5. package/.gitignore +52 -0
  6. package/.nvmrc +1 -0
  7. package/.vercelignore +6 -0
  8. package/README.md +17 -2
  9. package/ai/openai.ts +202 -0
  10. package/ai/transmitter.ts +367 -0
  11. package/backlogs/medium_term_backlog.md +26 -0
  12. package/backlogs/short_term_backlog.md +42 -0
  13. package/eslint.config.cjs +10 -0
  14. package/npmReadMe.md +17 -2
  15. package/npmrc.example +1 -0
  16. package/package.json +3 -28
  17. package/polyfills/buffer.ts +9 -0
  18. package/research & docs/ai_and_search_bar_input.md +94 -0
  19. package/research & docs/ai_bot_messages.md +124 -0
  20. package/research & docs/auth-and-centralized-encrypted-keys-plan.md +440 -0
  21. package/research & docs/blue_bar_tackling.md +143 -0
  22. package/research & docs/bot_async_streaming.md +174 -0
  23. package/research & docs/build_and_install.md +129 -0
  24. package/research & docs/database_messages.md +34 -0
  25. package/research & docs/fonts.md +18 -0
  26. package/research & docs/github-gitlab-bidirectional-mirroring.md +154 -0
  27. package/research & docs/keys-retrieval-console-scripts.js +131 -0
  28. package/research & docs/npm-release.md +46 -0
  29. package/research & docs/releases.md +201 -0
  30. package/research & docs/releases_github_actions.md +188 -0
  31. package/research & docs/scalability.md +34 -0
  32. package/research & docs/security_plan_raw.md +244 -0
  33. package/research & docs/security_raw.md +354 -0
  34. package/research & docs/storage-availability-console-script.js +152 -0
  35. package/research & docs/storage-lifetime.md +33 -0
  36. package/research & docs/telegram-raw-keys-cloud-storage-risks.md +31 -0
  37. package/research & docs/timing_raw.md +63 -0
  38. package/research & docs/tma_logo_bar_jump_investigation.md +69 -0
  39. package/research & docs/update.md +205 -0
  40. package/research & docs/wallet_telegram_standalone_multichain_proposal.md +192 -0
  41. package/research & docs/wallets_hosting_architecture.md +403 -0
  42. package/services/wallet/tonWallet.ts +73 -0
  43. package/ui/components/GlobalBottomBar.tsx +447 -0
  44. package/ui/components/GlobalBottomBarWeb.tsx +362 -0
  45. package/ui/components/GlobalLogoBar.tsx +108 -0
  46. package/ui/components/GlobalLogoBarFallback.tsx +66 -0
  47. package/ui/components/GlobalLogoBarWithFallback.tsx +24 -0
  48. package/ui/components/HyperlinksSpaceLogo.tsx +29 -0
  49. package/ui/components/Telegram.tsx +677 -0
  50. package/ui/components/telegramWebApp.ts +359 -0
  51. package/ui/fonts.ts +12 -0
  52. package/ui/theme.ts +117 -0
@@ -0,0 +1,447 @@
1
+ /**
2
+ * Global AI & Search bar (bottom block).
3
+ *
4
+ * This mirrors the Flutter GlobalBottomBar behaviour:
5
+ * - 20px line height, 20px top/bottom padding
6
+ * - Bar grows from 1–7 lines, then caps at 180px and enables internal scroll
7
+ * - Last line stays pinned 20px from the bottom while typing
8
+ * - Apply icon is always 25px from the bottom
9
+ */
10
+ import React, { useCallback, useEffect, useRef, useState } from "react";
11
+ import {
12
+ View,
13
+ Text,
14
+ TextInput,
15
+ Pressable,
16
+ StyleSheet,
17
+ Keyboard,
18
+ ScrollView,
19
+ Platform,
20
+ type NativeSyntheticEvent,
21
+ type TextInputSubmitEditingEventData,
22
+ type TextInputContentSizeChangeEventData,
23
+ type NativeScrollEvent,
24
+ type NativeSyntheticEvent as RnNativeEvent,
25
+ } from "react-native";
26
+ import { useRouter } from "expo-router";
27
+ import { useTelegram } from "./Telegram";
28
+ import Svg, { Path } from "react-native-svg";
29
+ import { layout, icons, useColors } from "../theme";
30
+
31
+ const { maxContentWidth } = layout;
32
+ const {
33
+ barMinHeight: BAR_MIN_HEIGHT,
34
+ horizontalPadding: HORIZONTAL_PADDING,
35
+ verticalPadding: VERTICAL_PADDING,
36
+ applyIconBottom: APPLY_ICON_BOTTOM,
37
+ lineHeight: LINE_HEIGHT,
38
+ maxLinesBeforeScroll: MAX_LINES_BEFORE_SCROLL,
39
+ maxBarHeight: MAX_BAR_HEIGHT,
40
+ } = layout.bottomBar;
41
+ const FONT_SIZE = 15;
42
+ // Same as web: 20px gap above first line and below last line inside the input.
43
+ const INNER_PADDING = 20;
44
+ const AUTO_SCROLL_THRESHOLD = 30;
45
+ const PREMADE_PROMPTS = [
46
+ "What is the universe?",
47
+ "Tell me about dogs token",
48
+ ];
49
+
50
+ export function GlobalBottomBar() {
51
+ const router = useRouter();
52
+ const { triggerHaptic, themeBgReady } = useTelegram();
53
+ const colors = useColors();
54
+ const backgroundColor = themeBgReady ? colors.background : "transparent";
55
+ const [value, setValue] = useState("");
56
+ const [isFocused, setIsFocused] = useState(false);
57
+ const inputRef = useRef<TextInput>(null);
58
+ const scrollRef = useRef<ScrollView>(null);
59
+ const [contentHeight, setContentHeight] = useState<number>(LINE_HEIGHT);
60
+ // Height of a hidden mirrored Text used for shrink (web) and grow (native when contentSize is unreliable).
61
+ const [mirrorHeight, setMirrorHeight] = useState<number | null>(null);
62
+ // Width of the input area so the mirror Text can wrap correctly on native (iOS/Android).
63
+ const [inputAreaWidth, setInputAreaWidth] = useState<number | null>(null);
64
+ const [scrollY, setScrollY] = useState(0);
65
+ const scrollYRef = useRef(0);
66
+ const contentHeightWithGapsRef = useRef(LINE_HEIGHT + INNER_PADDING * 2);
67
+ const wasNearBottomBeforeResizeRef = useRef(true);
68
+
69
+ const isTelegramIOSWeb =
70
+ Platform.OS === "web" &&
71
+ typeof window !== "undefined" &&
72
+ !!(window as any).Telegram?.WebApp &&
73
+ (window as any).Telegram.WebApp.platform === "ios";
74
+
75
+ // Web-only: wire up a native scroll listener on the underlying textarea
76
+ // rendered by TextInput so we can track manual scroll that React Native Web
77
+ // may not surface via onScroll.
78
+ useEffect(() => {
79
+ if (Platform.OS !== "web") return;
80
+ if (typeof document === "undefined") return;
81
+
82
+ const el = document.querySelector(
83
+ '[data-ai-input="true"]',
84
+ ) as HTMLElement | null;
85
+ if (!el) return;
86
+
87
+ const handleScroll = () => {
88
+ const scrollTop = (el as HTMLTextAreaElement).scrollTop;
89
+ if (typeof scrollTop !== "number") return;
90
+ setScrollY(scrollTop);
91
+ };
92
+
93
+ el.addEventListener("scroll", handleScroll, { passive: true });
94
+ return () => {
95
+ el.removeEventListener("scroll", handleScroll);
96
+ };
97
+ }, []);
98
+
99
+ const submit = useCallback(() => {
100
+ triggerHaptic("heavy");
101
+ let text = value.trim();
102
+ if (!text && PREMADE_PROMPTS.length > 0) {
103
+ text =
104
+ PREMADE_PROMPTS[
105
+ Math.floor(Math.random() * PREMADE_PROMPTS.length)
106
+ ] ?? "";
107
+ setValue(text);
108
+ }
109
+ if (!text) return;
110
+ Keyboard.dismiss();
111
+ setValue("");
112
+ router.push({ pathname: "/ai" as any, params: { prompt: text } });
113
+ }, [value, router, triggerHaptic]);
114
+
115
+ const onSubmitEditing = useCallback(
116
+ (_e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
117
+ submit();
118
+ },
119
+ [submit]
120
+ );
121
+
122
+ const onContentSizeChange = useCallback(
123
+ (e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
124
+ const h = e.nativeEvent.contentSize.height;
125
+ if (!Number.isFinite(h)) return;
126
+ setContentHeight(h);
127
+ },
128
+ []
129
+ );
130
+
131
+ const onChangeText = useCallback((text: string) => {
132
+ setValue(text);
133
+ }, []);
134
+
135
+ const onScroll = useCallback(
136
+ (e: RnNativeEvent<NativeScrollEvent>) => {
137
+ const y = e.nativeEvent.contentOffset.y;
138
+ scrollYRef.current = y;
139
+ setScrollY(y);
140
+ },
141
+ [],
142
+ );
143
+
144
+ // Same formula as GlobalBottomBarWeb: base height includes 20px top + bottom gaps.
145
+ // Mirror is given paddingVertical so mirrorHeight = content with gaps; else contentHeight is text-only from onContentSizeChange.
146
+ const baseHeight =
147
+ mirrorHeight != null
148
+ ? mirrorHeight
149
+ : contentHeight + INNER_PADDING * 2;
150
+ const effectiveTextHeight = Math.max(0, baseHeight - INNER_PADDING * 2);
151
+ const rawLines = Math.max(
152
+ 1,
153
+ Math.floor(
154
+ (effectiveTextHeight + LINE_HEIGHT * 0.2) / LINE_HEIGHT,
155
+ ),
156
+ );
157
+ const visibleLines = Math.min(rawLines, MAX_LINES_BEFORE_SCROLL);
158
+ const dynamicHeight = Math.max(
159
+ 60,
160
+ Math.min(
161
+ MAX_BAR_HEIGHT,
162
+ INNER_PADDING * 2 + visibleLines * LINE_HEIGHT,
163
+ ),
164
+ );
165
+
166
+ const barHeight = dynamicHeight;
167
+ const viewportHeight = barHeight;
168
+ const contentHeightWithGaps = baseHeight;
169
+ const scrollRange = Math.max(contentHeightWithGaps - viewportHeight, 0);
170
+ const isScrollMode =
171
+ contentHeightWithGaps > viewportHeight && scrollRange > 0;
172
+ const showScrollbar = isScrollMode;
173
+
174
+ let indicatorHeight = 0;
175
+ let topPosition = 0;
176
+ if (
177
+ showScrollbar &&
178
+ scrollRange > 0 &&
179
+ contentHeightWithGaps > 0 &&
180
+ barHeight != null
181
+ ) {
182
+ const indicatorHeightRatio = Math.min(
183
+ 1,
184
+ Math.max(0, viewportHeight / contentHeightWithGaps),
185
+ );
186
+ indicatorHeight = Math.min(
187
+ barHeight,
188
+ Math.max(0, barHeight * indicatorHeightRatio),
189
+ );
190
+ const scrollPosition = Math.min(1, Math.max(0, scrollY / scrollRange));
191
+ const availableSpace = Math.min(
192
+ barHeight,
193
+ Math.max(0, barHeight - indicatorHeight),
194
+ );
195
+ topPosition = Math.min(
196
+ barHeight,
197
+ Math.max(0, scrollPosition * availableSpace),
198
+ );
199
+ }
200
+
201
+ // When the 7th line first appears (max bar height, no scroll yet), shift
202
+ // content up by one inner padding so the last visible line aligns with the arrow (same as web).
203
+ useEffect(() => {
204
+ if (
205
+ rawLines === 7 &&
206
+ dynamicHeight >= MAX_BAR_HEIGHT &&
207
+ scrollY === 0 &&
208
+ wasNearBottomBeforeResizeRef.current
209
+ ) {
210
+ scrollRef.current?.scrollTo({ y: INNER_PADDING, animated: false });
211
+ }
212
+ }, [rawLines, dynamicHeight, scrollY]);
213
+
214
+ // Snap the ScrollView to bottom whenever the content becomes taller than
215
+ // the visible viewport. Using onContentSizeChange ensures the scroll
216
+ // happens after iOS has laid out the content, so scrollToEnd is effective.
217
+ const onScrollViewContentSizeChange = useCallback(
218
+ (_w: number, h: number) => {
219
+ const previousScrollRange = Math.max(
220
+ contentHeightWithGapsRef.current - viewportHeight,
221
+ 0,
222
+ );
223
+ const isNearBottomBeforeResize =
224
+ previousScrollRange <= 0 ||
225
+ scrollYRef.current >= previousScrollRange - AUTO_SCROLL_THRESHOLD;
226
+ wasNearBottomBeforeResizeRef.current = isNearBottomBeforeResize;
227
+ contentHeightWithGapsRef.current = h;
228
+
229
+ if (h > viewportHeight && scrollRef.current && isNearBottomBeforeResize) {
230
+ scrollRef.current.scrollToEnd({ animated: false });
231
+ }
232
+ },
233
+ [viewportHeight],
234
+ );
235
+
236
+ return (
237
+ <View style={[styles.wrapper, { height: barHeight, backgroundColor }]}>
238
+ <View style={[styles.container, { height: barHeight, backgroundColor }]}>
239
+ <View style={styles.inner}>
240
+ <View style={styles.row}>
241
+ <View style={{ flex: 1 }}>
242
+ <View
243
+ style={{
244
+ height: viewportHeight,
245
+ justifyContent: "flex-start",
246
+ }}
247
+ >
248
+ <ScrollView
249
+ ref={scrollRef}
250
+ style={{ flex: 1 }}
251
+ contentContainerStyle={{
252
+ paddingRight: 6,
253
+ flexGrow: 1,
254
+ justifyContent: "flex-start",
255
+ }}
256
+ onScroll={onScroll}
257
+ onContentSizeChange={onScrollViewContentSizeChange}
258
+ scrollEventThrottle={16}
259
+ showsVerticalScrollIndicator={false}
260
+ >
261
+ <View
262
+ style={{
263
+ flexGrow: 1,
264
+ justifyContent: "flex-start",
265
+ position: "relative",
266
+ }}
267
+ onLayout={
268
+ Platform.OS !== "web"
269
+ ? (e) => {
270
+ const w = e.nativeEvent.layout.width;
271
+ if (Number.isFinite(w) && w > 0) setInputAreaWidth(w);
272
+ }
273
+ : undefined
274
+ }
275
+ >
276
+ <TextInput
277
+ ref={inputRef}
278
+ style={[styles.input, styles.inputWeb, { color: colors.primary }]}
279
+ placeholder={isFocused ? "" : "AI & Search"}
280
+ placeholderTextColor={colors.primary}
281
+ value={value}
282
+ onChangeText={onChangeText}
283
+ onSubmitEditing={onSubmitEditing}
284
+ returnKeyType="send"
285
+ blurOnSubmit={false}
286
+ multiline
287
+ maxLength={4096}
288
+ onContentSizeChange={onContentSizeChange}
289
+ scrollEnabled={false}
290
+ onFocus={() => setIsFocused(true)}
291
+ onBlur={() => setIsFocused(false)}
292
+ // @ts-expect-error dataSet is a valid prop on web (used for CSS targeting)
293
+ dataSet={{ "ai-input": "true" }}
294
+ />
295
+ {Platform.OS === "web" && (
296
+ <View
297
+ pointerEvents="none"
298
+ style={{
299
+ position: "absolute",
300
+ top: 0,
301
+ bottom: 0,
302
+ right: 0,
303
+ // Wider gutter on Telegram iOS webview so the
304
+ // native blue scroll thumb (if drawn) sits well
305
+ // away from the caret and last characters.
306
+ width: isTelegramIOSWeb ? 24 : 12,
307
+ backgroundColor,
308
+ }}
309
+ />
310
+ )}
311
+ <Text
312
+ style={[
313
+ styles.input,
314
+ styles.inputWeb,
315
+ {
316
+ position: "absolute",
317
+ opacity: 0,
318
+ pointerEvents: "none",
319
+ left: 0,
320
+ right: 0,
321
+ paddingVertical: INNER_PADDING,
322
+ // On native, give mirror explicit width so it wraps like the input and reports correct height.
323
+ ...(Platform.OS !== "web" &&
324
+ inputAreaWidth != null && { width: inputAreaWidth }),
325
+ },
326
+ ]}
327
+ numberOfLines={0}
328
+ onLayout={(e) => {
329
+ const h = e.nativeEvent.layout.height;
330
+ if (Number.isFinite(h) && h > 0) {
331
+ setMirrorHeight(h);
332
+ }
333
+ }}
334
+ >
335
+ {value || " "}
336
+ </Text>
337
+ </View>
338
+ </ScrollView>
339
+ </View>
340
+ </View>
341
+ <Pressable
342
+ style={styles.applyWrap}
343
+ onPress={submit}
344
+ accessibilityRole="button"
345
+ accessibilityLabel="Send"
346
+ >
347
+ <Svg
348
+ width={icons.apply.width}
349
+ height={icons.apply.height}
350
+ viewBox="0 0 15 10"
351
+ >
352
+ <Path
353
+ d="M1 5H10M6 1L10 5L6 9"
354
+ stroke={colors.primary}
355
+ strokeWidth={1.5}
356
+ strokeLinecap="round"
357
+ strokeLinejoin="round"
358
+ />
359
+ </Svg>
360
+ </Pressable>
361
+ </View>
362
+ </View>
363
+ </View>
364
+ {showScrollbar && indicatorHeight > 0 && (
365
+ <View style={[styles.scrollbarContainer, { height: barHeight }]}>
366
+ <View
367
+ style={[
368
+ styles.scrollbarIndicator,
369
+ {
370
+ height: indicatorHeight,
371
+ marginTop: topPosition,
372
+ backgroundColor: colors.secondary,
373
+ },
374
+ ]}
375
+ />
376
+ </View>
377
+ )}
378
+ </View>
379
+ );
380
+ }
381
+
382
+ const SCROLLBAR_INSET = 5;
383
+
384
+ const styles = StyleSheet.create({
385
+ wrapper: {
386
+ width: "100%",
387
+ position: "relative",
388
+ },
389
+ container: {
390
+ width: "100%",
391
+ maxWidth: maxContentWidth,
392
+ alignSelf: "center",
393
+ // backgroundColor is applied dynamically via useColors()
394
+ paddingVertical: 0,
395
+ paddingHorizontal: HORIZONTAL_PADDING,
396
+ },
397
+ inner: {
398
+ width: "100%",
399
+ },
400
+ row: {
401
+ flexDirection: "row",
402
+ alignItems: "flex-end",
403
+ gap: 5,
404
+ },
405
+ input: {
406
+ flex: 1,
407
+ fontSize: FONT_SIZE,
408
+ color: "#000000",
409
+ lineHeight: LINE_HEIGHT,
410
+ paddingVertical: INNER_PADDING,
411
+ paddingHorizontal: 0,
412
+ borderWidth: 0,
413
+ borderColor: "transparent",
414
+ backgroundColor: "transparent",
415
+ },
416
+ // Baseline overrides: relax RN Web default minHeight (40) and rely on our
417
+ // dynamic height logic (inputDynamicStyle) instead.
418
+ inputWeb: {
419
+ minHeight: 0,
420
+ // Base gutter so the caret and last characters never sit directly in the
421
+ // system scrollbar lane. On Telegram iOS we add extra right padding at
422
+ // runtime via the overlay width (see isTelegramIOSWeb logic).
423
+ paddingRight: 12,
424
+ },
425
+ applyWrap: {
426
+ // 25px padding from the bottom edge of the bar.
427
+ paddingBottom: 25,
428
+ justifyContent: "center",
429
+ alignItems: "center",
430
+ },
431
+ applyIcon: {
432
+ width: 15,
433
+ height: 10,
434
+ backgroundColor: "#1a1a1a",
435
+ borderRadius: 1,
436
+ },
437
+ scrollbarContainer: {
438
+ position: "absolute",
439
+ right: SCROLLBAR_INSET,
440
+ top: 0,
441
+ alignItems: "flex-start",
442
+ justifyContent: "flex-start",
443
+ },
444
+ scrollbarIndicator: {
445
+ width: 1,
446
+ },
447
+ });