BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 268 lines 9.0 kB view raw
1import { useAppPreferences } from "$/contexts/app-preferences"; 2import { useAppSession } from "$/contexts/app-session"; 3import { useAppShellUi } from "$/contexts/app-shell-ui"; 4import { HashRouter, Navigate, Route, useLocation, useParams } from "@solidjs/router"; 5import type { RouteSectionProps } from "@solidjs/router"; 6import { type Component, createEffect, type JSX, type ParentProps, Show } from "solid-js"; 7import { Dynamic } from "solid-js/web"; 8import { DeckWorkspace } from "./components/deck/DeckWorkspace"; 9import { ExplorerPanel } from "./components/explorer/ExplorerPanel"; 10import { SavedPostsPanel } from "./components/saved/SavedPostsPanel"; 11import { HashtagPanel } from "./components/search/HashtagPanel"; 12import { SearchPanel } from "./components/search/SearchPanel"; 13import { SearchPreflightPanel } from "./components/search/SearchPreflightPanel"; 14import { SettingsPanel } from "./components/settings/SettingsPanel"; 15import { decodeMessagesRouteMemberDid } from "./lib/conversations"; 16import { TIMELINE_ROUTE } from "./lib/feeds"; 17import { decodePostRouteUri } from "./lib/post-routes"; 18import { decodeProfileRouteActor } from "./lib/profile"; 19import { buildSearchPreflightRoute, decodeHashtagRouteTag, parseSearchRouteState } from "./lib/search-routes"; 20 21type TMessagesRouteProps = { memberDid: string | null }; 22type TPostRouteProps = { uri: string | null }; 23type TProfileRouteProps = { actor: string | null }; 24 25type AppShellProps = ParentProps<{ fullWidth?: boolean }>; 26 27type AppRouterProps = { 28 renderAuth: () => JSX.Element; 29 renderComposer: () => JSX.Element; 30 renderMessages: Component<TMessagesRouteProps>; 31 renderNotifications: () => JSX.Element; 32 renderPostEngagement: Component<TPostRouteProps>; 33 renderPost: Component<TPostRouteProps>; 34 renderProfile: Component<TProfileRouteProps>; 35 renderShell: Component<AppShellProps>; 36 renderTimeline: () => JSX.Element; 37}; 38 39export function AppRouter(props: AppRouterProps) { 40 const session = useAppSession(); 41 const shell = useAppShellUi(); 42 43 const RouterFrame: Component<RouteSectionProps> = (routeProps) => { 44 const location = useLocation(); 45 let previousPath = location.pathname; 46 const standaloneComposerRoute = () => location.pathname === "/composer"; 47 48 createEffect(() => { 49 const nextPath = location.pathname; 50 if (nextPath !== previousPath) { 51 shell.closeSwitcher(); 52 previousPath = nextPath; 53 } 54 }); 55 56 const fullWidthShell = () => location.pathname === "/explorer" || location.pathname === "/deck"; 57 58 return ( 59 <Show 60 when={standaloneComposerRoute()} 61 fallback={<props.renderShell fullWidth={fullWidthShell()}>{routeProps.children}</props.renderShell>}> 62 {routeProps.children} 63 </Show> 64 ); 65 }; 66 67 const IndexRoute = () => ( 68 <Show when={!session.bootstrapping} fallback={<RouteLoadingState />}> 69 <Navigate href={session.hasSession ? TIMELINE_ROUTE : "/auth"} /> 70 </Show> 71 ); 72 73 const AuthRoute = () => <PublicOnlyRoute redirectHref={TIMELINE_ROUTE}>{props.renderAuth()}</PublicOnlyRoute>; 74 75 const TimelineRoute = () => <ProtectedRouteView>{props.renderTimeline()}</ProtectedRouteView>; 76 77 const SearchRoute = () => ( 78 <ProtectedRouteView> 79 <SearchRouteGate /> 80 </ProtectedRouteView> 81 ); 82 83 const SearchPreflightRoute = () => ( 84 <ProtectedRouteView> 85 <SearchPreflightPanel /> 86 </ProtectedRouteView> 87 ); 88 89 const ProfileRoute = () => ( 90 <ProtectedRouteView> 91 <Dynamic component={props.renderProfile} actor={null} /> 92 </ProtectedRouteView> 93 ); 94 95 const ActorProfileRoute = () => { 96 const params = useParams<{ actor: string }>(); 97 98 return ( 99 <ProtectedRouteView> 100 <Dynamic component={props.renderProfile} actor={decodeProfileRouteActor(params.actor)} /> 101 </ProtectedRouteView> 102 ); 103 }; 104 105 const NotificationsRoute = () => <ProtectedRouteView>{props.renderNotifications()}</ProtectedRouteView>; 106 107 const PostRoute = () => { 108 const params = useParams<{ encodedUri: string }>(); 109 110 return ( 111 <ProtectedRouteView> 112 <Dynamic component={props.renderPost} uri={decodePostRouteUri(params.encodedUri)} /> 113 </ProtectedRouteView> 114 ); 115 }; 116 117 const PostEngagementRoute = () => { 118 const params = useParams<{ encodedUri: string }>(); 119 120 return ( 121 <ProtectedRouteView> 122 <Dynamic component={props.renderPostEngagement} uri={decodePostRouteUri(params.encodedUri)} /> 123 </ProtectedRouteView> 124 ); 125 }; 126 127 const HashtagRoute = () => { 128 const params = useParams<{ hashtag: string }>(); 129 const tag = decodeHashtagRouteTag(params.hashtag); 130 131 return ( 132 <ProtectedRouteView> 133 <Show when={tag} fallback={<Navigate href="/search" />}> 134 <HashtagPanel /> 135 </Show> 136 </ProtectedRouteView> 137 ); 138 }; 139 140 const MessagesRoute = () => ( 141 <ProtectedRouteView> 142 <Dynamic component={props.renderMessages} memberDid={null} /> 143 </ProtectedRouteView> 144 ); 145 146 const MemberMessagesRoute = () => { 147 const params = useParams<{ memberDid: string }>(); 148 149 return ( 150 <ProtectedRouteView> 151 <Dynamic component={props.renderMessages} memberDid={decodeMessagesRouteMemberDid(params.memberDid)} /> 152 </ProtectedRouteView> 153 ); 154 }; 155 156 const ComposerRoute = () => <ProtectedRouteView>{props.renderComposer()}</ProtectedRouteView>; 157 158 const DeckRoute = () => ( 159 <ProtectedRouteView> 160 <DeckWorkspace /> 161 </ProtectedRouteView> 162 ); 163 164 const ExplorerRoute = () => ( 165 <ProtectedRouteView> 166 <ExplorerPanel /> 167 </ProtectedRouteView> 168 ); 169 170 const SettingsRoute = () => ( 171 <ProtectedRouteView> 172 <SettingsPanel /> 173 </ProtectedRouteView> 174 ); 175 176 const SavedPostsRoute = () => ( 177 <ProtectedRouteView> 178 <SavedPostsPanel /> 179 </ProtectedRouteView> 180 ); 181 182 const NotFoundRoute = () => ( 183 <Show when={session.bootstrapping} fallback={<Navigate href={session.hasSession ? TIMELINE_ROUTE : "/auth"} />}> 184 <RouteLoadingState /> 185 </Show> 186 ); 187 188 return ( 189 <HashRouter root={RouterFrame}> 190 <Route path="/" component={IndexRoute} /> 191 <Route path="/auth" component={AuthRoute} /> 192 <Route path="/timeline" component={TimelineRoute} /> 193 <Route path="/profile" component={ProfileRoute} /> 194 <Route path="/profile/:actor" component={ActorProfileRoute} /> 195 <Route path="/composer" component={ComposerRoute} /> 196 <Route path="/search/preflight" component={SearchPreflightRoute} /> 197 <Route path="/search" component={SearchRoute} /> 198 <Route path="/hashtag/:hashtag" component={HashtagRoute} /> 199 <Route path="/saved" component={SavedPostsRoute} /> 200 <Route path="/notifications" component={NotificationsRoute} /> 201 <Route path="/post/:encodedUri/engagement" component={PostEngagementRoute} /> 202 <Route path="/post/:encodedUri" component={PostRoute} /> 203 <Route path="/messages" component={MessagesRoute} /> 204 <Route path="/messages/:memberDid" component={MemberMessagesRoute} /> 205 <Route path="/deck" component={DeckRoute} /> 206 <Route path="/explorer" component={ExplorerRoute} /> 207 <Route path="/settings" component={SettingsRoute} /> 208 <Route path="*404" component={NotFoundRoute} /> 209 </HashRouter> 210 ); 211} 212 213function SearchRouteGate() { 214 const preferences = useAppPreferences(); 215 const location = useLocation(); 216 const routeState = () => parseSearchRouteState(location.search); 217 const nextRoute = () => `${location.pathname}${location.search}`; 218 const showLoading = () => preferences.embeddingsLoading && !preferences.embeddingsConfig; 219 const shouldRedirect = () => { 220 const config = preferences.embeddingsConfig; 221 if (!config || routeState().tab !== "posts") { 222 return false; 223 } 224 225 return !config.enabled && !config.preflightSeen; 226 }; 227 228 return ( 229 <Show when={!showLoading()} fallback={<RouteLoadingState />}> 230 <Show when={!shouldRedirect()} fallback={<Navigate href={buildSearchPreflightRoute(nextRoute())} />}> 231 <SearchPanel /> 232 </Show> 233 </Show> 234 ); 235} 236 237function PublicOnlyRoute(props: ParentProps & { redirectHref: string }) { 238 const session = useAppSession(); 239 240 return ( 241 <Show when={!session.hasSession || session.bootstrapping} fallback={<Navigate href={props.redirectHref} />}> 242 {props.children} 243 </Show> 244 ); 245} 246 247function ProtectedRouteView(props: ParentProps) { 248 const session = useAppSession(); 249 250 return ( 251 <Show 252 when={session.bootstrapping} 253 fallback={<Show when={session.activeSession} fallback={<Navigate href="/auth" />}>{props.children}</Show>}> 254 <RouteLoadingState /> 255 </Show> 256 ); 257} 258 259function RouteLoadingState() { 260 return ( 261 <div class="grid min-h-168 place-items-center rounded-4xl bg-white/2 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.03)]"> 262 <div class="grid gap-3 text-center"> 263 <p class="overline-copy text-sm text-on-surface-variant">Loading</p> 264 <p class="m-0 text-base text-on-surface">Restoring your workspace.</p> 265 </div> 266 </div> 267 ); 268}