slack status without the slack status.zzstoatzz.io/
quickslice

improve link previews and feed UX

- use lowercase in open graph metadata
- include actual status in link previews
- make feed handles visually distinct with secondary color
- remove unused HomeTemplate and home.html
- clean up dead code and old references

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>

+10 -456
+1 -3
src/ingester.rs
··· 74 74 pub async fn start_ingester(db_pool: Arc<Pool>) { 75 75 // init the builder 76 76 let opts = JetstreamOptions::builder() 77 - // your EXACT nsids 78 - // Which in this case is xyz.statusphere.status 77 + // listen for our status record collection 79 78 .wanted_collections(vec!["io.zzstoatzz.status.record".parse().unwrap()]) 80 79 .build(); 81 80 // create the jetstream connector 82 81 let jetstream = JetstreamConnection::new(opts); 83 82 84 83 // create your ingesters 85 - // Which in this case is xyz.statusphere.status 86 84 let mut ingesters: HashMap<String, Box<dyn LexiconIngestor + Send + Sync>> = HashMap::new(); 87 85 ingesters.insert( 88 86 // your EXACT nsid
-2
src/main.rs
··· 41 41 }; 42 42 use templates::{ErrorTemplate, Profile}; 43 43 44 - extern crate dotenv; 45 - 46 44 mod db; 47 45 mod ingester; 48 46 mod lexicons;
-11
src/templates.rs
··· 4 4 use askama::Template; 5 5 use serde::{Deserialize, Serialize}; 6 6 7 - #[derive(Template)] 8 - #[template(path = "home.html")] 9 - pub struct HomeTemplate<'a> { 10 - #[allow(dead_code)] 11 - pub title: &'a str, 12 - pub status_options: &'a [&'a str], 13 - pub profile: Option<Profile>, 14 - pub statuses: Vec<StatusFromDb>, 15 - pub my_status: Option<String>, 16 - } 17 - 18 7 #[derive(Serialize, Deserialize, Debug, Clone)] 19 8 pub struct Profile { 20 9 pub did: String,
+4 -4
templates/base.html
··· 8 8 <!-- Open Graph / Facebook --> 9 9 <meta property="og:type" content="website"> 10 10 <meta property="og:url" content="https://status.zzstoatzz.io{% block og_url %}{% endblock %}"> 11 - <meta property="og:title" content="{% block og_title %}Status Updates{% endblock %}"> 12 - <meta property="og:description" content="{% block og_description %}Real-time status updates from the BlueSky network{% endblock %}"> 11 + <meta property="og:title" content="{% block og_title %}status{% endblock %}"> 12 + <meta property="og:description" content="{% block og_description %}like slack status, but decoupled from any platform{% endblock %}"> 13 13 <meta property="og:image" content="{% block og_image %}https://status.zzstoatzz.io/og-image.png{% endblock %}"> 14 14 15 15 <!-- Twitter --> 16 16 <meta property="twitter:card" content="summary"> 17 17 <meta property="twitter:url" content="https://status.zzstoatzz.io{% block twitter_url %}{% endblock %}"> 18 - <meta property="twitter:title" content="{% block twitter_title %}Status Updates{% endblock %}"> 19 - <meta property="twitter:description" content="{% block twitter_description %}Real-time status updates from the BlueSky network{% endblock %}"> 18 + <meta property="twitter:title" content="{% block twitter_title %}status{% endblock %}"> 19 + <meta property="twitter:description" content="{% block twitter_description %}like slack status, but decoupled from any platform{% endblock %}"> 20 20 <meta property="twitter:image" content="{% block twitter_image %}https://status.zzstoatzz.io/og-image.png{% endblock %}"> 21 21 22 22 <link rel="stylesheet" href="/css/style.css">
+1 -1
templates/feed.html
··· 312 312 } 313 313 314 314 .status-author { 315 - color: var(--text-primary); 315 + color: var(--text-secondary); 316 316 font-weight: 600; 317 317 text-decoration: none; 318 318 transition: color 0.2s;
-431
templates/home.html
··· 1 - {% extends "base.html" %} 2 - 3 - {% block content %} 4 - <div id="root"> 5 - <div class="error"></div> 6 - <div id="header"> 7 - {% if let Some(Profile {did, display_name}) = profile %} 8 - {% if let Some(name) = display_name %} 9 - <h1>{{name}}'s status</h1> 10 - {% else %} 11 - <h1>your status</h1> 12 - {% endif %} 13 - <p>what are you up to right now?</p> 14 - {% else %} 15 - <h1>status tracker</h1> 16 - <p>share what you're up to</p> 17 - {% endif %} 18 - </div> 19 - <div class="container"> 20 - <div class="card"> 21 - {% if let Some(Profile {did, display_name}) = profile %} 22 - <form action="/logout" method="get" class="session-form"> 23 - <div> 24 - Hi, 25 - {% if let Some(display_name) = display_name %} 26 - <strong>{{display_name}}</strong> 27 - {% else %} 28 - <strong>friend</strong> 29 - {% endif %}. What's 30 - your status today?? 31 - </div> 32 - <div> 33 - <button type="submit">Log out</button> 34 - </div> 35 - </form> 36 - {% else %} 37 - <div class="session-form"> 38 - <div><a href="/login">Log in</a> to set your status!</div> 39 - <div> 40 - <a href="/login" class="button">Log in</a> 41 - </div> 42 - </div> 43 - {% endif %} 44 - 45 - 46 - </div> 47 - {% if let Some(Profile {did, display_name}) = profile %} 48 - <!-- Walkthrough Elements --> 49 - <div class="walkthrough-overlay"></div> 50 - <div class="walkthrough-spotlight"></div> 51 - <div class="walkthrough-tooltip"> 52 - <div class="walkthrough-progress"></div> 53 - <h3 id="walkthrough-title"></h3> 54 - <p id="walkthrough-text"></p> 55 - <div class="walkthrough-actions"> 56 - <button class="walkthrough-skip">Skip</button> 57 - <button class="walkthrough-next">Next</button> 58 - </div> 59 - </div> 60 - 61 - <div class="status-form-container"> 62 - <h3>set your status</h3> 63 - <div class="current-status-info"> 64 - {% if let Some(my_status) = my_status %} 65 - <p>current: <span class="current-emoji">{{my_status}}</span> 66 - {% for status in statuses %} 67 - {% if status.author_did == did.to_string() && loop.first %} 68 - {% if status.text.is_some() %} 69 - - {{ status.text.as_ref().unwrap() }} 70 - {% endif %} 71 - {% if status.expires_at.is_some() && !status.is_expired() %} 72 - (<span class="local-time" data-timestamp="{{ status.expires_at.as_ref().unwrap().to_rfc3339() }}" data-prefix="expires"></span>) 73 - {% endif %} 74 - {% endif %} 75 - {% endfor %} 76 - </p> 77 - {% else %} 78 - <p>no status set</p> 79 - {% endif %} 80 - </div> 81 - 82 - <form action="/status" method="post" class="status-form" id="status-form"> 83 - <div class="emoji-selector"> 84 - <label>emoji:</label> 85 - <div class="emoji-grid"> 86 - {% for emoji in status_options %} 87 - <label class="emoji-option"> 88 - <input type="radio" name="status" value="{{emoji}}" required 89 - data-emoji="{{emoji}}" 90 - {% if let Some(my_status) = my_status %}{%if my_status == emoji %}checked{% endif %}{% endif %}> 91 - <span class="emoji-display">{{emoji}}</span> 92 - </label> 93 - {% endfor %} 94 - </div> 95 - </div> 96 - 97 - <div class="text-input"> 98 - <label for="status_text">description (optional):</label> 99 - <input type="text" id="status_text" name="text" 100 - placeholder="what are you up to?" 101 - maxlength="100" 102 - value=""> 103 - </div> 104 - 105 - <div class="expiration-selector"> 106 - <label for="expires_in">expires in:</label> 107 - <select id="expires_in" name="expires_in"> 108 - <option value="">never</option> 109 - <option value="30m">30 minutes</option> 110 - <option value="1h" selected>1 hour</option> 111 - <option value="2h">2 hours</option> 112 - <option value="4h">4 hours</option> 113 - <option value="8h">8 hours</option> 114 - <option value="1d">1 day</option> 115 - <option value="1w">1 week</option> 116 - </select> 117 - </div> 118 - 119 - <div class="form-message" id="form-message"></div> 120 - <button type="submit" class="submit-button" id="submit-btn">update status</button> 121 - </form> 122 - </div> 123 - 124 - <script> 125 - // Store current status for comparison 126 - const currentStatus = { 127 - emoji: {% if let Some(my_status) = my_status %}"{{my_status}}"{% else %}null{% endif %}, 128 - text: {% for status in statuses %}{% if status.author_did == did.to_string() && loop.first %}{% if status.text.is_some() %}"{{ status.text.as_ref().unwrap() }}"{% else %}""{% endif %}{% endif %}{% endfor %} || "", 129 - expires_in: "" // We don't track the original expiration 130 - }; 131 - 132 - const form = document.getElementById('status-form'); 133 - const message = document.getElementById('form-message'); 134 - const submitBtn = document.getElementById('submit-btn'); 135 - const textInput = document.getElementById('status_text'); 136 - const expiresSelect = document.getElementById('expires_in'); 137 - 138 - function checkForChanges() { 139 - const selectedEmoji = document.querySelector('input[name="status"]:checked'); 140 - 141 - if (!selectedEmoji) { 142 - message.textContent = ''; 143 - submitBtn.disabled = false; 144 - return; 145 - } 146 - 147 - const newEmoji = selectedEmoji.value; 148 - const newText = textInput.value.trim(); 149 - 150 - // Check if this is identical to current status (ignoring expiration) 151 - if (currentStatus.emoji === newEmoji && currentStatus.text === newText) { 152 - message.textContent = 'This is already your current status. Change the emoji or text to update.'; 153 - message.className = 'form-message error'; 154 - submitBtn.disabled = true; 155 - } else { 156 - message.textContent = ''; 157 - message.className = 'form-message'; 158 - submitBtn.disabled = false; 159 - } 160 - } 161 - 162 - // Check on any change 163 - document.querySelectorAll('input[name="status"]').forEach(radio => { 164 - radio.addEventListener('change', checkForChanges); 165 - }); 166 - textInput.addEventListener('input', checkForChanges); 167 - 168 - // Initial check 169 - checkForChanges(); 170 - </script> 171 - {% endif %} 172 - {% for status in statuses %} 173 - <div class="{% if loop.first %} status-line no-line {% else %} status-line {% endif %} "> 174 - <div> 175 - <div class="status">{{status.status}}</div> 176 - </div> 177 - <div class="desc"> 178 - <a class="author" 179 - href="https://bsky.app/profile/{{status.author_did}}">{{status.author_display_name()}}</a> 180 - {% if status.text.is_some() %} 181 - - {{ status.text.as_ref().unwrap() }} 182 - {% endif %} 183 - <div class="timestamp"> 184 - <span class="local-time" data-timestamp="{{ status.started_at.to_rfc3339() }}" data-format="relative" data-prefix="started"></span> 185 - {% if status.expires_at.is_some() %} 186 - {% if !status.is_expired() %} 187 - · <span class="local-time" data-timestamp="{{ status.expires_at.as_ref().unwrap().to_rfc3339() }}" data-prefix="expires"></span> 188 - {% else %} 189 - · expired 190 - {% endif %} 191 - {% endif %} 192 - </div> 193 - </div> 194 - </div> 195 - {% endfor %} 196 - </div> 197 - </div> 198 - 199 - <script> 200 - // Format local times 201 - const formatLocalTime = () => { 202 - document.querySelectorAll('.local-time').forEach(el => { 203 - const timestamp = el.getAttribute('data-timestamp'); 204 - const format = el.getAttribute('data-format'); 205 - const prefix = el.getAttribute('data-prefix'); 206 - 207 - if (!timestamp) return; 208 - 209 - const date = new Date(timestamp); 210 - const now = new Date(); 211 - const diffMs = now - date; 212 - const diffMins = Math.floor(diffMs / 60000); 213 - const diffHours = Math.floor(diffMs / 3600000); 214 - const diffDays = Math.floor(diffMs / 86400000); 215 - 216 - let text = ''; 217 - 218 - if (format === 'relative' || prefix === 'started') { 219 - if (diffMins < 1) { 220 - text = prefix === 'started' ? 'started just now' : 'just now'; 221 - } else if (diffMins < 60) { 222 - const ago = `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`; 223 - text = prefix === 'started' ? `started ${ago}` : ago; 224 - } else if (diffHours < 24) { 225 - const ago = `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`; 226 - text = prefix === 'started' ? `started ${ago}` : ago; 227 - } else if (diffDays < 7) { 228 - const ago = `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`; 229 - text = prefix === 'started' ? `started ${ago}` : ago; 230 - } else { 231 - text = prefix === 'started' ? `started ${date.toLocaleDateString()}` : date.toLocaleDateString(); 232 - } 233 - } else if (prefix === 'expires') { 234 - const futureMs = date - now; 235 - const futureMins = Math.floor(futureMs / 60000); 236 - const futureHours = Math.floor(futureMs / 3600000); 237 - const futureDays = Math.floor(futureMs / 86400000); 238 - 239 - if (futureMins < 1) { 240 - text = 'expires soon'; 241 - } else if (futureMins < 60) { 242 - text = `expires in ${futureMins} minute${futureMins !== 1 ? 's' : ''}`; 243 - } else if (futureHours < 24) { 244 - text = `expires in ${futureHours} hour${futureHours !== 1 ? 's' : ''}`; 245 - } else { 246 - text = `expires in ${futureDays} day${futureDays !== 1 ? 's' : ''}`; 247 - } 248 - } else { 249 - // Default format 250 - const options = { 251 - month: 'short', 252 - day: 'numeric', 253 - hour: 'numeric', 254 - minute: '2-digit', 255 - hour12: true 256 - }; 257 - text = date.toLocaleString('en-US', options).toLowerCase(); 258 - } 259 - 260 - el.textContent = text; 261 - }); 262 - }; 263 - 264 - // Initialize on page load 265 - document.addEventListener('DOMContentLoaded', () => { 266 - formatLocalTime(); 267 - 268 - // Update times every minute 269 - setInterval(formatLocalTime, 60000); 270 - 271 - // Initialize walkthrough for logged-in users 272 - {% if let Some(Profile {did, display_name}) = profile %} 273 - initWalkthrough(); 274 - {% endif %} 275 - }); 276 - 277 - // Walkthrough functionality 278 - function initWalkthrough() { 279 - // Check if user has seen the walkthrough 280 - if (localStorage.getItem('walkthrough_completed') === 'true') { 281 - return; 282 - } 283 - 284 - const overlay = document.querySelector('.walkthrough-overlay'); 285 - const spotlight = document.querySelector('.walkthrough-spotlight'); 286 - const tooltip = document.querySelector('.walkthrough-tooltip'); 287 - const titleEl = document.getElementById('walkthrough-title'); 288 - const textEl = document.getElementById('walkthrough-text'); 289 - const progressEl = document.querySelector('.walkthrough-progress'); 290 - const nextBtn = document.querySelector('.walkthrough-next'); 291 - const skipBtn = document.querySelector('.walkthrough-skip'); 292 - 293 - const steps = [ 294 - { 295 - title: 'Welcome to Status Tracker! 👋', 296 - text: 'Share what you\'re up to with a simple emoji and optional text. Let\'s take a quick tour!', 297 - element: null, 298 - position: 'center' 299 - }, 300 - { 301 - title: 'Pick Your Emoji', 302 - text: 'Choose any emoji to represent your current status. Click on one to select it!', 303 - element: '.emoji-grid', 304 - position: 'bottom' 305 - }, 306 - { 307 - title: 'Add Context (Optional)', 308 - text: 'Want to add more detail? Type a short description of what you\'re doing.', 309 - element: '#status_text', 310 - position: 'bottom' 311 - }, 312 - { 313 - title: 'Set Expiration Time', 314 - text: 'Your status can expire automatically. Choose how long it should last.', 315 - element: '#expires_in', 316 - position: 'bottom' 317 - }, 318 - { 319 - title: 'You\'re All Set! 🎉', 320 - text: 'Click "update status" to share what you\'re up to. Your status will appear below and others can see it at your public URL.', 321 - element: '#submit-btn', 322 - position: 'top' 323 - } 324 - ]; 325 - 326 - let currentStep = 0; 327 - 328 - function showStep(step) { 329 - // Update progress dots 330 - progressEl.innerHTML = steps.map((_, i) => 331 - `<div class="walkthrough-dot ${i === step ? 'active' : ''}"></div>` 332 - ).join(''); 333 - 334 - // Update content 335 - titleEl.textContent = steps[step].title; 336 - textEl.textContent = steps[step].text; 337 - 338 - // Update button text 339 - nextBtn.textContent = step === steps.length - 1 ? 'Got it!' : 'Next'; 340 - 341 - // Position spotlight and tooltip 342 - if (steps[step].element) { 343 - const el = document.querySelector(steps[step].element); 344 - if (el) { 345 - const rect = el.getBoundingClientRect(); 346 - spotlight.style.left = `${rect.left - 10}px`; 347 - spotlight.style.top = `${rect.top - 10}px`; 348 - spotlight.style.width = `${rect.width + 20}px`; 349 - spotlight.style.height = `${rect.height + 20}px`; 350 - spotlight.style.display = 'block'; 351 - spotlight.classList.add('pulse'); 352 - 353 - // Position tooltip 354 - let tooltipTop, tooltipLeft; 355 - const tooltipWidth = 320; 356 - 357 - if (steps[step].position === 'bottom') { 358 - tooltipTop = rect.bottom + 20; 359 - tooltipLeft = rect.left + (rect.width / 2) - (tooltipWidth / 2); 360 - } else if (steps[step].position === 'top') { 361 - tooltipTop = rect.top - 200; 362 - tooltipLeft = rect.left + (rect.width / 2) - (tooltipWidth / 2); 363 - } 364 - 365 - // Keep tooltip within viewport 366 - tooltipLeft = Math.max(10, Math.min(tooltipLeft, window.innerWidth - tooltipWidth - 10)); 367 - tooltipTop = Math.max(10, tooltipTop); 368 - 369 - tooltip.style.left = `${tooltipLeft}px`; 370 - tooltip.style.top = `${tooltipTop}px`; 371 - } 372 - } else { 373 - // Center tooltip for intro/outro 374 - spotlight.style.display = 'none'; 375 - tooltip.style.left = '50%'; 376 - tooltip.style.top = '50%'; 377 - tooltip.style.transform = 'translate(-50%, -50%)'; 378 - } 379 - 380 - // Show tooltip with animation 381 - setTimeout(() => { 382 - tooltip.classList.add('active'); 383 - }, 100); 384 - } 385 - 386 - function nextStep() { 387 - tooltip.classList.remove('active'); 388 - 389 - setTimeout(() => { 390 - currentStep++; 391 - if (currentStep >= steps.length) { 392 - endWalkthrough(); 393 - } else { 394 - // Reset transform for positioned tooltips 395 - if (steps[currentStep].element) { 396 - tooltip.style.transform = 'translateY(0)'; 397 - } 398 - showStep(currentStep); 399 - } 400 - }, 300); 401 - } 402 - 403 - function endWalkthrough() { 404 - localStorage.setItem('walkthrough_completed', 'true'); 405 - overlay.classList.remove('active'); 406 - spotlight.style.display = 'none'; 407 - tooltip.classList.remove('active'); 408 - 409 - setTimeout(() => { 410 - overlay.style.display = 'none'; 411 - tooltip.style.display = 'none'; 412 - }, 300); 413 - } 414 - 415 - // Event listeners 416 - nextBtn.addEventListener('click', nextStep); 417 - skipBtn.addEventListener('click', endWalkthrough); 418 - 419 - // Start walkthrough after a short delay 420 - setTimeout(() => { 421 - overlay.style.display = 'block'; 422 - tooltip.style.display = 'block'; 423 - setTimeout(() => { 424 - overlay.classList.add('active'); 425 - showStep(0); 426 - }, 100); 427 - }, 500); 428 - } 429 - </script> 430 - 431 - {%endblock content%}
+4 -4
templates/status.html
··· 2 2 3 3 {% block title %}@{{ handle }} - status.zzstoatzz.io{% endblock %} 4 4 {% block og_url %}/@{{ handle }}{% endblock %} 5 - {% block og_title %}@{{ handle }}'s Status{% endblock %} 6 - {% block og_description %}{% if let Some(current) = current_status %}{{ current.status }} {% if current.text.is_some() %}{{ current.text.as_ref().unwrap() }}{% endif %}{% else %}No status set{% endif %}{% endblock %} 5 + {% block og_title %}@{{ handle }}'s status{% endblock %} 6 + {% block og_description %}{% if let Some(current) = current_status %}{{ current.status }} {% if current.text.is_some() %}{{ current.text.as_ref().unwrap() }}{% endif %}{% else %}no status currently set{% endif %}{% endblock %} 7 7 {% block twitter_url %}/@{{ handle }}{% endblock %} 8 - {% block twitter_title %}@{{ handle }}'s Status{% endblock %} 9 - {% block twitter_description %}{% if let Some(current) = current_status %}{{ current.status }} {% if current.text.is_some() %}{{ current.text.as_ref().unwrap() }}{% endif %}{% else %}No status set{% endif %}{% endblock %} 8 + {% block twitter_title %}@{{ handle }}'s status{% endblock %} 9 + {% block twitter_description %}{% if let Some(current) = current_status %}{{ current.status }} {% if current.text.is_some() %}{{ current.text.as_ref().unwrap() }}{% endif %}{% else %}no status currently set{% endif %}{% endblock %} 10 10 11 11 {% block content %} 12 12 <div id="root">