frontend client for gemstone. decentralised workplace app

Compare changes

Choose any two refs to compare.

+3 -9
src/components/Settings/LatticeInfo.tsx
···
import type { AtUri } from "@/lib/types/atproto";
import type { SystemsGmstnDevelopmentLattice } from "@/lib/types/lexicon/systems.gmstn.development.lattice";
import { useCurrentPalette } from "@/providers/ThemeProvider";
-
import { getOwnerInfoFromLattice } from "@/queries/get-owner-info-from-lattice";
-
import { useQuery } from "@tanstack/react-query";
+
import { useLatticeInfoQuery } from "@/queries/hooks/useLatticeInfoQuery";
import { BadgeCheck, X } from "lucide-react-native";
import { View } from "react-native";
···
};
}) => {
const latticeDomain = lattice.uri.rKey;
-
const { isLoading, data: latticeInfo } = useQuery({
-
queryKey: ["shardInfo", latticeDomain],
-
queryFn: async () => {
-
return await getOwnerInfoFromLattice(latticeDomain);
-
},
-
retry: 1,
-
});
+
const { useQuery } = useLatticeInfoQuery(latticeDomain);
+
const { isLoading, data: latticeInfo } = useQuery();
const { semantic } = useCurrentPalette();
return (
+3 -9
src/components/Settings/ShardInfo.tsx
···
import type { AtUri } from "@/lib/types/atproto";
import type { SystemsGmstnDevelopmentShard } from "@/lib/types/lexicon/systems.gmstn.development.shard";
import { useCurrentPalette } from "@/providers/ThemeProvider";
-
import { getOwnerInfoFromShard } from "@/queries/get-owner-info-from-shard";
-
import { useQuery } from "@tanstack/react-query";
+
import { useShardInfoQuery } from "@/queries/hooks/useShardInfoQuery";
import { BadgeCheck, X } from "lucide-react-native";
import { View } from "react-native";
···
};
}) => {
const shardDomain = shard.uri.rKey;
-
const { isLoading, data: shardInfo } = useQuery({
-
queryKey: ["shardInfo", shardDomain],
-
queryFn: async () => {
-
return await getOwnerInfoFromShard(shardDomain);
-
},
-
retry: 1,
-
});
+
const { useQuery } = useShardInfoQuery(shardDomain);
+
const { isLoading, data: shardInfo } = useQuery();
const { semantic } = useCurrentPalette();
return (
+17
src/queries/hooks/useLatticeInfoQuery.ts
···
+
import { getOwnerInfoFromLattice } from "@/queries/get-owner-info-from-lattice";
+
import { useQuery } from "@tanstack/react-query";
+
+
export const useLatticeInfoQuery = (latticeDomain: string) => {
+
const queryKey = ["latticeInfo", latticeDomain];
+
return {
+
queryKey,
+
useQuery: () =>
+
useQuery({
+
queryKey,
+
queryFn: async () => {
+
return await getOwnerInfoFromLattice(latticeDomain);
+
},
+
retry: 1,
+
}),
+
};
+
};
+17
src/queries/hooks/useShardInfoQuery.ts
···
+
import { getOwnerInfoFromShard } from "@/queries/get-owner-info-from-shard";
+
import { useQuery } from "@tanstack/react-query";
+
+
export const useShardInfoQuery = (shardDomain: string) => {
+
const queryKey = ["shardInfo", shardDomain];
+
return {
+
queryKey,
+
useQuery: () =>
+
useQuery({
+
queryKey,
+
queryFn: async () => {
+
return await getOwnerInfoFromShard(shardDomain);
+
},
+
retry: 1,
+
}),
+
};
+
};
+1 -5
src/queries/get-channels-from-pds.ts
···
if (!success) return { ok: false, error: z.treeifyError(error) };
-
allRecords.push(
-
...responses.map((data) => {
-
return data;
-
}),
-
);
+
allRecords.push(...responses);
if (records.length < 100) continueLoop = false;
cursor = nextCursor;
+4 -5
src/queries/get-invites-from-pds.ts
···
}): Promise<
Result<
Array<{
+
cid: string;
uri: string;
value: SystemsGmstnDevelopmentChannelInvite;
}>,
···
}): Promise<
Result<
Array<{
+
cid: string;
uri: string;
value: SystemsGmstnDevelopmentChannelInvite;
}>,
···
>
> => {
const allRecords: Array<{
+
cid: string;
uri: string;
value: SystemsGmstnDevelopmentChannelInvite;
}> = [];
···
.safeParse(records);
if (!success) return { ok: false, error: z.treeifyError(error) };
-
allRecords.push(
-
...responses.map((data) => {
-
return { uri: data.uri, value: data.value };
-
}),
-
);
+
allRecords.push(...responses);
if (records.length < 100) continueLoop = false;
cursor = nextCursor;
+4 -5
src/queries/get-lattices-from-pds.ts
···
}): Promise<
Result<
Array<{
+
cid: string;
uri: string;
value: SystemsGmstnDevelopmentLattice;
}>,
···
}): Promise<
Result<
Array<{
+
cid: string;
uri: string;
value: SystemsGmstnDevelopmentLattice;
}>,
···
>
> => {
const allRecords: Array<{
+
cid: string;
uri: string;
value: SystemsGmstnDevelopmentLattice;
}> = [];
···
if (!success) return { ok: false, error: z.treeifyError(error) };
-
allRecords.push(
-
...responses.map((data) => {
-
return { uri: data.uri, value: data.value };
-
}),
-
);
+
allRecords.push(...responses);
if (records.length < 100) continueLoop = false;
cursor = nextCursor;
+4 -5
src/queries/get-shards-from-pds.ts
···
}): Promise<
Result<
Array<{
+
cid: string;
uri: string;
value: SystemsGmstnDevelopmentShard;
}>,
···
}): Promise<
Result<
Array<{
+
cid: string;
uri: string;
value: SystemsGmstnDevelopmentShard;
}>,
···
>
> => {
const allRecords: Array<{
+
cid: string;
uri: string;
value: SystemsGmstnDevelopmentShard;
}> = [];
···
if (!success) return { ok: false, error: z.treeifyError(error) };
-
allRecords.push(
-
...responses.map((data) => {
-
return { uri: data.uri, value: data.value };
-
}),
-
);
+
allRecords.push(...responses);
if (records.length < 100) continueLoop = false;
cursor = nextCursor;
+6 -3
src/app/_layout.tsx
···
import { Slot, SplashScreen } from "expo-router";
import { useEffect } from "react";
import { Platform, View } from "react-native";
+
import { GestureHandlerRootView } from "react-native-gesture-handler";
const RootLayoutInner = () => {
const [loaded, error] = useFonts({
···
const RootLayout = () => {
return (
-
<RootProviders>
-
<RootLayoutInner />
-
</RootProviders>
+
<GestureHandlerRootView>
+
<RootProviders>
+
<RootLayoutInner />
+
</RootProviders>
+
</GestureHandlerRootView>
);
};
+2 -2
src/components/Settings/LatticeSettings.tsx
···
const { atoms, typography } = useFacet();
const session = useOAuthSessionGuaranteed();
const [showRegisterModal, setShowRegisterModal] = useState(false);
-
const { useQuery } = useLatticesQuery(session)
+
const { useQuery } = useLatticesQuery(session);
-
const { data: lattices, isLoading } = useQuery()
+
const { data: lattices, isLoading } = useQuery();
return isLoading ? (
<Loading />
+2 -2
src/components/Settings/ShardSettings.tsx
···
const { atoms, typography } = useFacet();
const session = useOAuthSessionGuaranteed();
const [showRegisterModal, setShowRegisterModal] = useState(false);
-
const { useQuery } = useShardsQuery(session)
+
const { useQuery } = useShardsQuery(session);
-
const { data: shards, isLoading } = useQuery()
+
const { data: shards, isLoading } = useQuery();
return isLoading ? (
<Loading />
+5 -2
src/components/Settings/RegisterLatticeModalContent.tsx
···
},
});
+
const readyToSubmit = !!inputText.trim();
+
return (
<View
style={{
···
/>
</View>
<Pressable
+
disabled={!readyToSubmit}
onPress={() => {
newLatticeMutation();
}}
···
) : (
<View
style={{
-
backgroundColor: inputText.trim()
+
backgroundColor: readyToSubmit
? hovered
? lighten(semantic.primary, 7)
: semantic.primary
: registerError
? semantic.error
-
: semantic.border,
+
: semantic.textPlaceholder,
borderRadius: atoms.radii.lg,
alignItems: "center",
paddingVertical: 10,
+5 -2
src/components/Settings/RegisterShardModalContent.tsx
···
},
});
+
const readyToSubmit = !!inputText.trim();
+
return (
<View
style={{
···
/>
</View>
<Pressable
+
disabled={!readyToSubmit}
onPress={() => {
newShardMutation();
}}
···
) : (
<View
style={{
-
backgroundColor: inputText.trim()
+
backgroundColor: readyToSubmit
? hovered
? lighten(semantic.primary, 7)
: semantic.primary
: registerError
? semantic.error
-
: semantic.border,
+
: semantic.textPlaceholder,
borderRadius: atoms.radii.lg,
alignItems: "center",
paddingVertical: 10,
+2 -5
src/components/Settings/ChannelInfo.tsx
···
import { InviteUserModalContent } from "@/components/Settings/InviteUserModalContent";
import { useFacet } from "@/lib/facet";
import { fade } from "@/lib/facet/src/lib/colors";
-
import type { AtUri } from "@/lib/types/atproto";
import type { SystemsGmstnDevelopmentChannel } from "@/lib/types/lexicon/systems.gmstn.development.channel";
-
import { atUriToString } from "@/lib/utils/atproto";
import { useCurrentPalette } from "@/providers/ThemeProvider";
import { Hash, UserRoundPlus } from "lucide-react-native";
import { useState } from "react";
···
}: {
channel: {
value: SystemsGmstnDevelopmentChannel;
-
uri: Required<AtUri>;
+
uriStr: string;
cid: string;
};
}) => {
const { semantic } = useCurrentPalette();
const { atoms } = useFacet();
const [showInviteModal, setShowInviteModal] = useState(false);
-
const channelAtUri = atUriToString(channel.uri);
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
···
>
<InviteUserModalContent
setShowInviteModal={setShowInviteModal}
-
channelAtUri={channelAtUri}
+
channelAtUri={channel.uriStr}
channelCid={channel.cid}
/>
</Pressable>
+1 -1
src/components/Settings/ChannelSettings.tsx
···
{ color: semantic.textInverse },
]}
>
-
Add
+
Add
</Text>
</View>
)}
-1
src/components/Settings/InviteUserModalContent.tsx
···
$type: "com.atproto.repo.strongRef",
},
});
-
console.log(inviteRes)
if (!inviteRes.ok) throw new Error(inviteRes.error);
},
onSuccess: async () => {
+3
src/components/Navigation/TopBar/ProfileModalContent.tsx
···
<Link href="/profile" style={listItemStyles} asChild>
<LinkText label="Profile" />
</Link>
+
<Link href="/invites" style={listItemStyles} asChild>
+
<LinkText label="Invites" />
+
</Link>
<Link href="/settings" style={listItemStyles} asChild>
<LinkText label="Settings" />
</Link>
+4
src/lib/consts.ts
···
};
export const DEFAULT_STALE_TIME = 5 * 60 * 1000;
+
+
export const CONSTELLATION_URL = new URL(
+
"https://constellation.microcosm.blue/",
+
);
+18
src/lib/types/constellation.ts
···
+
import { didSchema, nsidSchema } from "@/lib/types/atproto";
+
import { z } from "zod";
+
+
export const constellationBacklinkSchema = z.object({
+
did: didSchema,
+
collection: nsidSchema,
+
rkey: z.string(),
+
});
+
export type ConstellationBacklink = z.infer<typeof constellationBacklinkSchema>;
+
+
export const constellationBacklinkResponseSchema = z.object({
+
total: z.number(),
+
records: z.array(constellationBacklinkSchema),
+
cursor: z.optional(z.string().nullish()),
+
});
+
export type ConstellationBacklinkResponse = z.infer<
+
typeof constellationBacklinkResponseSchema
+
>;
+2 -5
src/lib/utils/constellation.ts
···
import { CONSTELLATION_URL } from "@/lib/consts";
-
import type {
-
ConstellationBacklinkResponse} from "@/lib/types/constellation";
-
import {
-
constellationBacklinkResponseSchema,
-
} from "@/lib/types/constellation";
+
import type { ConstellationBacklinkResponse } from "@/lib/types/constellation";
+
import { constellationBacklinkResponseSchema } from "@/lib/types/constellation";
import type { Result } from "@/lib/utils/result";
import type { ZodError } from "zod";
+2
src/queries/get-invites-from-constellation.ts
···
unknown
>
> => {
+
// FIXME: If there are too many records, we can't get them all
+
// because we aren't tracking the cursor and looping calls to get the backlinks
const backlinksResult = await getConstellationBacklinks({
subject: did,
source: {
+17
src/lib/utils/arrays.ts
···
+
export const partition = <T>(
+
array: Array<T>,
+
predicate: (value: T, index: number, array: Array<T>) => boolean,
+
): [Array<T>, Array<T>] => {
+
const truthy: Array<T> = [];
+
const falsy: Array<T> = [];
+
+
array.forEach((value, index, arr) => {
+
if (predicate(value, index, arr)) {
+
truthy.push(value);
+
} else {
+
falsy.push(value);
+
}
+
});
+
+
return [truthy, falsy];
+
};
+2
src/queries/hooks/useConstellationInvitesQuery.ts
···
import type { OAuthSession } from "@atproto/oauth-client";
import { useQuery } from "@tanstack/react-query";
+
// TODO: use prism instead, so that we can get the backlink, the invite, and the channel's actual record
+
// and not just the strongRef.
export const useConstellationInvitesQuery = (session: OAuthSession) => {
const queryKey = ["invites", session.did];
return {
+21
src/queries/get-invite-from-pds.ts
···
+
import type { AtUri } from "@/lib/types/atproto";
+
import type { SystemsGmstnDevelopmentChannelInvite } from "@/lib/types/lexicon/systems.gmstn.development.channel.invite";
+
import { systemsGmstnDevelopmentChannelInviteRecordSchema } from "@/lib/types/lexicon/systems.gmstn.development.channel.invite";
+
import { getRecordFromFullAtUri } from "@/lib/utils/atproto";
+
import type { Result } from "@/lib/utils/result";
+
+
export const getInviteFromPds = async (
+
atUri: Required<AtUri>,
+
): Promise<Result<SystemsGmstnDevelopmentChannelInvite, unknown>> => {
+
const record = await getRecordFromFullAtUri(atUri);
+
if (!record.ok) return { ok: false, error: record.error };
+
+
const {
+
success,
+
error,
+
data: invite,
+
} = systemsGmstnDevelopmentChannelInviteRecordSchema.safeParse(record.data);
+
if (!success) return { ok: false, error };
+
+
return { ok: true, data: invite };
+
};
+30
src/queries/hooks/useInviteQuery.ts
···
+
import type { AtUri } from "@/lib/types/atproto";
+
import { getInviteFromPds } from "@/queries/get-invite-from-pds";
+
import { useQuery } from "@tanstack/react-query";
+
+
export const useInviteQuery = (atUri: Required<AtUri>) => {
+
const queryKey = ["invite", atUri.authority, atUri.rKey];
+
return {
+
queryKey,
+
useQuery: () =>
+
useQuery({
+
queryKey,
+
queryFn: async () => {
+
return await pdsInviteQueryFn(atUri);
+
},
+
}),
+
};
+
};
+
+
const pdsInviteQueryFn = async (atUri: Required<AtUri>) => {
+
const invites = await getInviteFromPds(atUri);
+
+
if (!invites.ok) {
+
console.error("pdsInviteQueryFn error.", invites.error);
+
throw new Error(
+
`Something went wrong while getting the user's invite record directly.}`,
+
);
+
}
+
+
return invites.data;
+
};
+1
src/providers/authed/MembershipsProvider.tsx
···
const { session, isLoading, agent } = oauth;
const isOAuthReady = !isLoading && !!agent && !!session;
+
// TODO: move this to own query hook
const membershipsQuery = useQuery({
queryKey: ["membership", session?.did],
enabled: isOAuthReady,
+15 -15
src/queries/initiate-handshake-to.ts
···
if (!latticeUrlResult.ok)
return { ok: false, error: latticeUrlResult.error };
-
let jwt = "";
-
if (did.startsWith("did:web:localhost")) {
-
if (__DEV__)
-
jwt = await requestInterServiceJwtFromPds({ oauth, aud: did });
-
else
-
return {
-
ok: false,
-
error: `Cannot initiate handshake to a lattice at localhost. Provided handshake target's DID was ${did}`,
-
};
-
} else {
-
// do proxy
-
// for now we return error
-
// FIXME: actually do service proxying.
-
return { ok: false, error: "Service proxying not yet implemented" };
-
}
+
// FIXME: actually do service proxying.
+
const jwt = await requestInterServiceJwtFromPds({ oauth, aud: did });
+
// if (did.startsWith("did:web:localhost")) {
+
// if (__DEV__)
+
// jwt = await requestInterServiceJwtFromPds({ oauth, aud: did });
+
// else
+
// return {
+
// ok: false,
+
// error: `Cannot initiate handshake to a lattice at localhost. Provided handshake target's DID was ${did}`,
+
// };
+
// } else {
+
// // do proxy
+
// // for now we return error
+
// return { ok: false, error: "Service proxying not yet implemented" };
+
// }
const latticeBaseUrl = latticeUrlResult.data.origin;
+67 -1
src/lib/utils/atproto/index.ts
···
atUriAuthoritySchema,
nsidSchema,
} from "@/lib/types/atproto";
-
import { comAtprotoRepoGetRecordResponseSchema } from "@/lib/types/lexicon/com.atproto.repo.getRecord";
+
import type {
+
ComAtprotoRepoGetRecordResponse} from "@/lib/types/lexicon/com.atproto.repo.getRecord";
+
import {
+
comAtprotoRepoGetRecordResponseSchema,
+
} from "@/lib/types/lexicon/com.atproto.repo.getRecord";
import type { Result } from "@/lib/utils/result";
import type { DidDocumentResolver } from "@atcute/identity-resolver";
import {
···
return { ok: true, data: record.value };
};
+
export const getCommitFromFullAtUri = async ({
+
authority,
+
collection,
+
rKey,
+
}: AtUri): Promise<Result<ComAtprotoRepoGetRecordResponse, unknown>> => {
+
const didDocResult = await resolveDidDoc(authority);
+
if (!didDocResult.ok) return { ok: false, error: didDocResult.error };
+
+
if (!collection || !rKey)
+
return {
+
ok: false,
+
error: "No rkey or collection found in provided AtUri object",
+
};
+
+
const { service: services } = didDocResult.data;
+
if (!services)
+
return {
+
ok: false,
+
error: { message: "Resolved DID document has no service field." },
+
};
+
+
const pdsService = services.find(
+
(service) =>
+
service.id === "#atproto_pds" &&
+
service.type === "AtprotoPersonalDataServer",
+
);
+
+
if (!pdsService)
+
return {
+
ok: false,
+
error: {
+
message:
+
"Resolved DID document has no PDS service listed in the document.",
+
},
+
};
+
+
const pdsEndpointRecord = pdsService.serviceEndpoint;
+
let pdsEndpointUrl;
+
try {
+
// @ts-expect-error yes, we are coercing something that is explicitly not a string into a string, but in this case we want to be specific. only serviceEndpoints with valid atproto pds URLs should be allowed.
+
pdsEndpointUrl = new URL(pdsEndpointRecord).origin;
+
} catch (err) {
+
return { ok: false, error: err };
+
}
+
const req = new Request(
+
`${pdsEndpointUrl}/xrpc/com.atproto.repo.getRecord?repo=${didDocResult.data.id}&collection=${collection}&rkey=${rKey}`,
+
);
+
+
const res = await fetch(req);
+
const data: unknown = await res.json();
+
+
const {
+
success: responseParseSuccess,
+
error: responseParseError,
+
data: record,
+
} = comAtprotoRepoGetRecordResponseSchema.safeParse(data);
+
if (!responseParseSuccess) {
+
return { ok: false, error: responseParseError };
+
}
+
return { ok: true, data: record };
+
};
+
export const didDocResolver: DidDocumentResolver =
new CompositeDidDocumentResolver({
methods: {
+1 -1
assets/oauth-client-metadata.json
···
"client_name": "Gemstone",
"client_uri": "https://app.gmstn.systems",
"redirect_uris": [
-
"systems.gmstn.app:/oauth/callback"
+
"systems.gmstn.app:/login/"
],
"scope": "atproto transition:generic",
"token_endpoint_auth_method": "none",
+3 -1
package.json
···
"dev:web": "expo start --web",
"dev:android": "expo start --android",
"dev:ios": "expo start --ios",
-
"dev:expo": "expo start"
+
"dev:expo": "expo start",
+
"export:web": "expo export -p web",
+
"export:web:serve": "expo serve"
},
"dependencies": {
"@atcute/atproto": "^3.1.7",
+7 -5
src/components/Chat/index.tsx
···
export const Chat = ({ channelAtUri }: { channelAtUri: AtUri }) => {
const [inputText, setInputText] = useState("");
-
const { messages, sendMessageToChannel, isConnected } =
-
useChannel(channelAtUri);
const record = useChannelRecordByAtUriObject(channelAtUri);
const { semantic } = useCurrentPalette();
const { typography, atoms } = useFacet();
+
const channel = useChannel(channelAtUri);
+
const { isLoading } = useProfile();
+
+
if (!channel) return <></>;
+
+
const { messages, sendMessageToChannel, isConnected } = channel;
const handleSend = () => {
if (inputText.trim()) {
···
}
};
-
const { isLoading } = useProfile();
-
if (!record)
return (
<View>
<Text>
-
Something has gone wrong. Could not resolve channel record
+
Something has gone wrong.Could not resolve channel record
from given at:// URI.
</Text>
</View>
+17 -9
src/lib/hooks/useChannel.ts
···
const { sessionInfo, socket } = findChannelSession(channel);
useEffect(() => {
-
if (!sessionInfo)
-
throw new Error(
+
if (!sessionInfo) {
+
console.warn(
"Channel did not resolve to a valid sessionInfo object.",
);
-
if (!socket)
-
throw new Error(
+
return;
+
}
+
if (!socket) {
+
console.warn(
"Session info did not resolve to a valid websocket connection. This should not happen and is likely a bug. Check the sessions map object.",
);
+
return;
+
}
// attach handlers here
···
};
}, [socket, sessionInfo, channel]);
-
if (!oAuthSession) throw new Error("No OAuth session");
-
if (!sessionInfo)
-
throw new Error(
+
if (!oAuthSession) {console.warn("No OAuth session"); return }
+
if (!sessionInfo) {
+
console.warn(
"Channel did not resolve to a valid sessionInfo object.",
);
-
if (!socket)
-
throw new Error(
+
return;
+
}
+
if (!socket) {
+
console.warn(
"Session info did not resolve to a valid websocket connection. This should not happen and is likely a bug. Check the sessions map object.",
);
+
return;
+
}
const channelStringified = atUriToString(channel);
+1 -3
src/providers/authed/SessionsProvider.tsx
···
console.log("tried to find", channel);
if (!sessionInfo)
-
throw new Error(
-
"Provided channel at:// URI (object) could not be found in any existing lattice sessions",
-
);
+
return { sessionInfo: undefined, socket: undefined };
return { sessionInfo, socket: sessionsMap.get(sessionInfo) };
},
+1 -1
src/lib/utils/atproto/oauth.web.ts
···
} from "@atproto/oauth-client-browser";
import type { ExpoOAuthClientOptions } from "@atproto/oauth-client-expo";
import { ExpoOAuthClient as PbcWebExpoOAuthClient } from "@atproto/oauth-client-expo";
-
import oAuthMetadata from "../../../../assets/oauth-client-metadata.json";
+
import oAuthMetadata from "../../../../public/oauth-client-metadata.json";
import { __DEV__loopbackOAuthMetadata } from "@/lib/consts";
// suuuuuch a hack holy shit
+82 -23
src/components/Auth/Login.web.tsx
···
+
import { GmstnLogoColor } from "@/components/icons/gmstn/GmstnLogoColor";
+
import { Text } from "@/components/primitives/Text";
+
import { useFacet } from "@/lib/facet";
+
import { lighten } from "@/lib/facet/src/lib/colors";
import { useOAuthSetter, useOAuthValue } from "@/providers/OAuthProvider";
+
import { useCurrentPalette } from "@/providers/ThemeProvider";
import { Agent } from "@atproto/api";
+
import { ArrowRight } from "lucide-react-native";
import { useState } from "react";
-
import { Button, StyleSheet, TextInput, View } from "react-native";
+
import { Pressable, TextInput, View } from "react-native";
export const Login = () => {
+
const { semantic } = useCurrentPalette();
+
const { atoms, typography } = useFacet();
const [atprotoHandle, setAtprotoHandle] = useState("");
const oAuth = useOAuthValue();
const setOAuth = useOAuthSetter();
···
};
return (
-
<View>
-
<TextInput
-
style={styles.input}
-
value={atprotoHandle}
-
onChangeText={setAtprotoHandle}
-
placeholder="alice.bsky.social"
-
onSubmitEditing={handleSubmit}
-
/>
-
<Button title="Log in with your PDS ->" onPress={handleSubmit} />
+
<View
+
style={{
+
flex: 1,
+
flexDirection: "column",
+
alignItems: "center",
+
justifyContent: "center",
+
gap: 16,
+
}}
+
>
+
<View style={{ alignItems: "center" }}>
+
<View style={{ padding: 8, paddingLeft: 12, paddingTop: 12 }}>
+
<GmstnLogoColor height={36} width={36} />
+
</View>
+
<Text
+
style={[
+
typography.sizes.xl,
+
typography.weights.byName.medium,
+
]}
+
>
+
Gemstone
+
</Text>
+
</View>
+
<View style={{ gap: 10 }}>
+
<TextInput
+
style={[{
+
flex: 1,
+
borderWidth: 1,
+
borderColor: semantic.border,
+
borderRadius: atoms.radii.lg,
+
paddingHorizontal: 14,
+
paddingVertical: 12,
+
marginRight: 8,
+
fontSize: 16,
+
color: semantic.text
+
}, typography.weights.byName.light]}
+
value={atprotoHandle}
+
onChangeText={setAtprotoHandle}
+
placeholder="alice.bsky.social"
+
onSubmitEditing={handleSubmit}
+
placeholderTextColor={semantic.textPlaceholder}
+
/>
+
<Pressable onPress={handleSubmit}>
+
{({ hovered }) => (
+
<View
+
style={{
+
backgroundColor: hovered
+
? lighten(semantic.primary, 7)
+
: semantic.primary,
+
flexDirection: "row",
+
gap: 4,
+
alignItems: "center",
+
justifyContent: "center",
+
paddingVertical: 10,
+
borderRadius: atoms.radii.lg,
+
}}
+
>
+
<Text
+
style={[
+
{ color: semantic.textInverse },
+
typography.weights.byName.normal,
+
]}
+
>
+
Log in with ATProto
+
</Text>
+
<ArrowRight
+
height={16}
+
width={16}
+
color={semantic.textInverse}
+
/>
+
</View>
+
)}
+
</Pressable>
+
</View>
</View>
);
};
-
-
const styles = StyleSheet.create({
-
input: {
-
flex: 1,
-
borderWidth: 1,
-
borderColor: "#ccc",
-
borderRadius: 8,
-
paddingHorizontal: 12,
-
paddingVertical: 8,
-
marginRight: 8,
-
fontSize: 16,
-
},
-
});