Skip to content

Instantly share code, notes, and snippets.

@arthyn
Created January 30, 2026 19:52
Show Gist options
  • Select an option

  • Save arthyn/d790700b0da75721dca7934fd2db494f to your computer and use it in GitHub Desktop.

Select an option

Save arthyn/d790700b0da75721dca7934fd2db494f to your computer and use it in GitHub Desktop.
Tlon Presence & Availability System PRD

Presence & Availability System PRD

Working directory: ~/Projects/homestead
Author: ~nocsyx-lassul
Date: 2026-01-24
Status: Draft


Overview

A two-part presence system for Tlon that provides both ephemeral status indicators (typing, viewing, in-call) and persistent availability states (online, away, offline). Designed to feel alive while respecting user privacy through opt-out controls.

Goals

  1. Show when someone is typing in a chat
  2. Indicate who's currently viewing a channel
  3. Display call/listening status
  4. Show online/away/offline availability
  5. Track last-seen timestamps
  6. Allow users to opt-out (go unseen) at any level

System Architecture

Two complementary systems with distinct purposes:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          PRESENCE SYSTEM                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚      EPHEMERAL (seconds)        β”‚      AVAILABILITY (hours)         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  %presence agent (NEW)          β”‚  %contacts agent (EXISTING)       β”‚
β”‚  β€’ typing indicators            β”‚  β€’ online/away/offline            β”‚
β”‚  β€’ viewing status               β”‚  β€’ last-seen timestamp            β”‚
β”‚  β€’ call/listening               β”‚  β€’ custom status message          β”‚
β”‚  β€’ local storage only           β”‚  β€’ profile field sync             β”‚
β”‚  β€’ routed via %chat/%groups     β”‚  β€’ direct profile queries         β”‚
β”‚  β€’ auto-timeout (seconds)       β”‚  β€’ user-controlled updates        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Part 1: %presence Agent (Ephemeral Status)

Purpose

Handle short-lived, context-specific presence like typing indicators. Local-only storage with network routing delegated to %chat and %groups.

Key Design Principle

%presence only accepts self-pokes. It never talks to the network directly.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Network Flow: Typing Indicator                                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                      β”‚
β”‚  [Their Ship]                      [My Ship]                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚  β”‚  %chat      β”‚ ──── network ──→  β”‚  %chat      β”‚                   β”‚
β”‚  β”‚  (sends)    β”‚                   β”‚  (validates)β”‚                   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                           β”‚                          β”‚
β”‚                                           β”‚ local poke               β”‚
β”‚                                           ↓                          β”‚
β”‚                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚                                    β”‚  %presence  β”‚                   β”‚
β”‚                                    β”‚  (stores)   β”‚                   β”‚
β”‚                                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                           β”‚                          β”‚
β”‚                                           β”‚ subscription update      β”‚
β”‚                                           ↓                          β”‚
β”‚                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚                                    β”‚  UI/Client  β”‚                   β”‚
β”‚                                    β”‚  (displays) β”‚                   β”‚
β”‚                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

State Structure

+$  topic     @tas                              :: typing, viewing, call, listening
+$  context   path                              :: where (e.g. /chat/~zod/general, /call/~zod/room-1)
+$  status    [=topic =context expires=@da]     :: topic + context + expiry
+$  presence  (map ship (map topic status))     :: ship β†’ topic β†’ status

Context examples:

  • /chat/~sampel-palnet/general β€” typing in a chat channel
  • /notebook/~sampel-palnet/blog β€” viewing a notebook
  • /call/~sampel-palnet/room-1 β€” in a voice call
  • /groups/~sampel-palnet/my-group/settings β€” viewing group settings

Actions

+$  action
  $%  [%set topic=@tas context=path]            :: set status (resets timeout)
      [%clear topic=@tas context=path]          :: explicitly clear
      [%clear-all context=path]                 :: clear all for context
      [%heartbeat ~]                            :: process expired statuses
  ==

Default Timeouts (configurable)

Topic Timeout Description
typing 10s Typing indicator
viewing 5min Active in channel
call ∞* In voice call (clear on end)
listening 30s Listening to radio/audio

*call status cleared explicitly, not via timeout

Subscription Paths

/presence/[topic]/[...context]   :: all ships for topic in context (e.g. /presence/typing/chat/~zod/general)
/presence/ship/[ship]            :: all statuses for a specific ship
/presence/context/[...context]   :: all topics for a context
/presence/all                    :: firehose (admin/debug)

Integration Points

%chat integration:

::  in %chat's message handler, when receiving typing from network:
=/  valid  (validate-sender src.bowl our.bowl msg)
=/  ctx    /chat/(scot %p host.nest)/(scot %tas name.nest)
?:  valid
  (poke-self %presence [%set %typing ctx])
~

%groups integration:

::  similar pattern for group contexts
=/  ctx  /groups/(scot %p host.flag)/(scot %tas name.flag)/settings
(poke-self %presence [%set %viewing ctx])

Files to Create

desk/
β”œβ”€β”€ app/presence.hoon        :: agent implementation
β”œβ”€β”€ sur/presence.hoon        :: types and structures
β”œβ”€β”€ mar/presence/
β”‚   β”œβ”€β”€ action.hoon          :: mark for actions
β”‚   └── update.hoon          :: mark for subscription updates
└── lib/presence.hoon        :: helper functions (optional)

Part 2: %contacts Availability (Persistent Status)

Purpose

Longer-lived availability states that sync between ships via existing profile infrastructure.

Key Design Principle

Leverage existing %contacts profile sync. No new agent neededβ€”just new fields.

New Profile Fields

Add to existing contact profile structure:

::  in sur/contacts.hoon, extend +$  contact
+$  contact
  $:  nickname=@t
      bio=@t
      status=@t               :: existing: custom status text
      avatar=(unit @t)
      cover=(unit @t)
      color=@ux
      ::  NEW FIELDS
      availability=@t         :: "online" | "away" | "offline" | "dnd" | ""
      last-seen=(unit @da)    :: when last active (null if private)
      presence-mode=@t        :: "visible" | "last-seen-only" | "hidden"
  ==

Availability States

State Meaning Display
online Active now 🟒 Green dot
away Idle/stepped away 🟑 Yellow dot
offline Not connected ⚫ Gray dot
dnd Do not disturb πŸ”΄ Red dot
"" Not sharing (opted out) No indicator

Profile Sync Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Availability Sync Flow                                             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                     β”‚
β”‚  [My Ship]                        [Their Ship]                      β”‚
β”‚                                                                     β”‚
β”‚  Client sets availability         Profile query returns             β”‚
β”‚         β”‚                         availability + last-seen          β”‚
β”‚         ↓                                ↑                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚  β”‚  %contacts  β”‚ ── profile ───→ β”‚  %contacts  β”‚                   β”‚
β”‚  β”‚  (my ship)  β”‚    sync         β”‚  (their)    β”‚                   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚                                                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Automatic Updates

Client-side logic (not in agent):

// Pseudocode for client availability management
const IDLE_THRESHOLD = 5 * 60 * 1000;  // 5 minutes
const OFFLINE_THRESHOLD = 30 * 60 * 1000;  // 30 minutes

let lastActivity = Date.now();

function updateAvailability() {
  const idle = Date.now() - lastActivity;
  
  if (idle < IDLE_THRESHOLD) {
    setAvailability('online');
  } else if (idle < OFFLINE_THRESHOLD) {
    setAvailability('away');
  } else {
    setAvailability('offline');
  }
  
  // Also update last-seen unless hidden
  if (presenceMode !== 'hidden') {
    setLastSeen(Date.now());
  }
}

Part 3: Privacy & Opt-Out Settings

Privacy Philosophy

Users should have granular control over what they share. Default to visible, but make opting out easy and respected.

Privacy Modes

+$  presence-mode
  $?  %visible         :: show availability + last-seen + ephemeral
      %last-seen-only  :: show last-seen only (no live status)
      %hidden          :: completely invisible
  ==

Per-Feature Toggles

In addition to the global mode, users can toggle individual features:

Setting Default Description
share-availability true Show online/away/offline
share-last-seen true Show when last active
share-typing true Send typing indicators
share-viewing true Show when viewing a channel
show-others-availability true See others' availability (mutual)
show-others-typing true See others' typing indicators

Mutual Visibility (Optional)

If implemented, users who opt out of sharing don't see others' status either:

If I hide β†’ I don't see others
If I share β†’ I see others (who share)

Settings Storage

Store in %settings-store under presence namespace:

/apps/groups/presence/share-availability
/apps/groups/presence/share-last-seen
/apps/groups/presence/share-typing
/apps/groups/presence/share-viewing
/apps/groups/presence/mode

UI for Privacy Settings

Location: Settings β†’ Privacy β†’ Presence

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Privacy β†’ Presence                                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                     β”‚
β”‚  Presence Mode                                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ β—‹ Visible      Show everything                              β”‚   β”‚
β”‚  β”‚ β—‹ Last-seen    Only show when I was last active             β”‚   β”‚
β”‚  β”‚ β—‹ Hidden       Don't share any presence info                β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                     β”‚
β”‚  Individual Controls (when not Hidden)                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ [βœ“] Share typing indicators                                 β”‚   β”‚
β”‚  β”‚ [βœ“] Share viewing status                                    β”‚   β”‚
β”‚  β”‚ [βœ“] Share online/away/offline                               β”‚   β”‚
β”‚  β”‚ [βœ“] Share last-seen time                                    β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                     β”‚
β”‚  Note: If you hide your presence, you won't see others' either.    β”‚
β”‚                                                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Part 4: UI Integration

4.1 Typing Indicators

Chat input area:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ~sampel-palnet is typing...                                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  [                                                    ] [Send]      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Multiple people:

~sampel-palnet and ~master-botnet are typing...
~sampel-palnet and 2 others are typing...

Animation: Three bouncing dots (...) or subtle pulse

4.2 Channel Viewer List

Sidebar or header:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  #general                      πŸ‘ 3 viewing  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Viewing now:                                β”‚
β”‚  β€’ ~sampel-palnet                            β”‚
β”‚  β€’ ~master-botnet                            β”‚
β”‚  β€’ ~random-person                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4.3 Availability Indicators

In member lists and DMs:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  DIRECT MESSAGES               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  β”‚ 🟒 ~sampel-palnet          β”‚  ← online
β”‚  β”‚ 🟑 ~master-botnet          β”‚  ← away
β”‚  β”‚ ⚫ ~random-person          β”‚  ← offline
β”‚  β”‚    ~hidden-person          β”‚  ← opted out (no dot)
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Profile card / DM header:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ~sampel-palnet                                                     β”‚
β”‚  🟒 Online β€’ Last seen: just now                                    β”‚
β”‚  Status: "Building presence features"                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When hidden:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ~hidden-person                                                     β”‚
β”‚  Last seen: β€”                                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4.4 Voice/Call Integration

In channel when call active:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  #voice-chat                                     πŸ”Š 3 in call       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  In voice:                                                          β”‚
β”‚  β€’ ~sampel-palnet (speaking)                                        β”‚
β”‚  β€’ ~master-botnet                                                   β”‚
β”‚  β€’ ~random-person (muted)                                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Part 5: Implementation Plan

Phase 1: Foundation (Week 1-2)

Backend:

  1. Create %presence agent (app/presence.hoon, sur/presence.hoon)
  2. Add new fields to %contacts profile structure
  3. Implement %chat integration for routing presence pokes
  4. Add %settings-store entries for privacy settings

Frontend:

  1. Subscribe to %presence updates in relevant views
  2. Add typing indicator component to chat input
  3. Add availability dot component

Phase 2: Core UI (Week 3-4)

Backend:

  1. Add %groups integration for viewing status
  2. Implement heartbeat timer for expiring statuses
  3. Add privacy mode enforcement

Frontend:

  1. Typing indicators in chat view
  2. Availability dots in DM list and member lists
  3. Profile card availability display
  4. "X is typing" text above input

Phase 3: Settings & Privacy (Week 5)

Backend:

  1. Wire up privacy settings to filter what's shared/shown
  2. Implement mutual visibility logic (optional)

Frontend:

  1. Privacy settings UI in Settings β†’ Privacy
  2. Respect hidden users in UI (no placeholder, just absent)
  3. "X viewers" in channel header

Phase 4: Polish (Week 6)

  1. Animations for typing indicator
  2. Performance optimization (batch updates)
  3. Edge cases (reconnection, stale data)
  4. Mobile parity

Open Questions

  1. Mutual visibility: Should hiding your status also hide others from you? (Suggested: yes, for simplicity and fairness)

  2. Per-channel/group settings: Should users be able to hide presence in specific channels only? (Suggested: defer to v2)

  3. Bot/agent presence: Should Clawdbot and similar agents show typing? (Suggested: yes, opt-in for agents)

  4. Read receipts: Out of scope for this PRD, but related. Consider separately.

  5. Presence in public channels: Should viewing be visible in large public channels? (Suggested: show count only, not names, above threshold)


Success Metrics

  • Typing indicators appear within 500ms of user starting to type
  • Availability updates propagate within 5s
  • <1% false positives on typing (showing when not typing)
  • Privacy settings respected 100% of the time
  • No presence data leaks for hidden users

Appendix: Hoon Sketches

sur/presence.hoon

|%
+$  topic     ?(%typing %viewing %call %listening)
+$  context   path                                   :: flexible context path
+$  status    [=topic =context expires=@da]
+$  presence  (map ship (map topic status))
::
+$  action
  $%  [%set =topic =context]
      [%clear =topic =context]
      [%clear-all =context]
      [%heartbeat ~]
  ==
::
+$  update
  $%  [%set =ship =status]
      [%clear =ship =topic =context]
      [%full =presence]
  ==
::
+$  mode      ?(%visible %last-seen-only %hidden)
--

app/presence.hoon (skeleton)

/+  default-agent, dbug
|%
+$  versioned-state  $%(state-0)
+$  state-0  [%0 =presence:sur mode=mode:sur timeouts=(map topic:sur @dr)]
+$  card  card:agent:gall
--
%-  agent:dbug
^-  agent:gall
|_  =bowl:gall
+*  this  .
    def   ~(. (default-agent this %.n) bowl)
::
++  on-init
  ^-  (quip card _this)
  =/  defaults  (malt ~[[%typing ~s10] [%viewing ~m5] [%call *@dr] [%listening ~s30]])
  [~ this(state [%0 ~ %visible defaults])]
::
++  on-poke
  |=  [=mark =vase]
  ^-  (quip card _this)
  ?>  =(src.bowl our.bowl)  :: only self-pokes
  ?+  mark  (on-poke:def mark vase)
      %presence-action
    =/  act  !<(action:sur vase)
    ?-  -.act
        %set
      ::  update state, kick subscribers
      =/  expires  (add now.bowl (~(got by timeouts.state) topic.act))
      =/  new-status  [topic.act context.act expires]
      ::  ... update presence map, send updates
      [~ this]
    ::
        %clear
      ::  remove from state, kick subscribers
      [~ this]
    ::
        %clear-all
      ::  clear all for context
      [~ this]
    ::
        %heartbeat
      ::  expire old entries
      [~ this]
    ==
  ==
::
++  on-watch
  |=  =path
  ^-  (quip card _this)
  ?+  path  (on-watch:def path)
      [%presence %topic @ *]
    ::  /presence/topic/[topic]/[...context]
    [~ this]
  ::
      [%presence %ship @ ~]
    ::  /presence/ship/[ship]
    [~ this]
  ::
      [%presence %context *]
    ::  /presence/context/[...context]
    [~ this]
  ==
::
++  on-agent  on-agent:def
++  on-arvo
  |=  [=wire =sign-arvo]
  ^-  (quip card _this)
  ?+  wire  (on-arvo:def wire sign-arvo)
      [%heartbeat ~]
    ::  process timer, set next
    [~ this]
  ==
++  on-peek   on-peek:def
++  on-leave  on-leave:def
++  on-fail   on-fail:def
--

Related Work

  • Telegram: Online status + last seen + typing
  • Discord: Online/idle/dnd + typing + viewing voice
  • Slack: Active/away + typing + huddle presence
  • Signal: Typing only (privacy-focused)
  • WhatsApp: Online/last-seen + typing + read receipts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment