Live video on the AT Protocol
1import { useNavigation } from "@react-navigation/native";
2import { CircleHelp } from "@tamagui/lucide-icons";
3import { useToastController } from "@tamagui/toast";
4import Loading from "components/loading/loading";
5import NameColorPicker from "components/name-color-picker/name-color-picker";
6import {
7 login,
8 logout,
9 selectChatProfile,
10 selectIsReady,
11 selectLogin,
12 selectUserProfile,
13} from "features/bluesky/blueskySlice";
14import { useEffect, useState } from "react";
15import { KeyboardAvoidingView, Linking, Pressable } from "react-native";
16import { useAppDispatch, useAppSelector } from "store/hooks";
17import {
18 Button,
19 Form,
20 Input,
21 Spinner,
22 Text,
23 View,
24 XStack,
25 YStack,
26} from "tamagui";
27
28export default function Login() {
29 const dispatch = useAppDispatch();
30 const chatProfile = useAppSelector(selectChatProfile);
31 const userProfile = useAppSelector(selectUserProfile);
32 const loginState = useAppSelector(selectLogin);
33 const [handle, setHandle] = useState("");
34 const isReady = useAppSelector(selectIsReady);
35 const toast = useToastController();
36 const navigation = useNavigation();
37
38 const submit = () => {
39 let clean = handle;
40 if (handle.startsWith("@")) clean = handle.slice(1);
41 dispatch(login(clean));
42 };
43 const onEnterPress = (e: any) => {
44 if (e.nativeEvent.key === "Enter") {
45 submit();
46 }
47 };
48
49 useEffect(() => {
50 if (loginState?.error) {
51 toast.show("Login error", {
52 message: loginState.error,
53 });
54 }
55 }, [loginState?.error]);
56
57 if (!isReady) {
58 return (
59 <View f={1} jc="center" ai="stretch" gap="$3">
60 <Loading />
61 </View>
62 );
63 }
64
65 let rgb =
66 chatProfile.profile?.color &&
67 `rgb(${chatProfile.profile?.color?.red},${chatProfile.profile?.color?.green},${chatProfile.profile?.color?.blue})`;
68
69 if (userProfile) {
70 navigation.setOptions({ title: `Account` });
71 return (
72 <View f={1} jc="center" ai="stretch" gap="$3">
73 <Text textAlign="center" fontSize="$8">
74 Hey, <Text color={rgb || "#bd6e86"}>@{userProfile.handle}</Text>.
75 </Text>
76 <View flexDirection="row" gap="$2" justifyContent="center">
77 <Button
78 onPress={() => dispatch(logout())}
79 maxWidth={300}
80 textAlign="center"
81 marginHorizontal="auto"
82 flexBasis={250}
83 >
84 Log out
85 </Button>
86 </View>
87 <NameColorPicker
88 buttonProps={{
89 textAlign: "center",
90 flexBasis: 250,
91 maxWidth: 300,
92 marginHorizontal: "auto",
93 }}
94 />
95 </View>
96 );
97 }
98
99 return (
100 <KeyboardAvoidingView style={{ flex: 1 }} behavior="padding">
101 <Form flex={1} onSubmit={submit}>
102 <View
103 f={1}
104 jc="center"
105 ai="center"
106 padding="$4"
107 width="100%"
108 marginHorizontal="auto"
109 >
110 <YStack
111 px="$6"
112 py="$6"
113 br="$4"
114 backgroundColor="$color2"
115 width="100%"
116 maxWidth={600}
117 gap="$2"
118 >
119 <Text fontSize="$9" fontWeight="200">
120 Log in
121 </Text>
122 <View flexWrap="wrap" flexDirection="row" gap="$1.5">
123 <Text color="$color11">
124 Sign in using your handle on the AT Protocol
125 </Text>
126 <Pressable
127 onPress={() => {
128 const u = new URL(
129 "https://atproto.academy/docs/Authentication/why",
130 );
131 Linking.openURL(u.toString());
132 }}
133 >
134 <CircleHelp size={18} color="lightskyblue" />
135 </Pressable>
136 <Text color="$color11">(e.g. your Bluesky handle)</Text>
137 </View>
138 <YStack gap="$2" py="$4">
139 <Text htmlFor="pdsUrl" color="$color11">
140 Handle
141 </Text>
142 <Input
143 id="pdsUrl"
144 value={handle}
145 onChangeText={(text) =>
146 setHandle(
147 text
148 .toLowerCase()
149 // copying from bsky.app often includes some RTL/LTR characters
150 .replace(/[\u202A\u202C\u200E\u200F\u2066-\u2069]/g, "")
151 .trim(),
152 )
153 }
154 backgroundColor="$color2"
155 onSubmitEditing={onEnterPress}
156 autoCapitalize="none"
157 autoCorrect={false}
158 keyboardType="url"
159 />
160 </YStack>
161 <XStack justifyContent="space-between">
162 <Button
163 onPress={() => navigation.navigate("Signup")}
164 backgroundColor="$gray3"
165 color="$color"
166 >
167 Sign Up
168 </Button>
169 <Form.Trigger asChild>
170 <Button
171 px="$6"
172 // @ts-expect-error Not in the type definition but required for web.
173 type="submit"
174 backgroundColor="$accentColor"
175 disabled={loginState.loading}
176 >
177 <Text>{loginState.loading ? <Spinner /> : `Log in`}</Text>
178 </Button>
179 </Form.Trigger>
180 </XStack>
181 </YStack>
182 </View>
183 </Form>
184 </KeyboardAvoidingView>
185 );
186}