Ease docs/Concepts/Contacts
Contacts: the contact graph
Who the user knows. The graph that gates DM eligibility and group membership and drives the approval step on adding members. Send a request, accept it, become mutual contacts; from there the rest of the SDK opens up between you and the other party.
What's there
list(): established contacts.sendRequest(target, welcomeMessage?): by user id, email, or phone.incomingRequests()/outgoingRequests(): pending state, inbound and outbound.accept(requestId)/decline(requestId)/rescind(requestId): resolve a request.remove(userId): drop an established contact.block(userId)/unblock(userId): the blocking primitive.
The request flow
Alice sends a request to Bob. Bob's SDK receives a contactRequestReceived event. Bob accepts. Alice's SDK receives a contactRequestAccepted event. From that point on, Alice and Bob are mutual contacts. DMs work, group membership works, the rest of the SDK opens up between them.
// Alice's side
await ease.contacts.sendRequest(
{ email: 'bob@example.com' },
"Hey Bob, let's chat",
);
ease.contacts.onContactRequestAccepted((event) => {
console.log('Bob accepted; his user id is', event.contactUserId);
});
// Bob's side
ease.contacts.onContactRequestReceived((event) => {
// Show a UI prompt; on accept:
ease.contacts.accept(event.requestId);
});Subscribe to the request events before you open the socket so no inbound request slips past the listener. The received-request handler gives you a request you resolve with accept(requestId); accept returns the new contact's user id once the request settles.
The welcome message
The optional welcomeMessage on sendRequest is encrypted to the recipient before send. The server sees ciphertext; the recipient sees plaintext alongside the request. Keep it short. It is the first impression in your recipient's pending list.
Lookup and filtering
Two reads on the contact graph: strict lookup by handle to translate an email, phone, or username into a user id, and client-side filtering over the listed contacts.
Lookup by handle
You have an email, phone number, or username and you want the userId behind it. Useful when you're about to send a contact request and the form on your side collects an address, not an opaque id.
const { userId } = await ease.contacts.lookup({ email: 'bob@example.com' });
// Resolves the handle to a user id. Also accepts { phone } or { username }.Strict, single-result. Most apps skip the explicit lookup and let sendRequest({ email }) resolve it inline. The lookup is exposed for cases where you want the userId first.
Filtering the contact list
For finding a contact by name, list() returns the full set and the client filters. The SDK ships deterministic ordering, so substring filters render predictably.
const all = await ease.contacts.list();
const matches = all.filter((c) =>
c.name?.toLowerCase().includes('alice'),
);Online presence
Every contact row carries a live presence snapshot. Three fields:
online:truewhile the contact has at least one socket connection open;falsewhile they're away.lastSeenAt: Unix seconds, set when the contact disconnects;nullwhile they're online.statusText: an optional free-form status the contact set on their profile.
Subscribing to changes
The SDK pushes a presence event whenever a contact connects or disconnects:
const unsub = ease.contacts.onPresenceUpdate((evt) => {
// evt.userId, evt.online, evt.lastSeenAt, evt.statusText
// Re-render the contact row's online dot, or trigger a notification.
});
// later
unsub();The initial snapshot
The first time a session opens, the server pushes a single snapshot with every current contact's state. Subscribe to it on launch so you don't have to wait for a contact to change state before you see them online:
ease.contacts.onPresenceSnapshot((rows) => {
// rows is one entry per contact
});After the snapshot lands, onPresenceUpdate carries the deltas. The two listeners are independent; a typical UI subscribes to both. An onAvatarUpdated listener rounds out the row: it fires when a contact changes their profile image.
Blocking
block(userId) removes them from your contacts, if they were one, and prevents any future contact request, DM, or group-add from the blocked user. unblock(userId) reverses it, but doesn't re-establish contact; either party still has to send a fresh request.
Why DMs are contact-gated
The default is that you can DM someone who agreed to accept DMs from you. The contact graph is the gate that survives every other check: you can only DM a mutual contact, and group members must be contacts of the creator. Open inboxes are a separate decision per room type; for the E2EE-by-default rooms, contact-of-caller is the constraint that holds.