tangled
alpha
login
or
join now
stream.place
/
streamplace
77
fork
atom
Live video on the AT Protocol
77
fork
atom
overview
issues
1
pulls
pipelines
Add build info and misc settings UI improvements
Natalie B.
4 months ago
392813a0
5415e457
+314
-197
17 changed files
expand all
collapse all
unified
split
.gitignore
js
app
components
settings
about-category-settings.tsx
advanced-category-settings.tsx
components
setting-toggle.tsx
settings-navigation-item.tsx
danmu-category-settings.tsx
privacy-category-settings.tsx
settings-item-link.tsx
settings.tsx
streaming-category-settings.tsx
updates.native.tsx
updates.tsx
webhook-manager.tsx
package.json
scripts
generate-build-info.js
src
router.tsx
components
src
components
ui
button.tsx
+1
.gitignore
···
23
target
24
my-release-key.keystore
25
test.xml
0
···
23
target
24
my-release-key.keystore
25
test.xml
26
+
js/app/src/build-info.json
+47
-5
js/app/components/settings/about-category-settings.tsx
···
1
import { Text, View, zero } from "@streamplace/components";
2
import { useTranslation } from "react-i18next";
3
import { ScrollView } from "react-native";
4
-
import pkg from "../../package.json";
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
5
6
export function AboutCategorySettings() {
7
const { t } = useTranslation("settings");
8
0
0
0
0
0
0
0
0
0
0
0
0
9
return (
10
<ScrollView>
11
<View style={[zero.layout.flex.align.center, zero.px[4], zero.py[4]]}>
···
16
]}
17
>
18
<View>
19
-
<Text size="xl">{t("app-version", { version: pkg.version })}</Text>
20
-
<Text size="lg" color="muted">
21
-
{t("app-version-description")}
22
-
</Text>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
23
</View>
24
</View>
25
</View>
···
1
import { Text, View, zero } from "@streamplace/components";
2
import { useTranslation } from "react-i18next";
3
import { ScrollView } from "react-native";
4
+
import { Updates } from "./updates";
5
+
6
+
let buildInfo: {
7
+
hash: string;
8
+
shortHash: string;
9
+
branch: string;
10
+
tag: string;
11
+
isDirty: boolean;
12
+
buildTime: string;
13
+
} | null = null;
14
+
15
+
try {
16
+
buildInfo = require("../../src/build-info.json");
17
+
} catch {
18
+
// build-info.json doesn't exist in dev mode
19
+
}
20
21
export function AboutCategorySettings() {
22
const { t } = useTranslation("settings");
23
24
+
const getBuildStatus = () => {
25
+
if (!buildInfo) {
26
+
return "dev";
27
+
}
28
+
return buildInfo.isDirty || process?.env.NODE_ENV === "development"
29
+
? "dev"
30
+
: "prod";
31
+
};
32
+
33
+
const buildLabel = buildInfo ? buildInfo.tag : "development";
34
+
const buildStatus = getBuildStatus();
35
+
36
return (
37
<ScrollView>
38
<View style={[zero.layout.flex.align.center, zero.px[4], zero.py[4]]}>
···
43
]}
44
>
45
<View>
46
+
<Text>This version is </Text>
47
+
<Updates />
48
+
</View>
49
+
50
+
<View
51
+
style={[
52
+
{ flexDirection: "row" },
53
+
{ alignItems: "flex-start" },
54
+
{ justifyContent: "flex-start" },
55
+
]}
56
+
>
57
+
<View style={[{ flex: 1 }, { paddingRight: 12 }]}>
58
+
<Text size="lg">Build</Text>
59
+
</View>
60
+
<View style={{ alignItems: "flex-end" }}>
61
+
<Text size="lg" color="muted">
62
+
{buildLabel} ({buildStatus})
63
+
</Text>
64
+
</View>
65
</View>
66
</View>
67
</View>
+8
-19
js/app/components/settings/advanced-category-settings.tsx
···
1
import { Button, Input, Text, View, zero } from "@streamplace/components";
2
import { useEffect, useState } from "react";
3
import { useTranslation } from "react-i18next";
4
-
import { ScrollView, Switch } from "react-native";
5
import { useStore } from "store";
6
import { DEFAULT_URL } from "store/slices/streamplaceSlice";
0
7
8
export function AdvancedCategorySettings() {
9
const url = useStore((state) => state.url);
···
56
zero.gap.all[4],
57
]}
58
>
59
-
<View
60
-
style={[
61
-
{ flexDirection: "row" },
62
-
{ alignItems: "flex-start" },
63
-
{ justifyContent: "flex-start" },
64
-
]}
65
-
>
66
-
<View style={[{ flex: 1 }, { paddingRight: 12 }]}>
67
-
<Text size="xl">{t("use-custom-node")}</Text>
68
-
<Text size="lg" color="muted">
69
-
{t("default-url", { url: defaultUrl })}
70
-
</Text>
71
-
</View>
72
-
<Switch
73
-
value={overrideEnabled}
74
-
onValueChange={handleToggleOverride}
75
-
/>
76
-
</View>
77
78
{overrideEnabled && (
79
<View
···
1
import { Button, Input, Text, View, zero } from "@streamplace/components";
2
import { useEffect, useState } from "react";
3
import { useTranslation } from "react-i18next";
4
+
import { ScrollView } from "react-native";
5
import { useStore } from "store";
6
import { DEFAULT_URL } from "store/slices/streamplaceSlice";
7
+
import { SettingToggle } from "./components/setting-toggle";
8
9
export function AdvancedCategorySettings() {
10
const url = useStore((state) => state.url);
···
57
zero.gap.all[4],
58
]}
59
>
60
+
<SettingToggle
61
+
title={t("use-custom-node")}
62
+
description={t("default-url", { url: defaultUrl })}
63
+
value={overrideEnabled}
64
+
onValueChange={handleToggleOverride}
65
+
/>
0
0
0
0
0
0
0
0
0
0
0
0
66
67
{overrideEnabled && (
68
<View
+40
js/app/components/settings/components/setting-toggle.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { Text, View } from "@streamplace/components";
2
+
import { mergeStyles } from "@streamplace/components/src/ui";
3
+
import { Switch, ViewStyle } from "react-native";
4
+
5
+
export interface SettingToggleProps {
6
+
title: string;
7
+
description?: string;
8
+
value: boolean;
9
+
onValueChange: (value: boolean) => void;
10
+
style?: ViewStyle;
11
+
}
12
+
13
+
export function SettingToggle({
14
+
title,
15
+
description,
16
+
value,
17
+
onValueChange,
18
+
style,
19
+
}: SettingToggleProps) {
20
+
return (
21
+
<View
22
+
style={mergeStyles(
23
+
{ flexDirection: "row" },
24
+
{ alignItems: "flex-start" },
25
+
{ justifyContent: "flex-start" },
26
+
style,
27
+
)}
28
+
>
29
+
<View style={[{ flex: 1 }, { paddingRight: 12 }]}>
30
+
<Text size="xl">{title}</Text>
31
+
{description && (
32
+
<Text size="lg" color="muted">
33
+
{description}
34
+
</Text>
35
+
)}
36
+
</View>
37
+
<Switch value={value} onValueChange={onValueChange} />
38
+
</View>
39
+
);
40
+
}
+8
-21
js/app/components/settings/danmu-category-settings.tsx
···
8
} from "@streamplace/components";
9
import { useDanmuSettings } from "@streamplace/components/src/streamplace-store";
10
import { useTranslation } from "react-i18next";
11
-
import {
12
-
Platform,
13
-
ScrollView,
14
-
Switch,
15
-
useWindowDimensions,
16
-
} from "react-native";
17
18
export function DanmuCategorySettings() {
19
const { t } = useTranslation("settings");
···
44
>
45
<View style={[{ alignItems: "stretch" }, zero.gap.all[4]]}>
46
{/* Enable/Disable Danmu */}
47
-
<View
48
-
style={[
49
-
{ flexDirection: "row" },
50
-
{ alignItems: "flex-start" },
51
-
{ justifyContent: "flex-start" },
52
-
]}
53
-
>
54
-
<View style={[{ flex: 1 }, { paddingRight: 12 }]}>
55
-
<Text size="xl">{t("danmu-enabled")}</Text>
56
-
<Text size="lg" color="muted">
57
-
{t("danmu-enabled-description")}
58
-
</Text>
59
-
</View>
60
-
<Switch value={danmuEnabled} onValueChange={setDanmuEnabled} />
61
-
</View>
62
63
{/* Opacity */}
64
<View style={[zero.gap.all[6]]}>
···
8
} from "@streamplace/components";
9
import { useDanmuSettings } from "@streamplace/components/src/streamplace-store";
10
import { useTranslation } from "react-i18next";
11
+
import { Platform, ScrollView, useWindowDimensions } from "react-native";
12
+
import { SettingToggle } from "./components/setting-toggle";
0
0
0
0
13
14
export function DanmuCategorySettings() {
15
const { t } = useTranslation("settings");
···
40
>
41
<View style={[{ alignItems: "stretch" }, zero.gap.all[4]]}>
42
{/* Enable/Disable Danmu */}
43
+
<SettingToggle
44
+
title={t("danmu-enabled")}
45
+
description={t("danmu-enabled-description")}
46
+
value={danmuEnabled}
47
+
onValueChange={setDanmuEnabled}
48
+
/>
0
0
0
0
0
0
0
0
0
49
50
{/* Opacity */}
51
<View style={[zero.gap.all[6]]}>
+15
-29
js/app/components/settings/privacy-category-settings.tsx
···
1
-
import { Text, View, zero } from "@streamplace/components";
2
import { useEffect } from "react";
3
import { useTranslation } from "react-i18next";
4
-
import { ScrollView, Switch } from "react-native";
5
import { useStore } from "store";
6
import { useIsReady, useServerSettings, useStreamplaceUrl } from "store/hooks";
0
7
8
export function PrivacyCategorySettings() {
9
const { t } = useTranslation("settings");
···
35
{ paddingVertical: 24, maxWidth: 500, width: "100%" },
36
]}
37
>
38
-
<View
39
-
style={[
40
-
zero.layout.flex.row,
41
-
zero.layout.flex.align.center,
42
-
zero.layout.flex.justify.between,
43
-
{ width: "100%" },
44
-
]}
45
-
>
46
-
<View style={[zero.flex.values[1], { paddingRight: 12 }]}>
47
-
<Text size="xl">
48
-
{t("debug-recording-title", { host: u.host })}
49
-
</Text>
50
-
<Text size="lg" color="muted">
51
-
{t("debug-recording-description")}
52
-
</Text>
53
-
</View>
54
-
<Switch
55
-
value={debugRecordingOn}
56
-
onValueChange={(value) => {
57
-
if (value === true) {
58
-
createServerSettingsRecord(true);
59
-
} else {
60
-
createServerSettingsRecord(false);
61
-
}
62
-
}}
63
-
/>
64
-
</View>
65
</View>
66
</View>
67
</ScrollView>
···
1
+
import { View, zero } from "@streamplace/components";
2
import { useEffect } from "react";
3
import { useTranslation } from "react-i18next";
4
+
import { ScrollView } from "react-native";
5
import { useStore } from "store";
6
import { useIsReady, useServerSettings, useStreamplaceUrl } from "store/hooks";
7
+
import { SettingToggle } from "./components/setting-toggle";
8
9
export function PrivacyCategorySettings() {
10
const { t } = useTranslation("settings");
···
36
{ paddingVertical: 24, maxWidth: 500, width: "100%" },
37
]}
38
>
39
+
<SettingToggle
40
+
title={t("debug-recording-title", { host: u.host })}
41
+
description={t("debug-recording-description")}
42
+
value={debugRecordingOn}
43
+
onValueChange={(value) => {
44
+
if (value === true) {
45
+
createServerSettingsRecord(true);
46
+
} else {
47
+
createServerSettingsRecord(false);
48
+
}
49
+
}}
50
+
/>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
51
</View>
52
</View>
53
</ScrollView>
-55
js/app/components/settings/settings-item-link.tsx
···
1
-
import { useNavigation } from "@react-navigation/native";
2
-
import { Text, View } from "@streamplace/components";
3
-
import { ChevronRight, LucideIcon } from "lucide-react-native";
4
-
import { Pressable } from "react-native";
5
-
6
-
interface SettingsItemLinkProps {
7
-
title: string;
8
-
screen: string;
9
-
icon: LucideIcon;
10
-
rootScreen?: boolean; // if true, navigates to root stack instead of Settings stack
11
-
}
12
-
13
-
export function SettingsItemLink({
14
-
title,
15
-
screen,
16
-
icon: Icon,
17
-
rootScreen = false,
18
-
}: SettingsItemLinkProps) {
19
-
const navigation = useNavigation();
20
-
21
-
const handlePress = () => {
22
-
if (rootScreen) {
23
-
// Navigate to root stack screen
24
-
navigation.navigate(screen as never);
25
-
} else {
26
-
// Navigate within Settings stack
27
-
navigation.navigate(screen as never);
28
-
}
29
-
};
30
-
31
-
return (
32
-
<Pressable onPress={handlePress}>
33
-
{({ pressed }) => (
34
-
<View
35
-
style={[
36
-
{
37
-
flexDirection: "row",
38
-
alignItems: "center",
39
-
justifyContent: "space-between",
40
-
paddingVertical: 12,
41
-
paddingHorizontal: 16,
42
-
backgroundColor: pressed ? "#ffffff08" : "transparent",
43
-
},
44
-
]}
45
-
>
46
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
47
-
<Icon size={20} color="#999" />
48
-
<Text size="lg">{title}</Text>
49
-
</View>
50
-
<ChevronRight size={20} color="#666" />
51
-
</View>
52
-
)}
53
-
</Pressable>
54
-
);
55
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
js/app/components/settings/settings-navigation-item.tsx
js/app/components/settings/components/settings-navigation-item.tsx
+1
-1
js/app/components/settings/settings.tsx
···
6
zero,
7
} from "@streamplace/components";
8
import AQLink from "components/aqlink";
9
-
import { SettingsNavigationItem } from "components/settings/settings-navigation-item";
10
import { Code, Info, Lock, LogIn, Shield, Video } from "lucide-react-native";
11
import { ImageBackground, Pressable, ScrollView } from "react-native";
12
···
6
zero,
7
} from "@streamplace/components";
8
import AQLink from "components/aqlink";
9
+
import { SettingsNavigationItem } from "components/settings/components/settings-navigation-item";
10
import { Code, Info, Lock, LogIn, Shield, Video } from "lucide-react-native";
11
import { ImageBackground, Pressable, ScrollView } from "react-native";
12
+3
-4
js/app/components/settings/streaming-category-settings.tsx
···
1
import { View, zero } from "@streamplace/components";
2
-
import { SettingsItemLink } from "components/settings/settings-item-link";
3
import { Key, Webhook } from "lucide-react-native";
4
import { useTranslation } from "react-i18next";
5
import { ScrollView } from "react-native";
0
6
import { HorizontalBar } from "./settings";
7
8
export function StreamingCategorySettings() {
···
11
<ScrollView>
12
<View style={[zero.layout.flex.align.center, zero.px[2], zero.py[2]]}>
13
<View style={[{ paddingVertical: 0, maxWidth: 500, width: "100%" }]}>
14
-
<SettingsItemLink
15
title={t("key-management")}
16
screen="KeyManagement"
17
icon={Key}
18
-
rootScreen
19
/>
20
<HorizontalBar />
21
-
<SettingsItemLink
22
title={t("webhooks")}
23
screen="WebhooksSettings"
24
icon={Webhook}
···
1
import { View, zero } from "@streamplace/components";
0
2
import { Key, Webhook } from "lucide-react-native";
3
import { useTranslation } from "react-i18next";
4
import { ScrollView } from "react-native";
5
+
import { SettingsNavigationItem } from "./components/settings-navigation-item";
6
import { HorizontalBar } from "./settings";
7
8
export function StreamingCategorySettings() {
···
11
<ScrollView>
12
<View style={[zero.layout.flex.align.center, zero.px[2], zero.py[2]]}>
13
<View style={[{ paddingVertical: 0, maxWidth: 500, width: "100%" }]}>
14
+
<SettingsNavigationItem
15
title={t("key-management")}
16
screen="KeyManagement"
17
icon={Key}
0
18
/>
19
<HorizontalBar />
20
+
<SettingsNavigationItem
21
title={t("webhooks")}
22
screen="WebhooksSettings"
23
icon={Webhook}
+54
-49
js/app/components/settings/updates.native.tsx
···
10
import * as ExpoUpdates from "expo-updates";
11
import { useEffect, useState } from "react";
12
import { useTranslation } from "react-i18next";
13
-
import { Platform, TouchableOpacity, View } from "react-native";
14
import pkg from "../../package.json";
15
16
const UNLOCK_TAP_COUNT = 5;
···
85
<Text size="2xl" style={[{ fontWeight: "bold", color: "#fff" }]}>
86
Streamplace v{version}
87
</Text>
88
-
<View
0
89
style={[
90
{ alignSelf: "flex-start" },
91
theme.zero.bg.muted,
···
97
<Text size="base" center>
98
{runTypeMessage}
99
</Text>
100
-
</View>
101
</View>
102
-
<Button
103
-
onPress={async () => {
104
-
try {
105
-
setChecked(true);
106
-
const res = await ExpoUpdates.checkForUpdateAsync();
107
-
if (!res.isAvailable) {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
108
toast.show(
109
-
t("modal-latest-version"),
110
-
t("modal-no-update-available"),
111
-
{ duration: 2000 },
112
-
);
113
-
} else {
114
-
toast.show(
115
-
t("modal-update-available-title"),
116
-
t("modal-update-available-description"),
117
-
{ duration: 2000 },
118
);
119
}
120
-
} catch (e) {
121
-
toast.show(
122
-
t("modal-update-failed-title"),
123
-
t("modal-update-failed-description", {
124
-
store: Platform.OS === "ios" ? "App Store" : "Play Store",
125
-
}),
126
-
);
127
-
}
128
-
}}
129
-
>
130
-
<Text style={[{ color: "#fff", fontWeight: "600" }]}>{buttonText}</Text>
131
-
</Button>
132
-
{isUpdatePending && (
133
-
<TouchableOpacity
134
-
style={[
135
-
{
136
-
marginTop: 8,
137
-
backgroundColor: "#007AFF",
138
-
borderRadius: 8,
139
-
paddingHorizontal: 16,
140
-
paddingVertical: 12,
141
-
alignItems: "center",
142
-
},
143
-
]}
144
-
onPress={() => {
145
-
ExpoUpdates.reloadAsync();
146
}}
147
>
148
-
<Text style={[{ color: "#fff", fontWeight: "600" }]}>
149
-
{t("button-reload-app-on-update")}
150
-
</Text>
151
-
</TouchableOpacity>
152
-
)}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
153
</View>
154
);
155
}
···
10
import * as ExpoUpdates from "expo-updates";
11
import { useEffect, useState } from "react";
12
import { useTranslation } from "react-i18next";
13
+
import { Platform, Pressable, TouchableOpacity, View } from "react-native";
14
import pkg from "../../package.json";
15
16
const UNLOCK_TAP_COUNT = 5;
···
85
<Text size="2xl" style={[{ fontWeight: "bold", color: "#fff" }]}>
86
Streamplace v{version}
87
</Text>
88
+
<Pressable
89
+
onPress={handleVersionPress}
90
style={[
91
{ alignSelf: "flex-start" },
92
theme.zero.bg.muted,
···
98
<Text size="base" center>
99
{runTypeMessage}
100
</Text>
101
+
</Pressable>
102
</View>
103
+
<View>
104
+
<Button
105
+
variant="secondary"
106
+
width="full"
107
+
onPress={async () => {
108
+
try {
109
+
setChecked(true);
110
+
const res = await ExpoUpdates.checkForUpdateAsync();
111
+
if (!res.isAvailable) {
112
+
toast.show(
113
+
t("modal-latest-version"),
114
+
t("modal-no-update-available"),
115
+
{ duration: 2000 },
116
+
);
117
+
} else {
118
+
toast.show(
119
+
t("modal-update-available-title"),
120
+
t("modal-update-available-description"),
121
+
{ duration: 2000 },
122
+
);
123
+
}
124
+
} catch (e) {
125
toast.show(
126
+
t("modal-update-failed-title"),
127
+
t("modal-update-failed-description", {
128
+
store: Platform.OS === "ios" ? "App Store" : "Play Store",
129
+
}),
0
0
0
0
0
130
);
131
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
132
}}
133
>
134
+
{buttonText}
135
+
</Button>
136
+
{isUpdatePending && (
137
+
<TouchableOpacity
138
+
style={[
139
+
{
140
+
marginTop: 8,
141
+
backgroundColor: "#007AFF",
142
+
borderRadius: 8,
143
+
paddingHorizontal: 16,
144
+
paddingVertical: 12,
145
+
alignItems: "center",
146
+
},
147
+
]}
148
+
onPress={() => {
149
+
ExpoUpdates.reloadAsync();
150
+
}}
151
+
>
152
+
<Text style={[{ color: "#fff", fontWeight: "600" }]}>
153
+
{t("button-reload-app-on-update")}
154
+
</Text>
155
+
</TouchableOpacity>
156
+
)}
157
+
</View>
158
</View>
159
);
160
}
+48
-4
js/app/components/settings/updates.tsx
···
1
-
import { Text } from "@streamplace/components";
0
0
0
0
0
0
2
import { useTranslation } from "react-i18next";
3
-
import { View } from "react-native";
4
import pkg from "../../package.json";
5
0
0
6
// maybe someday some PWA update stuff will live here
7
export function Updates() {
8
const { t } = useTranslation("settings");
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
9
return (
10
-
<View>
11
<Text size="xl">{t("app-version", { version: pkg.version })}</Text>
12
-
</View>
13
);
14
}
···
1
+
import {
2
+
Text,
3
+
useDanmuUnlocked,
4
+
useSetDanmuUnlocked,
5
+
useToast,
6
+
} from "@streamplace/components";
7
+
import { useState } from "react";
8
import { useTranslation } from "react-i18next";
9
+
import { Pressable } from "react-native-gesture-handler";
10
import pkg from "../../package.json";
11
12
+
const UNLOCK_TAP_COUNT = 5;
13
+
14
// maybe someday some PWA update stuff will live here
15
export function Updates() {
16
const { t } = useTranslation("settings");
17
+
const toast = useToast();
18
+
const [checked, setChecked] = useState(false);
19
+
const [tapCount, setTapCount] = useState(0);
20
+
const danmuUnlocked = useDanmuUnlocked();
21
+
const setDanmuUnlocked = useSetDanmuUnlocked();
22
+
23
+
const handleVersionPress = () => {
24
+
if (danmuUnlocked) {
25
+
toast.show("You are already a developer", undefined, {
26
+
duration: 2,
27
+
variant: "info",
28
+
actionLabel: "Stop being a developer",
29
+
onAction: () => {
30
+
setDanmuUnlocked(false);
31
+
toast.show("You are no longer a developer", undefined, {
32
+
duration: 2,
33
+
variant: "info",
34
+
});
35
+
},
36
+
});
37
+
return;
38
+
}
39
+
40
+
const newCount = tapCount + 1;
41
+
setTapCount(newCount);
42
+
43
+
if (newCount >= UNLOCK_TAP_COUNT) {
44
+
setDanmuUnlocked(true);
45
+
toast.show("You are now a developer", "have fun! lol", {
46
+
duration: 20,
47
+
variant: "success",
48
+
});
49
+
setTapCount(0);
50
+
}
51
+
};
52
+
53
return (
54
+
<Pressable onPress={handleVersionPress}>
55
<Text size="xl">{t("app-version", { version: pkg.version })}</Text>
56
+
</Pressable>
57
);
58
}
+11
-2
js/app/components/settings/webhook-manager.tsx
···
508
<Text style={[text.gray[300], { fontSize: 14, fontWeight: "500" }]}>
509
Text Replacements
510
</Text>
511
-
<Button size="pill" onPress={addReplacement}>
512
<Text style={[text.white, { fontSize: 12 }]}>+ Add</Text>
513
</Button>
514
</View>
···
830
</Text>
831
832
<View
833
-
style={[layout.flex.row, layout.flex.justify.between, gap.all[3]]}
0
0
0
0
0
834
>
835
<Button
836
onPress={handleCreate}
837
size="pill"
0
838
leftIcon={<Plus color={theme.colors.text} />}
839
>
840
<Text>{t("create-webhook")}</Text>
···
845
disabled={loading}
846
leftIcon={<RefreshCw color={theme.colors.text} />}
847
size="pill"
0
848
variant="secondary"
849
>
850
<Text>{t("refresh")}</Text>
···
938
<View style={[layout.flex.row, layout.flex.justify.end, gap.all[3]]}>
939
<Button
940
variant="secondary"
0
941
onPress={() => setDeleteDialog({ isVisible: false, webhook: null })}
942
disabled={
943
deleteDialog.webhook
···
949
</Button>
950
<Button
951
variant="destructive"
0
952
onPress={confirmDelete}
953
disabled={
954
deleteDialog.webhook
···
508
<Text style={[text.gray[300], { fontSize: 14, fontWeight: "500" }]}>
509
Text Replacements
510
</Text>
511
+
<Button width="min" size="pill" onPress={addReplacement}>
512
<Text style={[text.white, { fontSize: 12 }]}>+ Add</Text>
513
</Button>
514
</View>
···
830
</Text>
831
832
<View
833
+
style={[
834
+
layout.flex.row,
835
+
layout.flex.justify.between,
836
+
gap.all[3],
837
+
w.percent[100],
838
+
]}
839
>
840
<Button
841
onPress={handleCreate}
842
size="pill"
843
+
width="min"
844
leftIcon={<Plus color={theme.colors.text} />}
845
>
846
<Text>{t("create-webhook")}</Text>
···
851
disabled={loading}
852
leftIcon={<RefreshCw color={theme.colors.text} />}
853
size="pill"
854
+
width="min"
855
variant="secondary"
856
>
857
<Text>{t("refresh")}</Text>
···
945
<View style={[layout.flex.row, layout.flex.justify.end, gap.all[3]]}>
946
<Button
947
variant="secondary"
948
+
width="full"
949
onPress={() => setDeleteDialog({ isVisible: false, webhook: null })}
950
disabled={
951
deleteDialog.webhook
···
957
</Button>
958
<Button
959
variant="destructive"
960
+
width="full"
961
onPress={confirmDelete}
962
disabled={
963
deleteDialog.webhook
+1
-1
js/app/package.json
···
10
"web": "npx expo start --web --port 38081",
11
"test": "jest --watchAll",
12
"build": "pnpm run build:web && pnpm run prebuild",
13
-
"build:web": "pnpm run export && node exportClientExpoConfig.js > dist/expoConfig.json",
14
"export": "expo export --dump-sourcemap || expo export --dump-sourcemap",
15
"check": "bash -c 'set -euo pipefail;export OUT=$(mktemp -d); npx tsc -p . --outDir $OUT; rm -rf $OUT'",
16
"prebuild": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean && sed -i.bak 's/org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m/org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m/' android/gradle.properties && pnpm run find-node",
···
10
"web": "npx expo start --web --port 38081",
11
"test": "jest --watchAll",
12
"build": "pnpm run build:web && pnpm run prebuild",
13
+
"build:web": "node scripts/generate-build-info.js && pnpm run export && node exportClientExpoConfig.js > dist/expoConfig.json",
14
"export": "expo export --dump-sourcemap || expo export --dump-sourcemap",
15
"check": "bash -c 'set -euo pipefail;export OUT=$(mktemp -d); npx tsc -p . --outDir $OUT; rm -rf $OUT'",
16
"prebuild": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean && sed -i.bak 's/org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m/org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m/' android/gradle.properties && pnpm run find-node",
+48
js/app/scripts/generate-build-info.js
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
#!/usr/bin/env node
2
+
3
+
const { execSync } = require("child_process");
4
+
const fs = require("fs");
5
+
const path = require("path");
6
+
7
+
function getGitInfo() {
8
+
try {
9
+
const hash = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
10
+
const shortHash = execSync("git rev-parse --short HEAD", {
11
+
encoding: "utf-8",
12
+
}).trim();
13
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
14
+
encoding: "utf-8",
15
+
}).trim();
16
+
const tag = execSync("git describe --tags --always --dirty", {
17
+
encoding: "utf-8",
18
+
}).trim();
19
+
const isDirty = tag.endsWith("-dirty");
20
+
21
+
return {
22
+
hash,
23
+
shortHash,
24
+
branch,
25
+
tag,
26
+
isDirty,
27
+
};
28
+
} catch (error) {
29
+
console.warn("Could not get git info:", error.message);
30
+
return {
31
+
hash: "unknown",
32
+
shortHash: "unknown",
33
+
branch: "unknown",
34
+
tag: "unknown",
35
+
isDirty: false,
36
+
};
37
+
}
38
+
}
39
+
40
+
const buildInfo = {
41
+
...getGitInfo(),
42
+
buildTime: new Date().toISOString(),
43
+
};
44
+
45
+
const outputPath = path.join(__dirname, "..", "src", "build-info.json");
46
+
fs.writeFileSync(outputPath, JSON.stringify(buildInfo, null, 2));
47
+
48
+
console.log("Generated build-info.json:", buildInfo);
+16
-6
js/app/src/router.tsx
···
235
]}
236
>
237
{sidebar?.isActive ? (
238
-
<Pressable style={{ padding: 5 }} onPress={handlePress}>
239
-
{sidebar.isCollapsed ? (
240
-
<PanelLeftOpen size={24} color={theme.colors.accentForeground} />
241
-
) : (
242
-
<PanelLeftClose size={24} color={theme.colors.accentForeground} />
0
0
0
0
0
0
0
0
0
0
243
)}
244
-
</Pressable>
245
) : (
246
<Pressable style={{ padding: 5 }} onPress={handleGoBackPress}>
247
{canGoBack ? (
···
235
]}
236
>
237
{sidebar?.isActive ? (
238
+
<>
239
+
<Pressable style={{ padding: 5 }} onPress={handlePress}>
240
+
{sidebar.isCollapsed ? (
241
+
<PanelLeftOpen size={24} color={theme.colors.accentForeground} />
242
+
) : (
243
+
<PanelLeftClose size={24} color={theme.colors.accentForeground} />
244
+
)}
245
+
</Pressable>
246
+
{canGoBack && (
247
+
<Pressable
248
+
style={{ marginLeft: 10, paddingVertical: 5 }}
249
+
onPress={handleGoBackPress}
250
+
>
251
+
<ArrowLeft size={24} color={theme.colors.accentForeground} />
252
+
</Pressable>
253
)}
254
+
</>
255
) : (
256
<Pressable style={{ padding: 5 }} onPress={handleGoBackPress}>
257
{canGoBack ? (
+13
-1
js/components/src/components/ui/button.tsx
···
39
rightIcon?: React.ReactNode;
40
loading?: boolean;
41
loadingText?: string;
0
42
}
43
44
export const Button = forwardRef<any, ButtonProps>(
···
53
loadingText,
54
disabled,
55
style,
0
56
...props
57
},
58
ref,
···
198
}
199
}, [variant, icons]);
200
0
0
0
0
0
0
0
0
0
0
201
return (
202
<ButtonPrimitive.Root
203
ref={ref}
204
disabled={disabled || loading}
205
-
style={[buttonStyle, sizeStyles.button, style]}
206
{...props}
207
>
208
<ButtonPrimitive.Content style={sizeStyles.inner}>
···
39
rightIcon?: React.ReactNode;
40
loading?: boolean;
41
loadingText?: string;
42
+
width?: "full" | "min" | number;
43
}
44
45
export const Button = forwardRef<any, ButtonProps>(
···
54
loadingText,
55
disabled,
56
style,
57
+
width = "full",
58
...props
59
},
60
ref,
···
200
}
201
}, [variant, icons]);
202
203
+
const widthStyle = useMemo(() => {
204
+
if (width === "full") {
205
+
return { width: "100%" };
206
+
} else if (width === "min") {
207
+
return { alignSelf: "flex-start" as const };
208
+
} else {
209
+
return { width };
210
+
}
211
+
}, [width]);
212
+
213
return (
214
<ButtonPrimitive.Root
215
ref={ref}
216
disabled={disabled || loading}
217
+
style={[buttonStyle, sizeStyles.button, widthStyle, style]}
218
{...props}
219
>
220
<ButtonPrimitive.Content style={sizeStyles.inner}>