an appview-less Bluesky client using Constellation and PDS Queries
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
1import { AtUri } from "@atproto/api";
2import { useNavigate, type UseNavigateResult } from "@tanstack/react-router";
3import { useAtom } from "jotai";
4import { useState } from "react";
5
6import { useAuth } from "~/providers/UnifiedAuthProvider";
7import { lycanURLAtom } from "~/utils/atoms";
8import { useQueryLycanStatus } from "~/utils/useQuery";
9
10/**
11 * Basically the best equivalent to Search that i can do
12 */
13export function Import({
14 optionaltextstring,
15}: {
16 optionaltextstring?: string;
17}) {
18 const [textInput, setTextInput] = useState<string | undefined>(
19 optionaltextstring
20 );
21 const navigate = useNavigate();
22
23 const { status } = useAuth();
24 const [lycandomain] = useAtom(lycanURLAtom);
25 const lycanExists = lycandomain !== "";
26 const { data: lycanstatusdata } = useQueryLycanStatus();
27 const lycanIndexed = lycanstatusdata?.status === "finished" || false;
28 const lycanIndexing = lycanstatusdata?.status === "in_progress" || false;
29 const lycanIndexingProgress = lycanIndexing
30 ? lycanstatusdata?.progress
31 : undefined;
32 const authed = status === "signedIn";
33
34 const lycanReady = lycanExists && lycanIndexed && authed;
35
36 const handleEnter = () => {
37 if (!textInput) return;
38 handleImport({
39 text: textInput,
40 navigate,
41 lycanReady:
42 lycanReady || (!!lycanIndexingProgress && lycanIndexingProgress > 0),
43 });
44 };
45
46 const placeholder = lycanReady ? "Search..." : "Import...";
47
48 return (
49 <div className="w-full relative">
50 <IconMaterialSymbolsSearch className="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" />
51
52 <input
53 type="text"
54 placeholder={placeholder}
55 value={textInput}
56 onChange={(e) => setTextInput(e.target.value)}
57 onKeyDown={(e) => {
58 if (e.key === "Enter") handleEnter();
59 }}
60 className="w-full h-12 pl-12 pr-4 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-500 box-border transition"
61 />
62 </div>
63 );
64}
65
66function handleImport({
67 text,
68 navigate,
69 lycanReady,
70}: {
71 text: string;
72 navigate: UseNavigateResult<string>;
73 lycanReady?: boolean;
74}) {
75 const trimmed = text.trim();
76 // parse text
77 /**
78 * text might be
79 * 1. bsky dot app url (reddwarf link segments might be uri encoded,)
80 * 2. aturi
81 * 3. plain handle
82 * 4. plain did
83 */
84
85 // 1. Check if it’s a URL
86 try {
87 const url = new URL(text);
88 const knownHosts = [
89 "bsky.app",
90 "social.daniela.lol",
91 "deer.social",
92 "reddwarf.whey.party",
93 "reddwarf.app",
94 "main.bsky.dev",
95 "catsky.social",
96 "blacksky.community",
97 "red-dwarf-social-app.whey.party",
98 "zeppelin.social",
99 ];
100 if (knownHosts.includes(url.hostname)) {
101 // parse path to get URI or handle
102 const path = decodeURIComponent(url.pathname.slice(1)); // remove leading /
103 console.log("BSky URL path:", path);
104 navigate({
105 to: `/${path}`,
106 });
107 return;
108 }
109 } catch {
110 // not a URL, continue
111 }
112
113 // 2. Check if text looks like an at-uri
114 try {
115 if (text.startsWith("at://")) {
116 console.log("AT URI detected:", text);
117 const aturi = new AtUri(text);
118 switch (aturi.collection) {
119 case "app.bsky.feed.post": {
120 navigate({
121 to: "/profile/$did/post/$rkey",
122 params: {
123 did: aturi.host,
124 rkey: aturi.rkey,
125 },
126 });
127 return;
128 }
129 case "app.bsky.actor.profile": {
130 navigate({
131 to: "/profile/$did",
132 params: {
133 did: aturi.host,
134 },
135 });
136 return;
137 }
138 // todo add more handlers as more routes are added. like feeds, lists, etc etc thanks!
139 default: {
140 // continue
141 }
142 }
143 }
144 } catch {
145 // continue
146 }
147
148 // 3. Plain handle (starts with @)
149 try {
150 if (text.startsWith("@")) {
151 const handle = text.slice(1);
152 console.log("Handle detected:", handle);
153 navigate({ to: "/profile/$did", params: { did: handle } });
154 return;
155 }
156 } catch {
157 // continue
158 }
159
160 // 4. Plain DID (starts with did:)
161 try {
162 if (text.startsWith("did:")) {
163 console.log("did detected:", text);
164 navigate({ to: "/profile/$did", params: { did: text } });
165 return;
166 }
167 } catch {
168 // continue
169 }
170
171 // if all else fails
172
173 // try {
174 // // probably a user?
175 // navigate({ to: "/profile/$did", params: { did: text } });
176 // return;
177 // } catch {
178 // // continue
179 // }
180
181 if (lycanReady) {
182 navigate({ to: "/search", search: { q: text } });
183 }
184}