Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

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

feat: improved gallery

Hugo 2152f0c1 c41a3519

+70 -14
+19 -2
app/components/AutomationCard/index.tsx
··· 1 - import { Copy } from "../../icons.ts"; 1 + import { ArrowRight, Copy, Webhook } from "../../icons.ts"; 2 + import { nsidToDomain } from "../../../lib/lexicons/resolver.ts"; 3 + import { isRecordProducingAction, type Action } from "../../../lib/db/schema.ts"; 2 4 import { Button } from "../Button/index.tsx"; 3 - import { NsidCode } from "../NsidCode/index.tsx"; 5 + import { Favicon, NsidCode } from "../NsidCode/index.tsx"; 4 6 import * as s from "./styles.css.ts"; 5 7 6 8 type AutomationCardProps = { ··· 10 12 name: string; 11 13 description?: string | null; 12 14 lexicon: string; 15 + actions: Action[]; 13 16 viewerAuthenticated: boolean; 14 17 isOwner?: boolean; 15 18 featured?: boolean; 16 19 }; 17 20 21 + function TargetIcon({ actions }: { actions: Action[] }) { 22 + const pds = actions.find((a) => isRecordProducingAction(a.$type)); 23 + if (pds?.$type === "bsky-post") return <Favicon domain="bsky.app" class={s.targetFavicon} />; 24 + if (pds?.$type === "bookmark") return <Favicon domain="margin.at" class={s.targetFavicon} />; 25 + if (pds?.$type === "record" || pds?.$type === "patch-record") 26 + return <Favicon domain={nsidToDomain(pds.targetCollection)} class={s.targetFavicon} />; 27 + return <Webhook size={14} />; 28 + } 29 + 18 30 export function AutomationCard({ 19 31 handle, 20 32 did, ··· 22 34 name, 23 35 description, 24 36 lexicon, 37 + actions, 25 38 viewerAuthenticated, 26 39 isOwner, 27 40 featured, ··· 35 48 <article class={featured ? `${s.card} ${s.cardFeatured}` : s.card}> 36 49 <div class={s.lexiconRow}> 37 50 <NsidCode>{lexicon}</NsidCode> 51 + <ArrowRight size={12} class={s.arrow} /> 52 + <span class={s.target}> 53 + <TargetIcon actions={actions} /> 54 + </span> 38 55 </div> 39 56 <h3 class={s.title} title={name}> 40 57 <a href={detailHref} class={s.titleLink}>
+19
app/components/AutomationCard/styles.css.ts
··· 33 33 color: vars.color.textMuted, 34 34 }); 35 35 36 + export const arrow = style({ 37 + color: vars.color.textMuted, 38 + opacity: 0.6, 39 + flexShrink: 0, 40 + }); 41 + 42 + export const target = style({ 43 + display: "inline-flex", 44 + alignItems: "center", 45 + color: vars.color.textSecondary, 46 + }); 47 + 48 + export const targetFavicon = style({ 49 + inlineSize: "16px", 50 + blockSize: "16px", 51 + flexShrink: 0, 52 + borderRadius: "2px", 53 + }); 54 + 36 55 export const title = style({ 37 56 fontSize: fontSize.base, 38 57 fontWeight: fontWeight.semibold,
+15 -3
app/components/NsidCode/index.tsx
··· 9 9 "tangled.sh": "/static/favicons/tangled.sh.cf36edf8.svg", 10 10 "airglow.run": "/static/favicons/airglow.run.cf36edf8.svg", 11 11 "exosphere.site": "/static/favicons/exosphere.site.cf36edf8.svg", 12 + "margin.at": "/static/favicons/margin.at.cf36edf8.svg", 12 13 }; 13 14 14 - export function NsidCode({ children }: { children: string }) { 15 - const domain = nsidToDomain(children); 15 + export function Favicon({ domain, class: className }: { domain: string; class?: string }) { 16 16 const src = STATIC_FAVICONS[domain] ?? `/api/favicon/${domain}`; 17 17 return ( 18 + <img 19 + src={src} 20 + alt="" 21 + class={className ?? s.favicon} 22 + loading="lazy" 23 + onerror="this.style.display='none'" 24 + /> 25 + ); 26 + } 27 + 28 + export function NsidCode({ children }: { children: string }) { 29 + return ( 18 30 <span class={s.wrapper}> 19 - <img src={src} alt="" class={s.favicon} loading="lazy" onerror="this.style.display='none'" /> 31 + <Favicon domain={nsidToDomain(children)} /> 20 32 <InlineCode>{children}</InlineCode> 21 33 </span> 22 34 );
+2
app/icons.ts
··· 34 34 import ChevronRightData from "lucide/icons/chevron-right"; 35 35 import CircleAlertData from "lucide/icons/circle-alert"; 36 36 import ArrowLeftData from "lucide/icons/arrow-left"; 37 + import ArrowRightData from "lucide/icons/arrow-right"; 37 38 import DatabaseData from "lucide/icons/database"; 38 39 import EyeData from "lucide/icons/eye"; 39 40 import FilePlus2Data from "lucide/icons/file-plus-corner"; ··· 59 60 60 61 export const Activity = icon(ActivityData); 61 62 export const ArrowLeft = icon(ArrowLeftData); 63 + export const ArrowRight = icon(ArrowRightData); 62 64 export const Bookmark = icon(BookmarkData); 63 65 export const Copy = icon(CopyData); 64 66 export const ChevronDown = icon(ChevronDownData);
+2
app/routes/automations.tsx
··· 101 101 name={a.name} 102 102 description={a.description} 103 103 lexicon={a.lexicon} 104 + actions={a.actions} 104 105 viewerAuthenticated={Boolean(viewer)} 105 106 isOwner={viewer?.did === a.did} 106 107 featured ··· 122 123 name={a.name} 123 124 description={a.description} 124 125 lexicon={a.lexicon} 126 + actions={a.actions} 125 127 viewerAuthenticated={Boolean(viewer)} 126 128 isOwner={viewer?.did === a.did} 127 129 />
+1
app/routes/index.tsx
··· 171 171 name={a.name} 172 172 description={a.description} 173 173 lexicon={a.lexicon} 174 + actions={a.actions} 174 175 viewerAuthenticated={Boolean(user)} 175 176 isOwner={user?.did === a.did} 176 177 />
+3 -1
lib/automations/featured.ts
··· 1 1 import { eq, inArray } from "drizzle-orm"; 2 2 import { config } from "../config.js"; 3 3 import { db } from "../db/index.js"; 4 - import { automations, users } from "../db/schema.js"; 4 + import { automations, users, type Action } from "../db/schema.js"; 5 5 6 6 export type FeaturedAutomation = { 7 7 uri: string; ··· 11 11 description: string | null; 12 12 lexicon: string; 13 13 handle: string; 14 + actions: Action[]; 14 15 }; 15 16 16 17 export function getFeaturedUris(): string[] { ··· 29 30 name: automations.name, 30 31 description: automations.description, 31 32 lexicon: automations.lexicon, 33 + actions: automations.actions, 32 34 handle: users.handle, 33 35 }) 34 36 .from(automations)
+3 -1
lib/automations/search.ts
··· 1 1 import { and, desc, eq, notInArray, or, sql } from "drizzle-orm"; 2 2 import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core"; 3 - import { automations, users } from "../db/schema.js"; 3 + import { automations, users, type Action } from "../db/schema.js"; 4 4 import type * as schema from "../db/schema.js"; 5 5 6 6 // Both bun:sqlite and better-sqlite3 backends extend BaseSQLiteDatabase. ··· 15 15 description: string | null; 16 16 lexicon: string; 17 17 handle: string; 18 + actions: Action[]; 18 19 }; 19 20 20 21 export type AutomationSearchParams = { ··· 62 63 name: automations.name, 63 64 description: automations.description, 64 65 lexicon: automations.lexicon, 66 + actions: automations.actions, 65 67 handle: users.handle, 66 68 }) 67 69 .from(automations)
+4
public/static/favicons/margin.at.cf36edf8.svg
··· 1 + <svg width="265" height="231" viewBox="0 0 265 231" fill="#027bff" xmlns="http://www.w3.org/2000/svg"> 2 + <path d="M0 230 V0 H199 V65.7156 H149.5 V115.216 H182.5 L199 131.716 V230 Z"/> 3 + <path d="M215 214.224 V230 H264.5 V0 H215 V16.2242 H248.5 V214.224 H215 Z"/> 4 + </svg>
+2 -7
public/static/favicons/tangled.sh.cf36edf8.svg
··· 19 19 xmlns:cc="http://creativecommons.org/ns#"> 20 20 <style> 21 21 .dolly { 22 - color: #000000; 23 - } 24 - 25 - @media (prefers-color-scheme: dark) { 26 - .dolly { 27 - color: #ffffff; 28 - } 22 + color: #ffffff; 29 23 } 30 24 </style> 25 + <rect x="0" y="0" width="25" height="25" rx="2" ry="2" fill="#000000" /> 31 26 <sodipodi:namedview 32 27 id="namedview1" 33 28 pagecolor="#ffffff"