# The Presence kit — who else is on a record, and who moved it first

> `Osysharp.Presence` shows who else has a record open right now — any record, addressed by its id — with what they are doing ("viewing", "replying", "assigning") and never what they wrote; and it turns a concurrency refusal into the sentence a person needs: "Ada changed Amount 2 minutes ago — your edit is still here."

<!-- id: realtime-collaboration-kit · area: realtime · stability: preview · html: https://osysharp.com/reference/realtime/collaboration-kit/ -->

## Summary        {#summary}
`use Osysharp.Presence@0;` gives every record in an app a **seat set**: open the record and you are in it, close it
and you are gone — the platform keeps it from the connection and stamps who sits in each seat. A seat carries an activity
word and never content. The app says one thing — who may see colleagues on a record:

```osy syntax
policy CollaboratesOnRecords => IsStaff;
```

## Signature      {#signature}
```osy syntax
policy CollaboratesOnRecords;                                // the app supplies it
public class RecordSeat { Principal? Who; string Activity; } // Who is stamped by the platform
topic RecordPresence(Guid recordId) { Candidates = u => CollaboratesOnRecords; Carries = RecordSeat; Presence = true; }

void SeatActivity(Guid recordId, string activity);           // say what you are doing on it now
List<RecordSeat> Colleagues(List<RecordSeat> here, Guid me); // everyone but the viewer
string WhoIsHere(List<RecordSeat> colleagues);               // "Raj is replying" · "Raj and Ines are here"
component RecordSeats(Guid recordId, string activity = "viewing");   // the header strip; announces the viewer
component RecordSeatsLine(Guid recordId);                            // one line for a list row; announces nothing

string MovedFirst(List<EntityChange> changes, string otherwise = "Somebody changed this while you had it open", DateTime? now = null);
string TookFirst(string who, DateTime at, string what = "took this", DateTime? now = null);
string Ago(DateTime at, DateTime now);                        // "just now" · "12 minutes ago" · "on 4 Oct at 14:05"
```

## Description    {#description}

### The record is the room                    {#room}
`RecordPresence` is addressed by a record's **id**, so the kit works for any entity — a ticket, a case, an approval —
without knowing which. Being subscribed IS having the record open: there is nothing to post and nothing to clean up, and
a crashed browser cannot linger. `RecordSeats(id)` subscribes and announces the viewer as `"viewing"` (or the activity you
pass) when it mounts; `SeatActivity(id, "replying")` changes what you are doing later, from a composer or a menu.

### Staff only, by the app's rule              {#who}
The topic's door is `CollaboratesOnRecords`. Anyone it refuses cannot subscribe, and announcing without a seat puts
nobody in the set — `Announce` decorates your own seat and never creates one. A customer looking at their own ticket is
never shown in, or shown, the desk's seats unless the app's policy says so.

### Who is stamped; content never travels      {#seat}
`RecordSeat.Who` is filled in by the platform from the connection; `new RecordSeat { Who = … }` is refused at compile. And
a seat has no field for words: what somebody wrote reaches an open page through the row's own read rules (SECURITY-INVARIANT
`realtime-broadcast-signal-only`), so a note written by a colleague cannot reach anyone through presence.

### The sentence that names who moved it first {#moved-first}
`[ConcurrencyCheck]` refuses a save over somebody else's newer one. `MovedFirst` turns the facts `<Entity>.Changes(id)`
hands over — who, when, which fields, never values — into the sentence: "Ada changed Amount and Owner 2 minutes ago". Make
it on the server (the trail is the server's) and add the half only the page knows: "— your edit is still here."
`TookFirst(who, at)` says the same for an ownership collision: "Ines took this 2 minutes ago".

### Online is not available                   {#not-availability}
A seat says who is **looking**. It is not consent to receive work: a routing kit reads its own explicit availability, never
this topic, because a held socket is not activity and an open tab is not a yes.

## Examples       {#examples}

### A case editor with seats and the refusal's sentence   {#example-editor}
```osy title="who else is on the case, and who moved it first" test app=realtime-collaboration-kit
app CaseDesk {
  use Osysharp.Identity;
  use Osysharp.Presence;
  use Osysharp.Observability;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Presence;
using Osysharp.Observability;

[Role] enum DeskRole { Authenticator, Staff }
policy IsStaff => RoleGrant.Any(g => g.Member == user && g.Role == DeskRole.Staff);
policy CollaboratesOnRecords => IsStaff;

[ConcurrencyCheck]
entity Case {
  [Required, MaxLength(40)] string Reference;
  decimal Amount;
  security { allow read, create, update when IsStaff; }
}

[Route("/case/{id}")]
component CaseEditor(Guid id) {
  var cases = Case.Where(c => c.Id == id).ToList();
  var message = "";
  action Save(Case c) {
    try { c.Amount = c.Amount + 1; UnitOfWork.Commit(); message = "Saved."; }
    catch (ConflictException e) { message = WhoMovedIt(c.Id) + " — your edit is still here."; }
  }
  render {
    Stack(gap: 3, p: 4) {
      RecordSeats(id);
      if (message != "") { Alert(message, tone: Tone.Warning); }
      foreach (var c in cases) {
        Row(gap: 3, align: Align.Center) {
          Text(c.Reference);
          Button(label: "Add one", onPress: () => Save(c));
        }
      }
    }
  }
}

string WhoMovedIt(Guid id) { return MovedFirst(Case.Changes(id)); }
```

## See also       {#see-also}
- [Here, Announce](https://osysharp.com/reference/realtime/presence/) — `Here` and `Announce`, the platform primitives under the seats
- [topic](https://osysharp.com/reference/realtime/topic/) — how a topic's address and `Candidates` work
- [ConcurrencyCheck](https://osysharp.com/reference/entity/concurrency-check/) — the refusal the sentence explains
- [Changes](https://osysharp.com/reference/entity/changes/) — the facts `MovedFirst` reads
- **`kits/presence/tests`** — the kit's front door: staff-only seats, who is stamped, the refusal through a screen
