Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

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);