Atproto primitives for Gleam.
atproto
48

Configure Feed

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

✨ Create small demo app to visualise tid generation.

Hayleigh Thompson (Jul 17, 2026, 1:14 AM +0200) af4bfa87 1f3a4242

+338
+4
.gitignore
··· 1 1 *.beam 2 2 *.ez 3 3 /build 4 + 5 + /demo/*/.lustre 6 + /demo/*/build 7 + 4 8 erl_crash.dump
+12
demo/tid/gleam.toml
··· 1 + name = "tid" 2 + version = "1.0.0" 3 + target = "javascript" 4 + 5 + [dependencies] 6 + at = { path = "../../" } 7 + gleam_stdlib = ">= 1.0.0 and < 2.0.0" 8 + gleam_time = ">= 1.8.0 and < 2.0.0" 9 + lustre = ">= 5.7.1 and < 6.0.0" 10 + 11 + [dev_dependencies] 12 + lustre_dev_tools = ">= 2.3.6 and < 3.0.0"
+53
demo/tid/manifest.toml
··· 1 + # Do not manually edit this file, it is managed by Gleam. 2 + # 3 + # This file locks the dependency versions used, to make your build 4 + # deterministic and to prevent unexpected versions from being included 5 + # in your application. 6 + # 7 + # You should check this file into your source control repository. 8 + 9 + packages = [ 10 + { name = "argv", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "3277D100448BDB4A29B6D58C0F36F631CBC349E8BDD09766C6309DF202831140" }, 11 + { name = "at", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "gleam_time"], source = "local", path = "../.." }, 12 + { name = "booklet", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "booklet", source = "hex", outer_checksum = "08E0FDB78DC4D8A5D3C80295B021505C7D2A2E7B6C6D5EAB7286C36F4A53C851" }, 13 + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, 14 + { name = "envoy", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "9C6FBB6BFA02A52798BEEC5977A738CAD6E4A057F4B67FD0C8061AD2502C191A" }, 15 + { name = "exception", version = "2.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "6BDEA95248093599391C3B5DF1835C5C6A86C353C2F99CE539B450E3432FE117" }, 16 + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 17 + { name = "gleam_community_ansi", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "B5AA433AF84313E23FDF90CCFF752B9380FE9FFCE02B2949D49B7AACCC77B16D" }, 18 + { name = "gleam_community_colour", version = "2.0.4", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "6DB4665555D7D2B27F0EA32EF47E8BEBC4303821765F9C73D483F38EE24894F0" }, 19 + { name = "gleam_crypto", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "2DE9E4EF53CF6FEE049D4F765731F7178F7A11AEFAE00EEE63BF7536B354AD3F" }, 20 + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 21 + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 22 + { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, 23 + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 24 + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 25 + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, 26 + { name = "gleam_stdlib", version = "1.0.3", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1F543AFBA5D33DA493E6087F4E4C4F20D899411343512686C98A8ABB2963CF22" }, 27 + { name = "gleam_time", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "533D8723774D61AD4998324F5DD1DABDCDBFABAFB9E87CB5D03C6955448FC97D" }, 28 + { name = "glint", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "26CCA9BC3ACB56CD9D754ACF598F49B4C281C4FDD81AE61FA21C8E92E9C8218E" }, 29 + { name = "glisten", version = "9.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging"], otp_app = "glisten", source = "hex", outer_checksum = "7795AA50830656F3A0316A6B26595F893C83272DA901B3405E31339CAA31A10B" }, 30 + { name = "gramps", version = "6.0.1", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "D55636072DEE173F6586A5679D3C02EC7A0DE3F8646B78C351B72908FF223DF7" }, 31 + { name = "group_registry", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "group_registry", source = "hex", outer_checksum = "BC798A53D6F2406DB94E27CB45C57052CB56B32ACF7CC16EA20F6BAEC7E36B90" }, 32 + { name = "houdini", version = "1.2.1", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "6F8AC2F12974567FB744BEA66AC93CEB76AAEA19AD28564623F76CDA9BC26A85" }, 33 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 34 + { name = "justin", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "8B1C62269E8607D0A0ED698B7903984CE0BF7B6B3C0DEA3C6C8302B39402837B" }, 35 + { name = "logging", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "BC5F18CE5DD9686100229FE5409BDC3DD5C46D5A7DF2F804AD2D8F0DD6C5060E" }, 36 + { name = "lustre", version = "5.7.1", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "663A2D1A3458914CA537A42AC07E601DD3FD85415EBDAF4717F2DFF6E86F90F9" }, 37 + { name = "lustre_dev_tools", version = "2.3.6", build_tools = ["gleam"], requirements = ["argv", "booklet", "filepath", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_regexp", "gleam_stdlib", "glint", "group_registry", "justin", "lustre", "mist", "polly", "simplifile", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "7A650A107C7E767D902A8CB608460B927DE0CDD47D5BA04DED71F9F622FE6AD9" }, 38 + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 39 + { name = "mist", version = "6.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "1B07F321D5FA0CB162D81496F2DE96AEB6EF8980F4F38230A4CC3F849497E020" }, 40 + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 41 + { name = "polly", version = "3.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_erlang", "gleam_otp", "gleam_stdlib", "simplifile"], otp_app = "polly", source = "hex", outer_checksum = "51FB565D81FF6212FDF3306D44419601F2A7C4EDD1F00FC9DA5C376A00AED4FE" }, 42 + { name = "simplifile", version = "2.5.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "6C72DCCDF25C38A5931740B30E823969F33106831FD1637719B5EDBCA30027A4" }, 43 + { name = "snag", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "274F41D6C3ECF99F7686FDCE54183333E41D2C1CA5A3A673F9A8B2C7A4401077" }, 44 + { name = "tom", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "DCF04CB7AB35D58CFC598C66EA2E1816D160759802C89B2BA6238780D59BC256" }, 45 + { name = "wisp", version = "2.2.2", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "5FF5F1E288C3437252ABB93D8F9CF42FF652CE7AD54480CFE736038DC09C4F22" }, 46 + ] 47 + 48 + [requirements] 49 + at = { path = "../../" } 50 + gleam_stdlib = { version = ">= 1.0.0 and < 2.0.0" } 51 + gleam_time = { version = ">= 1.8.0 and < 2.0.0" } 52 + lustre = { version = ">= 5.7.1 and < 6.0.0" } 53 + lustre_dev_tools = { version = ">= 2.3.6 and < 3.0.0" }
+6
demo/tid/src/tid.css
··· 1 + @import url(https://fonts.bunny.net/css?family=doto:800); 2 + @import "tailwindcss"; 3 + 4 + @theme { 5 + --font-mono: "Doto", monospace; 6 + }
+1
demo/tid/src/tid.ffi.mjs
··· 1 + export const timezone = () => new Date().getTimezoneOffset() * -1;
+262
demo/tid/src/tid.gleam
··· 1 + // IMPORTS --------------------------------------------------------------------- 2 + 3 + import at/tid.{type Tid} 4 + import gleam/bool 5 + import gleam/int 6 + import gleam/option.{type Option, None, Some} 7 + import gleam/string 8 + import gleam/time/duration 9 + import gleam/time/timestamp 10 + import lustre 11 + import lustre/attribute.{type Attribute} 12 + import lustre/effect.{type Effect} 13 + import lustre/element.{type Element} 14 + import lustre/element/html 15 + import lustre/event 16 + 17 + // MAIN ------------------------------------------------------------------------ 18 + 19 + pub fn main() { 20 + let clock = int.random(1024) 21 + let tid = tid.now(clock) 22 + let offset = timezone() 23 + let app = lustre.application(init:, update:, view:) 24 + let assert Ok(_) = lustre.start(app, "#app", Arguments(tid:, clock:, offset:)) 25 + 26 + Nil 27 + } 28 + 29 + @external(javascript, "./tid.ffi.mjs", "timezone") 30 + fn timezone() -> Int 31 + 32 + // MODEL ----------------------------------------------------------------------- 33 + 34 + type Model { 35 + Model( 36 + tid: Tid, 37 + clock: Int, 38 + offset: Int, 39 + ticks_until_clock: Int, 40 + // 41 + hovered: Option(TidPart), 42 + ) 43 + } 44 + 45 + type TidPart { 46 + Timestamp 47 + Clock 48 + } 49 + 50 + const ticks_until_clock = 60 51 + 52 + type Arguments { 53 + Arguments(tid: Tid, clock: Int, offset: Int) 54 + } 55 + 56 + fn init(arguments: Arguments) -> #(Model, Effect(Message)) { 57 + let model = 58 + Model( 59 + tid: arguments.tid, 60 + clock: arguments.clock, 61 + offset: arguments.offset, 62 + ticks_until_clock:, 63 + hovered: None, 64 + ) 65 + 66 + let effect = generate_next_tid(arguments.tid) 67 + 68 + #(model, effect) 69 + } 70 + 71 + // UPDATE ---------------------------------------------------------------------- 72 + 73 + type Message { 74 + ClockRefreshedTid(tid: Tid) 75 + ClockSteppedTid(tid: Tid) 76 + UserHoveredTidPart(part: Option(TidPart)) 77 + } 78 + 79 + fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { 80 + case message { 81 + ClockRefreshedTid(tid:) -> { 82 + let model = 83 + Model(..model, tid:, clock: tid.clock(tid), ticks_until_clock:) 84 + 85 + let effect = generate_next_tid(tid) 86 + 87 + #(model, effect) 88 + } 89 + 90 + ClockSteppedTid(tid:) if model.ticks_until_clock == 0 -> { 91 + let model = Model(..model, tid:, ticks_until_clock:) 92 + let effect = generate_new_tid() 93 + 94 + #(model, effect) 95 + } 96 + 97 + ClockSteppedTid(tid:) -> { 98 + let ticks_until_clock = model.ticks_until_clock - 1 99 + let model = Model(..model, tid:, ticks_until_clock:) 100 + let effect = generate_next_tid(tid) 101 + 102 + #(model, effect) 103 + } 104 + 105 + UserHoveredTidPart(part:) -> { 106 + let model = 107 + Model(..model, hovered: case part == model.hovered { 108 + True -> None 109 + False -> part 110 + }) 111 + let effect = effect.none() 112 + 113 + #(model, effect) 114 + } 115 + } 116 + } 117 + 118 + fn generate_new_tid() -> Effect(Message) { 119 + use dispatch, _ <- effect.after_paint 120 + let clock = int.random(1024) 121 + let tid = tid.now(clock) 122 + 123 + dispatch(ClockRefreshedTid(tid:)) 124 + } 125 + 126 + fn generate_next_tid(now: Tid) -> Effect(Message) { 127 + use dispatch, _ <- effect.after_paint 128 + let tid = tid.next(now) 129 + 130 + dispatch(ClockSteppedTid(tid:)) 131 + } 132 + 133 + // VIEW ------------------------------------------------------------------------ 134 + 135 + fn view(model: Model) -> Element(Message) { 136 + let part = case model.hovered { 137 + Some(Timestamp) -> "timestamp" 138 + Some(Clock) -> "clock" 139 + None -> "none" 140 + } 141 + 142 + html.div([attribute.class("max-w-2xl mx-auto py-32")], [ 143 + html.h1([attribute.class("font-serif text-4xl")], [ 144 + html.text("Timestamp Ids"), 145 + ]), 146 + 147 + html.section( 148 + [attribute.class("group cursor-default"), attribute.data("part", part)], 149 + [ 150 + view_tid(model.tid), 151 + view_tid_details(model.tid, model.offset), 152 + ], 153 + ), 154 + ]) 155 + } 156 + 157 + fn view_tid(tid: Tid) -> Element(Message) { 158 + let base32 = tid.to_string(tid) 159 + 160 + html.p( 161 + [ 162 + attribute.class("tracking-widest"), 163 + attribute.class( 164 + "border border-slate-200 bg-slate-50 font-mono p-4 text-5xl", 165 + ), 166 + ], 167 + [ 168 + view_tid_part(Timestamp, html.span, [], [ 169 + html.text(string.slice(base32, 0, 11)), 170 + ]), 171 + view_tid_part(Clock, html.span, [], [ 172 + html.text(string.slice(base32, 11, 2)), 173 + ]), 174 + ], 175 + ) 176 + } 177 + 178 + fn view_tid_details(tid: Tid, offset: Int) -> Element(Message) { 179 + let rfc3339 = 180 + timestamp.to_rfc3339(tid.to_timestamp(tid), duration.minutes(offset)) 181 + 182 + html.dl( 183 + [ 184 + attribute.class("grid grid-cols-[auto_1fr] gap-x-8 gap-y-2 text-sm"), 185 + ], 186 + [ 187 + view_detail_label("datetime"), 188 + view_tid_datetime(rfc3339, offset), 189 + view_detail_label("microseconds"), 190 + view_tid_part(Timestamp, html.dd, [attribute.class("tabular-nums")], [ 191 + html.text(int.to_string(tid.microseconds(tid))), 192 + ]), 193 + view_detail_label("clock"), 194 + view_tid_part(Clock, html.dd, [attribute.class("tabular-nums")], [ 195 + html.text(int.to_string(tid.clock(tid))), 196 + ]), 197 + ], 198 + ) 199 + } 200 + 201 + fn view_detail_label(text: String) -> Element(Message) { 202 + html.dt([attribute.class("")], [html.text(text)]) 203 + } 204 + 205 + fn view_tid_datetime(rfc3339: String, offset: Int) -> Element(Message) { 206 + html.dd([attribute.class("tabular-nums")], [ 207 + html.text(string.slice(rfc3339, 0, 10)), 208 + html.text(" "), 209 + html.text(string.slice(rfc3339, 11, 8)), 210 + html.text(" "), 211 + view_offset(offset), 212 + ]) 213 + } 214 + 215 + fn view_tid_part( 216 + part: TidPart, 217 + element: fn(List(Attribute(Message)), List(Element(Message))) -> 218 + Element(Message), 219 + attributes: List(Attribute(Message)), 220 + children: List(Element(Message)), 221 + ) -> Element(Message) { 222 + case part { 223 + Timestamp -> 224 + element( 225 + [ 226 + attribute.class("group-data-[part=timestamp]:text-blue-700"), 227 + event.on_mouse_enter(UserHoveredTidPart(Some(Timestamp))), 228 + event.on_mouse_leave(UserHoveredTidPart(None)), 229 + ..attributes 230 + ], 231 + children, 232 + ) 233 + 234 + Clock -> 235 + element( 236 + [ 237 + attribute.class("group-data-[part=clock]:text-blue-700"), 238 + event.on_mouse_enter(UserHoveredTidPart(Some(Clock))), 239 + event.on_mouse_leave(UserHoveredTidPart(None)), 240 + ..attributes 241 + ], 242 + children, 243 + ) 244 + } 245 + } 246 + 247 + fn view_offset(offset: Int) -> Element(message) { 248 + use <- bool.guard(offset == 0, html.text("")) 249 + let hours = 250 + { int.absolute_value(offset) / 60 } 251 + |> int.to_string 252 + |> string.pad_start(2, "0") 253 + let minutes = 254 + { int.absolute_value(offset) % 60 } 255 + |> int.to_string 256 + |> string.pad_start(2, "0") 257 + 258 + case offset < 0 { 259 + True -> html.text("-" <> hours <> ":" <> minutes) 260 + False -> html.text("+" <> hours <> ":" <> minutes) 261 + } 262 + }