(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
99
fork

Configure Feed

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

atmosphereconf 2026 event for margin

+179 -52
+2 -2
web/astro.config.mjs
··· 12 12 adapter: node({ mode: "standalone" }), 13 13 integrations: [react(), tailwind()], 14 14 prefetch: { 15 - prefetchAll: false, 16 - defaultStrategy: "hover", 15 + prefetchAll: true, 16 + defaultStrategy: "viewport", 17 17 }, 18 18 security: { 19 19 checkOrigin: true,
+38 -7
web/src/components/feed/FeedItems.tsx
··· 51 51 setOffset(cached.offset); 52 52 setLoading(false); 53 53 54 - getFeed({ type, motivation, tag, creator, source, limit: LIMIT, offset: 0 }) 54 + getFeed({ 55 + type, 56 + motivation, 57 + tag, 58 + creator, 59 + source, 60 + limit: LIMIT, 61 + offset: 0, 62 + }) 55 63 .then((data) => { 56 64 if (cancelled) return; 57 65 const fetched = data.items; 58 66 setItems(fetched); 59 67 setHasMore(data.hasMore); 60 68 setOffset(data.fetchedCount); 61 - feedCache.set(cacheKey, { items: fetched, hasMore: data.hasMore, offset: data.fetchedCount, timestamp: Date.now() }); 69 + feedCache.set(cacheKey, { 70 + items: fetched, 71 + hasMore: data.hasMore, 72 + offset: data.fetchedCount, 73 + timestamp: Date.now(), 74 + }); 62 75 }) 63 76 .catch(console.error); 64 - 65 - return () => { cancelled = true; }; 77 + 78 + return () => { 79 + cancelled = true; 80 + }; 66 81 } 67 82 68 83 setLoading(true); ··· 74 89 setHasMore(data.hasMore); 75 90 setOffset(data.fetchedCount); 76 91 setLoading(false); 77 - feedCache.set(cacheKey, { items: fetched, hasMore: data.hasMore, offset: data.fetchedCount, timestamp: Date.now() }); 92 + feedCache.set(cacheKey, { 93 + items: fetched, 94 + hasMore: data.hasMore, 95 + offset: data.fetchedCount, 96 + timestamp: Date.now(), 97 + }); 78 98 }) 79 99 .catch((e) => { 80 100 if (cancelled) return; ··· 92 112 const loadMore = useCallback(async () => { 93 113 setLoadingMore(true); 94 114 try { 95 - const cacheKey = JSON.stringify({ type, motivation, tag, creator, source }); 115 + const cacheKey = JSON.stringify({ 116 + type, 117 + motivation, 118 + tag, 119 + creator, 120 + source, 121 + }); 96 122 const data = await getFeed({ 97 123 type, 98 124 motivation, ··· 108 134 setHasMore(data.hasMore); 109 135 const newOffset = offset + data.fetchedCount; 110 136 setOffset(newOffset); 111 - feedCache.set(cacheKey, { items: newItems, hasMore: data.hasMore, offset: newOffset, timestamp: Date.now() }); 137 + feedCache.set(cacheKey, { 138 + items: newItems, 139 + hasMore: data.hasMore, 140 + offset: newOffset, 141 + timestamp: Date.now(), 142 + }); 112 143 } catch (e) { 113 144 console.error(e); 114 145 } finally {
+50 -8
web/src/layouts/AppLayout.astro
··· 1 1 --- 2 - import BaseLayout from './BaseLayout.astro'; 3 - import Sidebar from '../components/navigation/Sidebar'; 4 - import RightSidebar from '../components/navigation/RightSidebar'; 5 - import MobileNav from '../components/navigation/MobileNav'; 6 - import type { UserProfile } from '../types'; 2 + import BaseLayout from "./BaseLayout.astro"; 3 + import Sidebar from "../components/navigation/Sidebar"; 4 + import RightSidebar from "../components/navigation/RightSidebar"; 5 + import MobileNav from "../components/navigation/MobileNav"; 6 + import { BlueskyIcon } from "../components/common/Icons"; 7 + import type { UserProfile } from "../types"; 7 8 8 9 interface Props { 9 10 title?: string; ··· 16 17 --- 17 18 18 19 <BaseLayout title={title} description={description} image={image}> 20 + <div 21 + class="bg-blue-600 dark:bg-blue-500 text-white font-medium text-sm flex items-center justify-center gap-x-3 gap-y-1 flex-wrap py-2 px-4 w-full z-50" 22 + > 23 + <div 24 + class="w-12 h-9 overflow-hidden rounded flex items-start justify-center -my-1" 25 + > 26 + <img 27 + src="https://atmosphereconf.org/_image?href=%2F_astro%2Fgoodstuff-goose.DKPXDrcQ.png&w=792&h=990&f=webp" 28 + alt="Atmosphere Goose" 29 + class="w-12 h-12 object-cover object-top drop-shadow-md" 30 + /> 31 + </div> 32 + <span 33 + >Welcome to <a 34 + href="https://atmosphereconf.org/" 35 + target="_blank" 36 + rel="noopener noreferrer" 37 + class="font-bold underline hover:text-blue-200 transition-colors" 38 + >ATmosphereConf 2026</a 39 + >!</span 40 + > 41 + <a 42 + href="https://bsky.app/profile/atmosphereconf.org/feed/atmosphereconf" 43 + target="_blank" 44 + rel="noopener noreferrer" 45 + class="hover:text-blue-200 transition-colors flex items-center gap-1.5 ml-1 bg-blue-700/50 hover:bg-blue-700 px-2 py-0.5 rounded-full" 46 + > 47 + <BlueskyIcon size={14} color="currentColor" /> 48 + <span>View feed</span> 49 + </a> 50 + </div> 19 51 <div class="min-h-screen bg-surface-100 dark:bg-surface-900 flex"> 20 52 <div transition:persist="sidebar"> 21 - <Sidebar client:load initialUser={user} currentPath={Astro.url.pathname} /> 53 + <Sidebar 54 + client:idle 55 + initialUser={user} 56 + currentPath={Astro.url.pathname} 57 + /> 22 58 </div> 23 59 24 60 <div class="flex-1 min-w-0 transition-all duration-200"> 25 61 <div class="flex w-full max-w-[1800px] mx-auto"> 26 62 <main class="flex-1 w-full min-w-0 py-2 md:py-3"> 27 - <div class="bg-white dark:bg-surface-800 rounded-2xl min-h-[calc(100vh-16px)] md:min-h-[calc(100vh-24px)] py-6 px-4 md:px-6 lg:px-8 pb-20 md:pb-6"> 63 + <div 64 + class="bg-white dark:bg-surface-800 rounded-2xl min-h-[calc(100vh-16px)] md:min-h-[calc(100vh-24px)] py-6 px-4 md:px-6 lg:px-8 pb-20 md:pb-6" 65 + > 28 66 <slot /> 29 67 </div> 30 68 </main> ··· 36 74 </div> 37 75 38 76 <div transition:persist="mobile-nav"> 39 - <MobileNav client:load initialUser={user} currentPath={Astro.url.pathname} /> 77 + <MobileNav 78 + client:media="(max-width: 768px)" 79 + initialUser={user} 80 + currentPath={Astro.url.pathname} 81 + /> 40 82 </div> 41 83 </div> 42 84 </BaseLayout>
+12 -7
web/src/views/collections/Collections.tsx
··· 34 34 const [creating, setCreating] = useState(false); 35 35 36 36 const fetchCollections = async () => { 37 - if (collectionsCache.data && Date.now() - collectionsCache.timestamp < 5 * 60 * 1000) { 37 + if ( 38 + collectionsCache.data && 39 + Date.now() - collectionsCache.timestamp < 5 * 60 * 1000 40 + ) { 38 41 setCollections(collectionsCache.data); 39 42 setLoading(false); 40 - 41 - getCollections().then(data => { 42 - setCollections(data); 43 - collectionsCache.data = data; 44 - collectionsCache.timestamp = Date.now(); 45 - }).catch(console.error); 43 + 44 + getCollections() 45 + .then((data) => { 46 + setCollections(data); 47 + collectionsCache.data = data; 48 + collectionsCache.timestamp = Date.now(); 49 + }) 50 + .catch(console.error); 46 51 return; 47 52 } 48 53
+8 -2
web/src/views/core/Feed.tsx
··· 65 65 const tabs = [ 66 66 { id: "all", label: "Recent" }, 67 67 { id: "popular", label: "Popular" }, 68 + { id: "atmosphereconf", label: "ATmosphereConf" }, 68 69 { id: "shelved", label: "Shelved" }, 69 70 { id: "margin", label: "Margin" }, 70 71 { id: "semble", label: "Semble" }, ··· 157 158 158 159 <FeedItems 159 160 key={`${activeTab}-${activeFilter || "all"}-${tag || ""}`} 160 - type={activeTab} 161 + type={activeTab === "atmosphereconf" ? "all" : activeTab} 161 162 motivation={activeFilter} 162 163 emptyMessage={emptyMessage} 163 164 layout={layout} 164 - tag={tag} 165 + tag={ 166 + activeTab === "atmosphereconf" || 167 + tag?.toLowerCase() === "atmosphereconf" 168 + ? "ATmosphereConf" 169 + : tag 170 + } 165 171 /> 166 172 </div> 167 173 );
+13 -8
web/src/views/core/Notifications.tsx
··· 228 228 229 229 useEffect(() => { 230 230 const load = async () => { 231 - if (notificationsCache.data && Date.now() - notificationsCache.timestamp < 5 * 60 * 1000) { 231 + if ( 232 + notificationsCache.data && 233 + Date.now() - notificationsCache.timestamp < 5 * 60 * 1000 234 + ) { 232 235 setNotifications(notificationsCache.data); 233 236 setLoading(false); 234 - 235 - getNotifications().then(data => { 236 - setNotifications(data); 237 - notificationsCache.data = data; 238 - notificationsCache.timestamp = Date.now(); 239 - }).catch(console.error); 240 - 237 + 238 + getNotifications() 239 + .then((data) => { 240 + setNotifications(data); 241 + notificationsCache.data = data; 242 + notificationsCache.timestamp = Date.now(); 243 + }) 244 + .catch(console.error); 245 + 241 246 markNotificationsRead(); 242 247 return; 243 248 }
+34 -14
web/src/views/core/Search.tsx
··· 66 66 setResults([]); 67 67 return; 68 68 } 69 - 70 - const cacheKey = JSON.stringify({ q: q.trim(), myItemsOnly: myItemsRef.current }); 71 - 69 + 70 + const cacheKey = JSON.stringify({ 71 + q: q.trim(), 72 + myItemsOnly: myItemsRef.current, 73 + }); 74 + 72 75 if (!append && newOffset === 0) { 73 76 const cached = searchCache.get(cacheKey); 74 77 if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) { ··· 76 79 setHasMore(cached.hasMore); 77 80 setOffset(cached.offset); 78 81 setLoading(false); 79 - 82 + 80 83 const id = ++fetchIdRef.current; 81 84 searchItems(q.trim(), { 82 85 creator: myItemsRef.current && user ? user.did : undefined, 83 86 limit: 30, 84 87 offset: newOffset, 85 - }).then(data => { 86 - if (id !== fetchIdRef.current) return; 87 - setResults(data.items); 88 - setHasMore(data.hasMore); 89 - setOffset(newOffset + data.items.length); 90 - searchCache.set(cacheKey, { results: data.items, hasMore: data.hasMore, offset: newOffset + data.items.length, timestamp: Date.now() }); 91 - }).catch(console.error); 92 - 88 + }) 89 + .then((data) => { 90 + if (id !== fetchIdRef.current) return; 91 + setResults(data.items); 92 + setHasMore(data.hasMore); 93 + setOffset(newOffset + data.items.length); 94 + searchCache.set(cacheKey, { 95 + results: data.items, 96 + hasMore: data.hasMore, 97 + offset: newOffset + data.items.length, 98 + timestamp: Date.now(), 99 + }); 100 + }) 101 + .catch(console.error); 102 + 93 103 return; 94 104 } 95 105 } ··· 105 115 if (append) { 106 116 setResults((prev) => { 107 117 const newResults = [...prev, ...data.items]; 108 - searchCache.set(cacheKey, { results: newResults, hasMore: data.hasMore, offset: newOffset + data.items.length, timestamp: Date.now() }); 118 + searchCache.set(cacheKey, { 119 + results: newResults, 120 + hasMore: data.hasMore, 121 + offset: newOffset + data.items.length, 122 + timestamp: Date.now(), 123 + }); 109 124 return newResults; 110 125 }); 111 126 } else { 112 127 setResults(data.items); 113 - searchCache.set(cacheKey, { results: data.items, hasMore: data.hasMore, offset: newOffset + data.items.length, timestamp: Date.now() }); 128 + searchCache.set(cacheKey, { 129 + results: data.items, 130 + hasMore: data.hasMore, 131 + offset: newOffset + data.items.length, 132 + timestamp: Date.now(), 133 + }); 114 134 } 115 135 setHasMore(data.hasMore); 116 136 setOffset(newOffset + data.items.length);
+22 -4
web/src/views/profile/Profile.tsx
··· 186 186 try { 187 187 const rel = await getModerationRelationship(did); 188 188 setModRelation(rel); 189 - profileCache.set(did, { profile: merged, labels: marginData?.labels || [], relation: rel, timestamp: Date.now() }); 189 + profileCache.set(did, { 190 + profile: merged, 191 + labels: marginData?.labels || [], 192 + relation: rel, 193 + timestamp: Date.now(), 194 + }); 190 195 } catch { 191 - profileCache.set(did, { profile: merged, labels: marginData?.labels || [], relation: modRelation, timestamp: Date.now() }); 196 + profileCache.set(did, { 197 + profile: merged, 198 + labels: marginData?.labels || [], 199 + relation: modRelation, 200 + timestamp: Date.now(), 201 + }); 192 202 } 193 203 } else { 194 - profileCache.set(did, { profile: merged, labels: marginData?.labels || [], relation: modRelation, timestamp: Date.now() }); 204 + profileCache.set(did, { 205 + profile: merged, 206 + labels: marginData?.labels || [], 207 + relation: modRelation, 208 + timestamp: Date.now(), 209 + }); 195 210 } 196 211 } catch (e) { 197 212 console.error("Profile load failed", e); ··· 233 248 } 234 249 const res = await getCollections(resolvedDid); 235 250 setCollections(res); 236 - profileCollectionsCache.set(resolvedDid, { collections: res, timestamp: Date.now() }); 251 + profileCollectionsCache.set(resolvedDid, { 252 + collections: res, 253 + timestamp: Date.now(), 254 + }); 237 255 } 238 256 } catch (e) { 239 257 console.error(e);