tangled
alpha
login
or
join now
stream.place
/
streamplace
76
fork
atom
Live video on the AT Protocol
76
fork
atom
overview
issues
1
pulls
pipelines
lil styling and refactor
Natalie B.
3 months ago
bb55919c
2002d821
+321
-390
4 changed files
expand all
collapse all
unified
split
js
app
components
login
login-form.tsx
login-modal.tsx
login.tsx
hooks
useActorTypeahead.tsx
+292
js/app/components/login/login-form.tsx
···
1
1
+
import {
2
2
+
Button,
3
3
+
Input,
4
4
+
Loader,
5
5
+
Text,
6
6
+
useTheme,
7
7
+
zero,
8
8
+
} from "@streamplace/components";
9
9
+
import useActorTypeahead from "hooks/useActorTypeahead";
10
10
+
import {
11
11
+
ArrowRightToLine,
12
12
+
AtSign,
13
13
+
CornerDownRight,
14
14
+
Info,
15
15
+
} from "lucide-react-native";
16
16
+
import { useEffect, useMemo, useState } from "react";
17
17
+
import { Alert, Image, Linking, Platform, Pressable, View } from "react-native";
18
18
+
import { useStore } from "store";
19
19
+
import { useLogin } from "store/hooks";
20
20
+
21
21
+
interface LoginFormProps {
22
22
+
onSuccess?: () => void;
23
23
+
}
24
24
+
25
25
+
export default function LoginForm({ onSuccess }: LoginFormProps) {
26
26
+
const { theme } = useTheme();
27
27
+
const loginAction = useStore((state) => state.login);
28
28
+
const openLoginLink = useStore((state) => state.openLoginLink);
29
29
+
const loginState = useLogin();
30
30
+
const [handle, setHandle] = useState("");
31
31
+
const [imageLoading, setImageLoading] = useState(false);
32
32
+
const { actors } = useActorTypeahead(handle);
33
33
+
34
34
+
const filteredActors = useMemo(
35
35
+
() => actors.filter((actor) => actor.handle.startsWith(handle)),
36
36
+
[actors, handle],
37
37
+
);
38
38
+
39
39
+
const suggestion = useMemo(
40
40
+
() =>
41
41
+
filteredActors.length > 0 &&
42
42
+
handle.length >= 3 &&
43
43
+
filteredActors[0].handle.startsWith(handle)
44
44
+
? filteredActors[0]
45
45
+
: null,
46
46
+
[filteredActors],
47
47
+
);
48
48
+
49
49
+
const completionText = useMemo(
50
50
+
() =>
51
51
+
suggestion && suggestion.handle
52
52
+
? suggestion.handle.slice(handle.length)
53
53
+
: null,
54
54
+
[suggestion, handle],
55
55
+
);
56
56
+
57
57
+
const avatarUri = useMemo(() => suggestion?.avatar, [suggestion?.avatar]);
58
58
+
59
59
+
const submit = () => {
60
60
+
let clean = handle;
61
61
+
if (handle.startsWith("@")) clean = handle.slice(1);
62
62
+
loginAction(clean, openLoginLink);
63
63
+
};
64
64
+
65
65
+
const acceptSuggestion = () => {
66
66
+
if (suggestion) {
67
67
+
setHandle(suggestion.handle);
68
68
+
}
69
69
+
};
70
70
+
71
71
+
const onSignup = () => {
72
72
+
loginAction("https://bsky.social", openLoginLink);
73
73
+
};
74
74
+
75
75
+
const isMobile = Platform.OS === "ios" || Platform.OS === "android";
76
76
+
77
77
+
const onKeyPress = (e: any) => {
78
78
+
if (e.nativeEvent.key === "Enter") {
79
79
+
if (completionText && isMobile) {
80
80
+
e.preventDefault();
81
81
+
acceptSuggestion();
82
82
+
} else if (!completionText) {
83
83
+
submit();
84
84
+
}
85
85
+
} else if (e.nativeEvent.key === "Tab" && completionText) {
86
86
+
e.preventDefault();
87
87
+
acceptSuggestion();
88
88
+
} else if (e.nativeEvent.key === "ArrowRight" && completionText) {
89
89
+
const input = e.target;
90
90
+
if (input.selectionStart === handle.length) {
91
91
+
e.preventDefault();
92
92
+
acceptSuggestion();
93
93
+
}
94
94
+
} else if (e.nativeEvent.key === " " && completionText) {
95
95
+
e.preventDefault();
96
96
+
acceptSuggestion();
97
97
+
}
98
98
+
};
99
99
+
100
100
+
useEffect(() => {
101
101
+
if (loginState?.error) {
102
102
+
Alert.alert("Login error", loginState.error);
103
103
+
}
104
104
+
}, [loginState?.error]);
105
105
+
106
106
+
return (
107
107
+
<>
108
108
+
<View
109
109
+
style={[
110
110
+
zero.layout.flex.row,
111
111
+
{ flexWrap: "wrap" },
112
112
+
zero.gap.all[1],
113
113
+
zero.mb[4],
114
114
+
]}
115
115
+
>
116
116
+
<Text style={[{ color: theme.colors.textMuted }]}>
117
117
+
Sign in using your handle on the AT Protocol
118
118
+
</Text>
119
119
+
<Pressable
120
120
+
onPress={() => {
121
121
+
const u = new URL(
122
122
+
"https://atproto.academy/docs/Authentication/why",
123
123
+
);
124
124
+
Linking.openURL(u.toString());
125
125
+
}}
126
126
+
>
127
127
+
<Info size={16} style={{ paddingTop: 4 }} color={theme.colors.ring} />
128
128
+
</Pressable>
129
129
+
<Text style={[{ color: theme.colors.textMuted }]}>
130
130
+
(e.g. your Bluesky handle)
131
131
+
</Text>
132
132
+
</View>
133
133
+
134
134
+
<View style={[zero.mb[4], { position: "relative" }]}>
135
135
+
<Text style={[{ color: "#aaa", marginBottom: 8 }]}>Handle</Text>
136
136
+
<View style={{ position: "relative" }}>
137
137
+
{completionText && suggestion?.handle !== handle ? (
138
138
+
<View
139
139
+
style={[
140
140
+
{
141
141
+
position: "absolute",
142
142
+
left: 13 + 28 + 8,
143
143
+
top: 12,
144
144
+
zIndex: 1000000,
145
145
+
pointerEvents: "none",
146
146
+
},
147
147
+
zero.layout.flex.row,
148
148
+
zero.layout.flex.alignCenter,
149
149
+
zero.gap.all[1],
150
150
+
]}
151
151
+
>
152
152
+
<Text
153
153
+
style={[
154
154
+
{
155
155
+
color: "#555",
156
156
+
pointerEvents: "none",
157
157
+
zIndex: 1000000,
158
158
+
fontSize: 16,
159
159
+
},
160
160
+
]}
161
161
+
>
162
162
+
<Text
163
163
+
style={{
164
164
+
opacity: 0.2,
165
165
+
fontSize: 16,
166
166
+
}}
167
167
+
>
168
168
+
{handle}
169
169
+
</Text>
170
170
+
{completionText}
171
171
+
</Text>
172
172
+
{isMobile ? (
173
173
+
<CornerDownRight
174
174
+
height={18}
175
175
+
color="#555"
176
176
+
style={{
177
177
+
paddingBottom: 1,
178
178
+
}}
179
179
+
/>
180
180
+
) : (
181
181
+
<ArrowRightToLine
182
182
+
height={18}
183
183
+
color="#555"
184
184
+
style={{
185
185
+
paddingBottom: 1,
186
186
+
}}
187
187
+
/>
188
188
+
)}
189
189
+
</View>
190
190
+
) : (
191
191
+
<></>
192
192
+
)}
193
193
+
<View
194
194
+
style={[
195
195
+
zero.layout.position.absolute,
196
196
+
zero.layout.flex.row,
197
197
+
{ zIndex: 32, top: 8 },
198
198
+
]}
199
199
+
>
200
200
+
{avatarUri ? (
201
201
+
<View
202
202
+
style={{
203
203
+
width: 28,
204
204
+
height: 28,
205
205
+
borderRadius: 900,
206
206
+
justifyContent: "center",
207
207
+
alignItems: "center",
208
208
+
}}
209
209
+
>
210
210
+
{imageLoading && (
211
211
+
<View
212
212
+
style={{
213
213
+
position: "absolute",
214
214
+
zIndex: 1,
215
215
+
}}
216
216
+
>
217
217
+
<Loader />
218
218
+
</View>
219
219
+
)}
220
220
+
<Image
221
221
+
key={avatarUri}
222
222
+
source={{ uri: avatarUri }}
223
223
+
style={{
224
224
+
width: 32,
225
225
+
height: 32,
226
226
+
borderRadius: 900,
227
227
+
opacity: suggestion?.handle === handle ? 1 : 0.5,
228
228
+
}}
229
229
+
onLayout={() => setImageLoading(true)}
230
230
+
onLoad={() => setImageLoading(false)}
231
231
+
onError={() => setImageLoading(false)}
232
232
+
/>
233
233
+
</View>
234
234
+
) : (
235
235
+
<View
236
236
+
style={{
237
237
+
width: 28,
238
238
+
height: 28,
239
239
+
borderRadius: 900,
240
240
+
justifyContent: "center",
241
241
+
alignItems: "center",
242
242
+
}}
243
243
+
>
244
244
+
<AtSign size={20} color="#eee" />
245
245
+
</View>
246
246
+
)}
247
247
+
</View>
248
248
+
<Input
249
249
+
value={handle}
250
250
+
onChangeText={(text) =>
251
251
+
setHandle(
252
252
+
text
253
253
+
.toLowerCase()
254
254
+
.replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, "")
255
255
+
.trim(),
256
256
+
)
257
257
+
}
258
258
+
onKeyPress={onKeyPress}
259
259
+
autoCapitalize="none"
260
260
+
autoCorrect={false}
261
261
+
keyboardType="url"
262
262
+
placeholderTextColor="#666"
263
263
+
containerStyle={{
264
264
+
marginLeft: 28 + 8,
265
265
+
}}
266
266
+
/>
267
267
+
</View>
268
268
+
</View>
269
269
+
270
270
+
<View
271
271
+
style={[
272
272
+
zero.layout.flex.row,
273
273
+
{ justifyContent: "flex-end", zIndex: -32 },
274
274
+
zero.gap.all[3],
275
275
+
]}
276
276
+
>
277
277
+
<Button width="min" onPress={() => onSignup()} variant="ghost">
278
278
+
<Text style={[{ color: "white" }]}>Sign Up on Bluesky</Text>
279
279
+
</Button>
280
280
+
<Button
281
281
+
onPress={submit}
282
282
+
disabled={loginState.loading}
283
283
+
style={[zero.px[6]]}
284
284
+
width="min"
285
285
+
loading={loginState.loading}
286
286
+
>
287
287
+
<Text style={[{ color: "white" }]}>Log in</Text>
288
288
+
</Button>
289
289
+
</View>
290
290
+
</>
291
291
+
);
292
292
+
}
+5
-221
js/app/components/login/login-modal.tsx
···
1
1
-
import { Button, Input, Text, useTheme, zero } from "@streamplace/components";
2
2
-
import useActorTypeahead from "hooks/useActorTypeahead";
3
3
-
import { ArrowRightToLine, AtSign, Info, X } from "lucide-react-native";
4
4
-
import { useEffect, useState } from "react";
5
5
-
import {
6
6
-
Alert,
7
7
-
Image,
8
8
-
Linking,
9
9
-
Modal,
10
10
-
Pressable,
11
11
-
TouchableOpacity,
12
12
-
View,
13
13
-
} from "react-native";
14
14
-
import { useStore } from "store";
15
15
-
import { useLogin } from "store/hooks";
1
1
+
import { Text, useTheme, zero } from "@streamplace/components";
2
2
+
import { X } from "lucide-react-native";
3
3
+
import { Modal, Pressable, TouchableOpacity, View } from "react-native";
4
4
+
import LoginForm from "./login-form";
16
5
17
6
interface LoginModalProps {
18
7
visible: boolean;
···
21
10
22
11
export default function LoginModal({ visible, onClose }: LoginModalProps) {
23
12
const { theme } = useTheme();
24
24
-
const loginAction = useStore((state) => state.login);
25
25
-
const openLoginLink = useStore((state) => state.openLoginLink);
26
26
-
const loginState = useLogin();
27
27
-
const [handle, setHandle] = useState("");
28
28
-
const { actors } = useActorTypeahead(handle);
29
29
-
30
30
-
const filteredActors = actors.filter((actor) =>
31
31
-
actor.handle.startsWith(handle),
32
32
-
);
33
33
-
34
34
-
const suggestion =
35
35
-
filteredActors.length > 0 &&
36
36
-
handle.length >= 3 &&
37
37
-
filteredActors[0].handle.startsWith(handle)
38
38
-
? filteredActors[0]
39
39
-
: null;
40
40
-
41
41
-
const completionText =
42
42
-
suggestion && suggestion.handle
43
43
-
? suggestion.handle.slice(handle.length)
44
44
-
: null;
45
45
-
46
46
-
const submit = () => {
47
47
-
let clean = handle;
48
48
-
if (handle.startsWith("@")) clean = handle.slice(1);
49
49
-
loginAction(clean, openLoginLink);
50
50
-
};
51
51
-
52
52
-
const acceptSuggestion = () => {
53
53
-
if (suggestion) {
54
54
-
setHandle(suggestion.handle);
55
55
-
}
56
56
-
};
57
57
-
58
58
-
const onSignup = () => {
59
59
-
loginAction("https://bsky.social", openLoginLink);
60
60
-
};
61
61
-
62
62
-
const onKeyPress = (e: any) => {
63
63
-
if (e.nativeEvent.key === "Enter") {
64
64
-
submit();
65
65
-
} else if (e.nativeEvent.key === "Tab" && completionText) {
66
66
-
e.preventDefault();
67
67
-
acceptSuggestion();
68
68
-
} else if (e.nativeEvent.key === "ArrowRight" && completionText) {
69
69
-
const input = e.target;
70
70
-
if (input.selectionStart === handle.length) {
71
71
-
e.preventDefault();
72
72
-
acceptSuggestion();
73
73
-
}
74
74
-
}
75
75
-
};
76
76
-
77
77
-
useEffect(() => {
78
78
-
if (loginState?.error) {
79
79
-
Alert.alert("Login error", loginState.error);
80
80
-
}
81
81
-
}, [loginState?.error]);
82
13
83
14
return (
84
15
<Modal
···
134
65
</TouchableOpacity>
135
66
</View>
136
67
137
137
-
<View
138
138
-
style={[
139
139
-
{ flexWrap: "wrap", flexDirection: "row" },
140
140
-
zero.gap.all[1],
141
141
-
zero.mb[4],
142
142
-
]}
143
143
-
>
144
144
-
<Text style={[{ color: theme.colors.textMuted }]}>
145
145
-
Sign in using your handle on the AT Protocol
146
146
-
</Text>
147
147
-
<Pressable
148
148
-
onPress={() => {
149
149
-
const u = new URL(
150
150
-
"https://atproto.academy/docs/Authentication/why",
151
151
-
);
152
152
-
Linking.openURL(u.toString());
153
153
-
}}
154
154
-
>
155
155
-
<Info
156
156
-
size={16}
157
157
-
style={{ paddingTop: 4 }}
158
158
-
color={theme.colors.ring}
159
159
-
/>
160
160
-
</Pressable>
161
161
-
<Text style={[{ color: theme.colors.textMuted }]}>
162
162
-
(e.g. your Bluesky handle)
163
163
-
</Text>
164
164
-
</View>
165
165
-
166
166
-
<View style={[zero.mb[4], { position: "relative" }]}>
167
167
-
<Text style={[{ color: "#aaa", marginBottom: 8 }]}>Handle</Text>
168
168
-
<View style={{ position: "relative" }}>
169
169
-
{completionText && (
170
170
-
<View
171
171
-
style={[
172
172
-
{
173
173
-
position: "absolute",
174
174
-
left: 13 + 28 + 8,
175
175
-
top: 12,
176
176
-
zIndex: 1000000,
177
177
-
// clickthroughable
178
178
-
pointerEvents: "none",
179
179
-
},
180
180
-
zero.layout.flex.row,
181
181
-
zero.layout.flex.alignCenter,
182
182
-
zero.gap.all[1],
183
183
-
]}
184
184
-
>
185
185
-
<Text
186
186
-
style={[
187
187
-
{
188
188
-
color: "#555",
189
189
-
pointerEvents: "none",
190
190
-
zIndex: 1000000,
191
191
-
fontSize: 16,
192
192
-
},
193
193
-
]}
194
194
-
>
195
195
-
<Text
196
196
-
style={{
197
197
-
opacity: 0.2,
198
198
-
fontSize: 16,
199
199
-
}}
200
200
-
>
201
201
-
{handle}
202
202
-
</Text>
203
203
-
{completionText}
204
204
-
</Text>
205
205
-
<ArrowRightToLine
206
206
-
height={18}
207
207
-
color="#555"
208
208
-
style={{
209
209
-
paddingBottom: 1,
210
210
-
}}
211
211
-
/>
212
212
-
</View>
213
213
-
)}
214
214
-
<View
215
215
-
style={{
216
216
-
position: "absolute",
217
217
-
flexDirection: "row",
218
218
-
zIndex: 32,
219
219
-
top: 8,
220
220
-
}}
221
221
-
>
222
222
-
{suggestion?.avatar ? (
223
223
-
<Image
224
224
-
source={{ uri: suggestion.avatar }}
225
225
-
style={{
226
226
-
width: 28,
227
227
-
height: 28,
228
228
-
borderRadius: 900,
229
229
-
opacity: suggestion.handle === handle ? 1 : 0.7,
230
230
-
}}
231
231
-
/>
232
232
-
) : (
233
233
-
<View
234
234
-
style={{
235
235
-
width: 28,
236
236
-
height: 28,
237
237
-
borderRadius: 900,
238
238
-
}}
239
239
-
>
240
240
-
<AtSign size={28} color="#eee" />
241
241
-
</View>
242
242
-
)}
243
243
-
</View>
244
244
-
<Input
245
245
-
value={handle}
246
246
-
onChangeText={(text) =>
247
247
-
setHandle(
248
248
-
text
249
249
-
.toLowerCase()
250
250
-
.replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, "")
251
251
-
.trim(),
252
252
-
)
253
253
-
}
254
254
-
onKeyPress={onKeyPress}
255
255
-
autoCapitalize="none"
256
256
-
autoCorrect={false}
257
257
-
keyboardType="url"
258
258
-
placeholderTextColor="#666"
259
259
-
containerStyle={{
260
260
-
marginLeft: 28 + 8,
261
261
-
}}
262
262
-
/>
263
263
-
</View>
264
264
-
</View>
265
265
-
266
266
-
<View
267
267
-
style={[
268
268
-
{ flexDirection: "row", justifyContent: "flex-end", zIndex: -32 },
269
269
-
zero.gap.all[3],
270
270
-
]}
271
271
-
>
272
272
-
<Button width="min" onPress={() => onSignup()} variant="ghost">
273
273
-
<Text style={[{ color: "white" }]}>Sign Up on Bluesky</Text>
274
274
-
</Button>
275
275
-
<Button
276
276
-
onPress={submit}
277
277
-
disabled={loginState.loading}
278
278
-
style={[zero.px[6]]}
279
279
-
width="min"
280
280
-
loading={loginState.loading}
281
281
-
>
282
282
-
<Text style={[{ color: "white" }]}>Log in</Text>
283
283
-
</Button>
284
284
-
</View>
68
68
+
<LoginForm onSuccess={onClose} />
285
69
</Pressable>
286
70
</View>
287
71
</Modal>
+5
-168
js/app/components/login/login.tsx
···
1
1
import { useNavigation } from "@react-navigation/native";
2
2
-
import { Button, storage, Text, useTheme, zero } from "@streamplace/components";
2
2
+
import { storage, Text, useTheme, zero } from "@streamplace/components";
3
3
import { Redirect } from "components/aqlink";
4
4
import Loading from "components/loading/loading";
5
5
-
import useActorTypeahead from "hooks/useActorTypeahead";
6
6
-
import { Info } from "lucide-react-native";
7
5
import { useEffect, useState } from "react";
8
8
-
import {
9
9
-
ActivityIndicator,
10
10
-
Alert,
11
11
-
KeyboardAvoidingView,
12
12
-
Linking,
13
13
-
Platform,
14
14
-
Pressable,
15
15
-
ScrollView,
16
16
-
TextInput,
17
17
-
View,
18
18
-
} from "react-native";
6
6
+
import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
19
7
import { useStore } from "store";
20
20
-
import { useIsReady, useLogin, useUserProfile } from "store/hooks";
8
8
+
import { useIsReady, useUserProfile } from "store/hooks";
21
9
import { navigateToRoute } from "../../utils/navigation";
10
10
+
import LoginForm from "./login-form";
22
11
23
12
export default function Login() {
24
13
const { theme } = useTheme();
25
25
-
const loginAction = useStore((state) => state.login);
26
26
-
const openLoginLink = useStore((state) => state.openLoginLink);
27
14
const closeLoginModal = useStore((state) => state.closeLoginModal);
28
15
const userProfile = useUserProfile();
29
29
-
const loginState = useLogin();
30
16
const navigation = useNavigation();
31
31
-
const [handle, setHandle] = useState("");
32
17
const isReady = useIsReady();
33
33
-
const { actors } = useActorTypeahead(handle);
34
34
-
35
35
-
const suggestion =
36
36
-
actors.length > 0 &&
37
37
-
handle.length >= 3 &&
38
38
-
actors[0].handle.startsWith(handle)
39
39
-
? actors[0].handle
40
40
-
: null;
41
41
-
42
42
-
const completionText = suggestion ? suggestion.slice(handle.length) : null;
43
43
-
// null: no return route, undefined: hasn't checked yet
44
18
const [localReturnRoute, setLocalReturnRoute] = useState<
45
19
| {
46
20
name: string;
···
71
45
});
72
46
}, [navigation, closeLoginModal]);
73
47
74
74
-
const submit = () => {
75
75
-
let clean = handle;
76
76
-
if (handle.startsWith("@")) clean = handle.slice(1);
77
77
-
loginAction(clean, openLoginLink);
78
78
-
};
79
79
-
80
80
-
const acceptSuggestion = () => {
81
81
-
if (suggestion) {
82
82
-
setHandle(suggestion);
83
83
-
}
84
84
-
};
85
85
-
86
86
-
const onSignup = () => {
87
87
-
loginAction("https://bsky.social", openLoginLink);
88
88
-
};
89
89
-
90
90
-
const onKeyPress = (e: any) => {
91
91
-
if (e.nativeEvent.key === "Enter") {
92
92
-
submit();
93
93
-
} else if (e.nativeEvent.key === "Tab" && completionText) {
94
94
-
e.preventDefault();
95
95
-
acceptSuggestion();
96
96
-
} else if (e.nativeEvent.key === "ArrowRight" && completionText) {
97
97
-
const input = e.target;
98
98
-
if (input.selectionStart === handle.length) {
99
99
-
e.preventDefault();
100
100
-
acceptSuggestion();
101
101
-
}
102
102
-
}
103
103
-
};
104
104
-
105
105
-
useEffect(() => {
106
106
-
if (loginState?.error) {
107
107
-
Alert.alert("Login error", loginState.error);
108
108
-
}
109
109
-
}, [loginState?.error]);
110
110
-
111
48
if (!isReady || localReturnRoute === undefined) {
112
49
return (
113
50
<View
···
166
103
<Text style={[{ fontSize: 36, fontWeight: "200", color: "white" }]}>
167
104
Log in
168
105
</Text>
169
169
-
<View
170
170
-
style={[
171
171
-
{ flexWrap: "wrap", flexDirection: "row" },
172
172
-
zero.gap.all[1],
173
173
-
]}
174
174
-
>
175
175
-
<Text style={[{ color: theme.colors.textMuted }]}>
176
176
-
Sign in using your handle on the AT Protocol
177
177
-
</Text>
178
178
-
<Pressable
179
179
-
onPress={() => {
180
180
-
const u = new URL(
181
181
-
"https://atproto.academy/docs/Authentication/why",
182
182
-
);
183
183
-
Linking.openURL(u.toString());
184
184
-
}}
185
185
-
>
186
186
-
<Info
187
187
-
size={16}
188
188
-
style={{ paddingTop: 4 }}
189
189
-
color={theme.colors.ring}
190
190
-
/>
191
191
-
</Pressable>
192
192
-
<Text style={[{ color: theme.colors.textMuted }]}>
193
193
-
(e.g. your Bluesky handle)
194
194
-
</Text>
195
195
-
</View>
196
196
-
<View style={[zero.pb[2], { position: "relative" }]}>
197
197
-
<Text style={[{ color: "#aaa" }]}>Handle</Text>
198
198
-
<View style={{ position: "relative" }}>
199
199
-
{completionText && (
200
200
-
<Text
201
201
-
style={[
202
202
-
{
203
203
-
position: "absolute",
204
204
-
left: 12,
205
205
-
top: 12,
206
206
-
color: "#555",
207
207
-
pointerEvents: "none",
208
208
-
zIndex: 1,
209
209
-
},
210
210
-
]}
211
211
-
>
212
212
-
<Text style={{ opacity: 0 }}>{handle}</Text>
213
213
-
{completionText}
214
214
-
</Text>
215
215
-
)}
216
216
-
<TextInput
217
217
-
value={handle}
218
218
-
onChangeText={(text) =>
219
219
-
setHandle(
220
220
-
text
221
221
-
.toLowerCase()
222
222
-
.replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, "")
223
223
-
.trim(),
224
224
-
)
225
225
-
}
226
226
-
onKeyPress={onKeyPress}
227
227
-
style={[
228
228
-
{
229
229
-
backgroundColor: "#1a1a1a",
230
230
-
borderWidth: 1,
231
231
-
borderColor: "#333",
232
232
-
borderRadius: 8,
233
233
-
padding: 12,
234
234
-
color: "white",
235
235
-
position: "relative",
236
236
-
zIndex: 2,
237
237
-
},
238
238
-
]}
239
239
-
autoCapitalize="none"
240
240
-
autoCorrect={false}
241
241
-
keyboardType="url"
242
242
-
placeholderTextColor="#666"
243
243
-
/>
244
244
-
</View>
245
245
-
</View>
246
246
-
<View
247
247
-
style={[
248
248
-
{ flexDirection: "row", justifyContent: "flex-end" },
249
249
-
zero.gap.all[3],
250
250
-
]}
251
251
-
>
252
252
-
<Button width="min" onPress={() => onSignup()} variant="ghost">
253
253
-
<Text style={[{ color: "white" }]}>Sign Up on Bluesky</Text>
254
254
-
</Button>
255
255
-
<Button
256
256
-
onPress={submit}
257
257
-
disabled={loginState.loading}
258
258
-
style={[zero.px[6]]}
259
259
-
width="min"
260
260
-
>
261
261
-
<Text style={[{ color: "white" }]}>
262
262
-
{loginState.loading ? (
263
263
-
<ActivityIndicator size="small" color="white" />
264
264
-
) : (
265
265
-
"Log in"
266
266
-
)}
267
267
-
</Text>
268
268
-
</Button>
269
269
-
</View>
106
106
+
<LoginForm />
270
107
</View>
271
108
</View>
272
109
</ScrollView>
+19
-1
js/app/hooks/useActorTypeahead.tsx
···
26
26
const abortControllerRef = useRef<AbortController | null>(null);
27
27
const lastRequestTimeRef = useRef<number>(0);
28
28
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
29
29
+
const actorsRef = useRef<Actor[]>([]);
29
30
30
31
useEffect(() => {
31
32
if (debounceTimerRef.current) {
···
78
79
const data = await response.json();
79
80
80
81
if (!controller.signal.aborted) {
81
81
-
setActors(data.actors || []);
82
82
+
const newActors = data.actors || [];
83
83
+
84
84
+
// check if actors actually changed
85
85
+
const actorsChanged =
86
86
+
newActors.length !== actorsRef.current.length ||
87
87
+
newActors.some(
88
88
+
(actor: Actor, i: number) =>
89
89
+
actor.did !== actorsRef.current[i]?.did ||
90
90
+
actor.avatar !== actorsRef.current[i]?.avatar,
91
91
+
);
92
92
+
93
93
+
if (actorsChanged) {
94
94
+
actorsRef.current = newActors;
95
95
+
setActors(newActors);
96
96
+
} else {
97
97
+
// keep the same reference to prevent re-renders
98
98
+
setActors(actorsRef.current);
99
99
+
}
82
100
setLoading(false);
83
101
}
84
102
} catch (err: any) {