Esposter

Per-Room Nicknames

Members can set a per-room display name that overrides their global username within that room.

The resolution rule

All member name display goes through getDisplayName(user, roomId) from useUserToRoomStore. Never read user.name or member.name directly in a room context.

WRONG   — {{ creator.name }}                    (ignores room nickname)
CORRECT — {{ getDisplayName(member, roomId) }}  (nickname, falls back to global name)

Nicknames are stored as text().notNull().default("") — empty string means "no nickname set", never null. Empty string is falsy, so fallback uses ||, never ??:

// WRONG — empty string passes through, user sees blank name
getNicknameMap(roomId)?.get(user.id) ?? user.name;

// CORRECT
getNicknameMap(roomId)?.get(user.id) || user.name;

Applied in: mention labels (useMessageWithMentions), the member list sidebar (MemberListItem), the room settings Members panel (Settings/Type/Member/ListItem), and the push notification title (queried server-side before the EventGrid publish). useCreator does not overlay the nickname — components that need the message creator's nickname call getDisplayName directly.

Data model

One column: usersToRooms.nicknametext, NOT NULL DEFAULT "", max 32 chars.

Client state — useUserToRoomStore

Two internal maps, both keyed roomId → inner map, deliberately split for privacy:

  • userToRoomMapMap<roomId, Map<userId, UserToRoomInMessage>> — the current user's own full row (notificationType, lastMessageAt, isHidden…); populated by readMyUsersToRooms and the onUpdateUserToRoom subscription.
  • nicknameMapMap<roomId, Map<userId, string>>all members' nicknames (including self); populated by readNicknames (called per-room from readMetadata in useReadMembers) and the same subscription.

Procedures

Two focused procedures keep each member's private fields (notificationType, lastMessageAt, isHidden) to themselves:

ProcedureInputReturnsWho
readNicknames{ roomId, userIds[] }{ userId, roomId, nickname }[]All listed members
readMyUsersToRooms{ roomIds[] }Full UserToRoomInMessage[]Current user only

readMyUsersToRooms is called from useReadRooms to pre-load the current user's private settings (unread state, notification type) for all rooms at startup.

Setting your nickname

Room settings → My Profile tab (visible to all members, no permission required). Text field pre-filled with the current nickname; empty = no nickname. Saves via updateUserToRoom({ roomId, nickname }); the update propagates via the onUpdateUserToRoom subscription into both maps.

Key files

FileRole
packages/app/app/store/message/room/userToRoom.tsuseUserToRoomStore, getDisplayName
packages/app/app/composables/message/mentions/useMessageWithMentions.tsmention label resolution
packages/app/app/components/Message/Model/Room/Settings/Type/Profile/My Profile tab + nickname field

Notes

Setting other members' nicknames via the ManageNicknames permission bit is not wired to a UI yet — the bit exists in RoomPermission (see /docs/esbabbler/rbac) but only self-service nickname editing is built.