tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
add url import bar in lieu of full-text search
rimar1337
4 months ago
08b6118f
96fdf44f
+230
-30
4 changed files
expand all
collapse all
unified
split
src
components
Import.tsx
Login.tsx
routes
__root.tsx
search.tsx
+149
src/components/Import.tsx
···
1
1
+
import { AtUri } from "@atproto/api";
2
2
+
import { useNavigate, type UseNavigateResult } from "@tanstack/react-router";
3
3
+
import { useState } from "react";
4
4
+
5
5
+
/**
6
6
+
* Basically the best equivalent to Search that i can do
7
7
+
*/
8
8
+
export function Import() {
9
9
+
const [textInput, setTextInput] = useState<string | undefined>();
10
10
+
const navigate = useNavigate();
11
11
+
12
12
+
const handleEnter = () => {
13
13
+
if (!textInput) return;
14
14
+
handleImport({
15
15
+
text: textInput,
16
16
+
navigate,
17
17
+
});
18
18
+
};
19
19
+
20
20
+
return (
21
21
+
<div className="w-full relative">
22
22
+
<IconMaterialSymbolsSearch className="w-5 h-5 absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500" />
23
23
+
24
24
+
<input
25
25
+
type="text"
26
26
+
placeholder="Import..."
27
27
+
value={textInput}
28
28
+
onChange={(e) => setTextInput(e.target.value)}
29
29
+
onKeyDown={(e) => {
30
30
+
if (e.key === "Enter") handleEnter();
31
31
+
}}
32
32
+
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"
33
33
+
/>
34
34
+
</div>
35
35
+
);
36
36
+
}
37
37
+
38
38
+
function handleImport({
39
39
+
text,
40
40
+
navigate,
41
41
+
}: {
42
42
+
text: string;
43
43
+
navigate: UseNavigateResult<string>;
44
44
+
}) {
45
45
+
const trimmed = text.trim();
46
46
+
// parse text
47
47
+
/**
48
48
+
* text might be
49
49
+
* 1. bsky dot app url (deer.social, reddwarf.whey.party, main.bsky.dev, catskys.social) (reddwarf link segments might be uri encoded,)
50
50
+
* 2. aturi
51
51
+
* 3. plain handle
52
52
+
* 4. plain did
53
53
+
*/
54
54
+
55
55
+
// 1. Check if it’s a URL
56
56
+
try {
57
57
+
const url = new URL(text);
58
58
+
const knownHosts = [
59
59
+
"bsky.app",
60
60
+
"social.daniela.lol",
61
61
+
"deer.social",
62
62
+
"reddwarf.whey.party",
63
63
+
"main.bsky.dev",
64
64
+
"catsky.social",
65
65
+
"blacksky.community",
66
66
+
"red-dwarf-social-app.whey.party",
67
67
+
"zeppelin.social",
68
68
+
];
69
69
+
if (knownHosts.includes(url.hostname)) {
70
70
+
// parse path to get URI or handle
71
71
+
const path = decodeURIComponent(url.pathname.slice(1)); // remove leading /
72
72
+
console.log("BSky URL path:", path);
73
73
+
navigate({
74
74
+
to: `/${path}`,
75
75
+
});
76
76
+
return;
77
77
+
}
78
78
+
} catch {
79
79
+
// not a URL, continue
80
80
+
}
81
81
+
82
82
+
// 2. Check if text looks like an at-uri
83
83
+
try {
84
84
+
if (text.startsWith("at://")) {
85
85
+
console.log("AT URI detected:", text);
86
86
+
const aturi = new AtUri(text);
87
87
+
switch (aturi.collection) {
88
88
+
case "app.bsky.feed.post": {
89
89
+
navigate({
90
90
+
to: "/profile/$did/post/$rkey",
91
91
+
params: {
92
92
+
did: aturi.host,
93
93
+
rkey: aturi.rkey,
94
94
+
},
95
95
+
});
96
96
+
return;
97
97
+
}
98
98
+
case "app.bsky.actor.profile": {
99
99
+
navigate({
100
100
+
to: "/profile/$did",
101
101
+
params: {
102
102
+
did: aturi.host,
103
103
+
},
104
104
+
});
105
105
+
return;
106
106
+
}
107
107
+
// todo add more handlers as more routes are added. like feeds, lists, etc etc thanks!
108
108
+
default: {
109
109
+
// continue
110
110
+
}
111
111
+
}
112
112
+
}
113
113
+
} catch {
114
114
+
// continue
115
115
+
}
116
116
+
117
117
+
// 3. Plain handle (starts with @)
118
118
+
try {
119
119
+
if (text.startsWith("@")) {
120
120
+
const handle = text.slice(1);
121
121
+
console.log("Handle detected:", handle);
122
122
+
navigate({ to: "/profile/$did", params: { did: handle } });
123
123
+
return;
124
124
+
}
125
125
+
} catch {
126
126
+
// continue
127
127
+
}
128
128
+
129
129
+
// 4. Plain DID (starts with did:)
130
130
+
try {
131
131
+
if (text.startsWith("did:")) {
132
132
+
console.log("did detected:", text);
133
133
+
navigate({ to: "/profile/$did", params: { did: text } });
134
134
+
return;
135
135
+
}
136
136
+
} catch {
137
137
+
// continue
138
138
+
}
139
139
+
140
140
+
// if all else fails
141
141
+
142
142
+
// try {
143
143
+
// // probably a user?
144
144
+
// navigate({ to: "/profile/$did", params: { did: text } });
145
145
+
// return;
146
146
+
// } catch {
147
147
+
// // continue
148
148
+
// }
149
149
+
}
+3
-3
src/components/Login.tsx
···
24
24
className={
25
25
compact
26
26
? "flex items-center justify-center p-1"
27
27
-
: "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4 flex justify-center items-center h-[280px]"
27
27
+
: "p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-4 mx-4 flex justify-center items-center h-[280px]"
28
28
}
29
29
>
30
30
<span
···
43
43
// Large view
44
44
if (!compact) {
45
45
return (
46
46
-
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-6 mx-4">
46
46
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4">
47
47
<div className="flex flex-col items-center justify-center text-center">
48
48
<p className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100">
49
49
You are logged in!
···
77
77
if (!compact) {
78
78
// Large view renders the form directly in the card
79
79
return (
80
80
-
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-6 mx-4">
80
80
+
<div className="p-4 bg-gray-100 dark:bg-gray-900 rounded-xl border-gray-200 dark:border-gray-800 mt-4 mx-4">
81
81
<UnifiedLoginForm />
82
82
</div>
83
83
);
+28
-26
src/routes/__root.tsx
···
18
18
19
19
import { Composer } from "~/components/Composer";
20
20
import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary";
21
21
+
import { Import } from "~/components/Import";
21
22
import Login from "~/components/Login";
22
23
import { NotFound } from "~/components/NotFound";
23
24
import { FluentEmojiHighContrastGlowingStar } from "~/components/Star";
···
154
155
/>
155
156
156
157
<MaterialNavItem
158
158
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
159
159
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
160
160
+
active={locationEnum === "search"}
161
161
+
onClickCallbback={() =>
162
162
+
navigate({
163
163
+
to: "/search",
164
164
+
//params: { did: agent.assertDid },
165
165
+
})
166
166
+
}
167
167
+
text="Explore"
168
168
+
/>
169
169
+
<MaterialNavItem
157
170
InactiveIcon={
158
171
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
159
172
}
···
180
193
})
181
194
}
182
195
text="Feeds"
183
183
-
/>
184
184
-
<MaterialNavItem
185
185
-
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
186
186
-
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
187
187
-
active={locationEnum === "search"}
188
188
-
onClickCallbback={() =>
189
189
-
navigate({
190
190
-
to: "/search",
191
191
-
//params: { did: agent.assertDid },
192
192
-
})
193
193
-
}
194
194
-
text="Search"
195
196
/>
196
197
<MaterialNavItem
197
198
InactiveIcon={
···
389
390
390
391
<MaterialNavItem
391
392
small
393
393
+
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
394
394
+
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
395
395
+
active={locationEnum === "search"}
396
396
+
onClickCallbback={() =>
397
397
+
navigate({
398
398
+
to: "/search",
399
399
+
//params: { did: agent.assertDid },
400
400
+
})
401
401
+
}
402
402
+
text="Explore"
403
403
+
/>
404
404
+
<MaterialNavItem
405
405
+
small
392
406
InactiveIcon={
393
407
<IconMaterialSymbolsNotificationsOutline className="w-6 h-6" />
394
408
}
···
419
433
/>
420
434
<MaterialNavItem
421
435
small
422
422
-
InactiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
423
423
-
ActiveIcon={<IconMaterialSymbolsSearch className="w-6 h-6" />}
424
424
-
active={locationEnum === "search"}
425
425
-
onClickCallbback={() =>
426
426
-
navigate({
427
427
-
to: "/search",
428
428
-
//params: { did: agent.assertDid },
429
429
-
})
430
430
-
}
431
431
-
text="Search"
432
432
-
/>
433
433
-
<MaterialNavItem
434
434
-
small
435
436
InactiveIcon={
436
437
<IconMaterialSymbolsAccountCircleOutline className="w-6 h-6" />
437
438
}
···
498
499
</main>
499
500
500
501
<aside className="hidden lg:flex h-screen w-[250px] sticky top-0 self-start flex-col">
502
502
+
<div className="px-4 pt-4"><Import /></div>
501
503
<Login />
502
504
503
505
<div className="flex-1"></div>
···
551
553
//params: { did: agent.assertDid },
552
554
})
553
555
}
554
554
-
text="Search"
556
556
+
text="Explore"
555
557
/>
556
558
{/* <Link
557
559
to="/search"
+50
-1
src/routes/search.tsx
···
1
1
import { createFileRoute } from "@tanstack/react-router";
2
2
3
3
+
import { Header } from "~/components/Header";
4
4
+
import { Import } from "~/components/Import";
5
5
+
3
6
export const Route = createFileRoute("/search")({
4
7
component: Search,
5
8
});
6
9
7
10
export function Search() {
8
8
-
return <div className="p-6">Search page (coming soon)</div>;
11
11
+
return (
12
12
+
<>
13
13
+
<Header
14
14
+
title="Explore"
15
15
+
backButtonCallback={() => {
16
16
+
if (window.history.length > 1) {
17
17
+
window.history.back();
18
18
+
} else {
19
19
+
window.location.assign("/");
20
20
+
}
21
21
+
}}
22
22
+
/>
23
23
+
<div className=" flex flex-col items-center mt-4 mx-4 gap-4">
24
24
+
<Import />
25
25
+
<div className="flex flex-col">
26
26
+
<p className="text-gray-600 dark:text-gray-400">
27
27
+
Sorry we dont have search. But instead, you can load some of these
28
28
+
types of content into Red Dwarf:
29
29
+
</p>
30
30
+
<ul className="list-disc list-inside mt-2 text-gray-600 dark:text-gray-400">
31
31
+
<li>
32
32
+
Bluesky URLs from supported clients (like{" "}
33
33
+
<code className="text-sm">bsky.app</code> or{" "}
34
34
+
<code className="text-sm">deer.social</code>).
35
35
+
</li>
36
36
+
<li>
37
37
+
AT-URIs (e.g.,{" "}
38
38
+
<code className="text-sm">at://did:example/collection/item</code>
39
39
+
).
40
40
+
</li>
41
41
+
<li>
42
42
+
Plain handles (like{" "}
43
43
+
<code className="text-sm">@username.bsky.social</code>).
44
44
+
</li>
45
45
+
<li>
46
46
+
Direct DIDs (Decentralized Identifiers, starting with{" "}
47
47
+
<code className="text-sm">did:</code>).
48
48
+
</li>
49
49
+
</ul>
50
50
+
<p className="mt-2 text-gray-600 dark:text-gray-400">
51
51
+
Simply paste one of these into the import field above and press
52
52
+
Enter to load the content.
53
53
+
</p>
54
54
+
</div>
55
55
+
</div>
56
56
+
</>
57
57
+
);
9
58
}