For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: WhatsApp-style 2-state read receipts on your own sent messages — ✓ sent, ✓✓ read — driven by a live relay (no persistence).
Architecture: Two new CUSTOM packets. Client→server MarkConsoleRead(peerId) is sent when you focus/read a conversation. The emulator relays it to the peer (if online and a friend) as server→client ConsoleReadReceipt(readerId). The recipient's client marks its own messages in that conversation as READ and renders ✓✓.
Design note — no DB, live-relay only (refinement of the spec): the spec proposed a messenger_read_state table + a login-time receipt batch. The Nitro client does NOT persist per-message history across sessions, so a persisted receipt would have no message to update on next login. Persistence is therefore omitted; receipts are a live in-session relay. This keeps Phase 3 simpler with no loss of user-visible behavior. (If cross-session receipts are ever wanted, they'd require persisting client-side message history first — out of scope.)
All repos on feat/messenger-groups-receipts. Client commits use git -c user.name=simoleo89 -c user.email=simoleo89@users.noreply.github.com. No Co-Authored-By / AI attribution. Emulator working tree has an UNRELATED modified soundboard/SoundboardPlayEvent.java + untracked jars — never stage those; git add only the listed files.
Header IDs (custom, verified free in all 4 files)
Packet
Direction
Renderer header
Emulator header
Value
MarkConsoleRead
client→server
OutgoingHeader.MARK_CONSOLE_READ
Incoming.MarkConsoleReadEvent
4085
ConsoleReadReceipt
server→client
IncomingHeader.CONSOLE_READ_RECEIPT
Outgoing.ConsoleReadReceiptComposer
4086
(Renderer Outgoing N == Emulator Incoming N; Renderer Incoming N == Emulator Outgoing N — the verified convention.)
Task P3-1: Renderer — packets + registration + parser test
Files: see File map (renderer).
Step 1: Write the failing parser test
Create packages/communication/src/messages/parser/friendlist/__tests__/ConsoleReadReceiptParser.test.ts (mirror the existing __tests__/FriendCategoryComposers.test.ts / mentions parser test style with a TestWrapper over BinaryReader/BinaryWriter):
import { describe, expect, it } from 'vitest';
import { BinaryReader, BinaryWriter } from '@nitrots/utils';
import { ConsoleReadReceiptParser } from '../ConsoleReadReceiptParser';
class TestWrapper
{
constructor(private reader: BinaryReader) {}
readByte() { return this.reader.readByte(); }
readShort() { return this.reader.readShort(); }
readInt() { return this.reader.readInt(); }
readString() { const len = this.reader.readShort(); return this.reader.readBytes(len).toString(); }
header = 0;
get bytesAvailable() { return this.reader.remaining() > 0; }
}
describe('ConsoleReadReceiptParser', () =>
{
it('parses the reader id', () =>
{
const w = new BinaryWriter();
w.writeInt(42);
const parser = new ConsoleReadReceiptParser();
parser.flush();
parser.parse(new TestWrapper(new BinaryReader(w.getBuffer())) as any);
expect(parser.readerId).toBe(42);
});
});
Step 2: Run it, confirm FAIL
Run: cd Nitro_Render_V3 && yarn test --run packages/communication/src/messages/parser/friendlist/__tests__/ConsoleReadReceiptParser.test.ts
Expected: FAIL (module not found).
import { IMessageComposer } from '@nitrots/api';
export class MarkConsoleReadComposer implements IMessageComposer<ConstructorParameters<typeof MarkConsoleReadComposer>>
{
private _data: ConstructorParameters<typeof MarkConsoleReadComposer>;
constructor(peerId: number)
{
this._data = [ peerId ];
}
public getMessageArray()
{
return this._data;
}
public dispose(): void
{
return;
}
}
Step 6: Add header constants
In OutgoingHeader.ts (near the friend headers): public static MARK_CONSOLE_READ = 4085;
In IncomingHeader.ts (near the messenger headers): public static CONSOLE_READ_RECEIPT = 4086;
Step 7: Barrel exports
messages/outgoing/friendlist/index.ts: export * from './MarkConsoleReadComposer';
messages/incoming/friendlist/index.ts: export * from './ConsoleReadReceiptEvent';
messages/parser/friendlist/index.ts: export * from './ConsoleReadReceiptParser';
Step 8: Register in NitroMessages
In NitroMessages.ts: add the two classes to the existing friendlist imports, then:
in the events block (next to this._events.set(IncomingHeader.MESSENGER_CHAT, NewConsoleMessageEvent);): this._events.set(IncomingHeader.CONSOLE_READ_RECEIPT, ConsoleReadReceiptEvent);
in the composers block (next to this._composers.set(OutgoingHeader.MESSENGER_CHAT, SendMessageComposer);): this._composers.set(OutgoingHeader.MARK_CONSOLE_READ, MarkConsoleReadComposer);
Step 9: Compile + test
Run: cd Nitro_Render_V3 && yarn compile:fast && yarn test --run
Expected: compile clean; all tests pass (142 prior + 1 new = 143).
In Incoming.java (near the friend constants): public static final int MarkConsoleReadEvent = 4085;
In Outgoing.java (near the friend composers): public final static int ConsoleReadReceiptComposer = 4086;
package com.eu.habbo.messages.outgoing.friends;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class ConsoleReadReceiptComposer extends MessageComposer {
private final int readerId;
public ConsoleReadReceiptComposer(int readerId) {
this.readerId = readerId;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.ConsoleReadReceiptComposer);
this.response.appendInt(this.readerId);
return this.response;
}
}
Step 3: Create the incoming handler
messages/incoming/friends/MarkConsoleReadEvent.java. The reader (me) tells the server it read peerId's messages; the server relays a receipt to peerId IF peerId is online AND a friend (anti-spoof). 1:1 only — peerId <= 0 (e.g. StaffChat = -1) is ignored.
package com.eu.habbo.messages.incoming.friends;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.friends.ConsoleReadReceiptComposer;
public class MarkConsoleReadEvent extends MessageHandler {
@Override
public void handle() throws Exception {
int peerId = this.packet.readInt();
Habbo me = this.client.getHabbo();
if (me == null || peerId <= 0) return;
if (me.getMessenger().getFriend(peerId) == null) return;
Habbo peer = Emulator.getGameServer().getGameClientManager().getHabbo(peerId);
if (peer == null || peer.getClient() == null) return;
peer.getClient().sendResponse(new ConsoleReadReceiptComposer(me.getHabboInfo().getId()));
}
}
Before writing, confirm me.getMessenger().getFriend(int) exists (it's used in FriendPrivateMessageEvent) and Emulator.getGameServer().getGameClientManager().getHabbo(int) (used in MessengerBuddy.onMessageReceived). Adapt + report if a signature differs.
Step 4: Register the handler
In PacketManager.registerFriends(): this.registerHandler(Incoming.MarkConsoleReadEvent, MarkConsoleReadEvent.class);
(registerFriends uses a wildcard import com.eu.habbo.messages.incoming.friends.*; — confirm with grep -n "incoming.friends" PacketManager.java; if explicit imports are used instead, add import ...MarkConsoleReadEvent;.)
Run: cd Nitro-V3 && yarn typecheck && yarn test --run
Expected: only the known pre-existing FloorplanCanvasSVG.tsx(143,20): TS2503; no new failures (3 known floorplan failures remain).
Step 7: Commit
cd Nitro-V3
git add src/api/friends/MessengerThreadChat.ts src/api/friends/MessengerThreadChat.test.ts src/api/friends/MessengerThread.ts src/api/friends/MessengerThread.test.ts
git -c user.name=simoleo89 -c user.email=simoleo89@users.noreply.github.com commit -m "feat(messenger): SENT/READ status on thread chats + mark-read helper"
Task P3-4: Client — wire receipts into useMessenger
In the top @nitrots/nitro-renderer import of useMessenger.ts, add ConsoleReadReceiptEvent and MarkConsoleReadComposer (alongside NewConsoleMessageEvent, SendMessageComposer as SendMessageComposerPacket). GetSessionDataManager is already imported.
Step 2: Send MarkConsoleRead when a conversation is focused
The existing useEffect([activeThreadId]) marks the active thread read locally. Extend it to also tell the peer. Replace that effect's body so that, after computing the active thread, it sends the composer for a real 1:1 participant:
Step 4: Handle the incoming receipt — mark own messages READ
Add a new event subscription (near the other useMessageEvent calls). parser.readerId is the friend who read MY messages; find the thread with that participant and mark my own messages READ:
Run: cd Nitro-V3 && yarn typecheck && yarn test --run
Expected: only the pre-existing typecheck error; no new test failures.
Step 6: Commit
cd Nitro-V3
git add src/hooks/friends/useMessenger.ts
git -c user.name=simoleo89 -c user.email=simoleo89@users.noreply.github.com commit -m "feat(messenger): send mark-read on focus, mark own messages read on receipt"
Step 1: Render the status indicator on own private-chat bubbles
In FriendsMessengerThreadGroup.tsx, the final return (...) renders the message row; the own-message avatar is gated by isOwnChat. MessengerThreadChat and MessengerGroupType are already imported. After the .messenger-message-time<Base> (inside .messenger-message-body), add a status indicator shown only for own 1:1 CHAT groups. Compute the last chat once and render:
Run: cd Nitro-V3 && yarn typecheck && yarn test --run
Expected: only the pre-existing typecheck error; no new test failures.
Step 4: Commit
cd Nitro-V3
git add src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadGroup.tsx src/css/friends/FriendsView.css
git -c user.name=simoleo89 -c user.email=simoleo89@users.noreply.github.com commit -m "feat(messenger): render sent/read checkmarks on own messages"
Task P3-6: Integration verification
Files: none (automated + manual; fix-ups only).
Step 1: Automated checks
cd Nitro_Render_V3 && yarn compile:fast && yarn test --run
cd Nitro-V3 && yarn typecheck && yarn test --run
cd Arcturus-Morningstar-Extended/Emulator && mvn -q clean package -DskipTests
Expected: renderer tests green (143); client typecheck shows only the pre-existing FloorplanCanvasSVG.tsx(143,20): TS2503, tests green except the 3 known floorplan failures (+ the new model cases pass); emulator BUILD SUCCESS.
Step 2: Live two-session manual test
Run the new jar + yarn start. Accounts A and B (friends), both online:
Sent (✓): A sends B a message while B's messenger thread with A is NOT focused → on A's side the message shows a single ✓.
Read (✓✓): B opens/focuses the conversation with A → A's message flips to ✓✓ (blue) live.
New message after read: A sends another message → shows ✓ again; when B (thread still focused) receives it, A flips to ✓✓ (the active-thread mark-read path).
Offline interplay (Phase 2): A messages B while B offline → A shows ✓; B logs in and opens the thread → A (if still online) sees ✓✓.
No receipts for non-1:1: opening the Staff Chat / a group chat thread does not produce errors and shows no checkmarks on those messages.
Privacy/abuse: a receipt only arrives for actual friends (the handler ignores non-friends and peerId <= 0).
No regressions: sending, receiving, offline markers (Phase 2), and groups (Phase 1) all still work.
No persistence / no login batch (see Design note): receipts are live-relay only; ✓✓ applies within the session the sender is online for.
2-state only:✓ (sent) and ✓✓ (read). No separate "delivered" state.
1:1 only: group chat, staff chat, and bots never produce receipts (peerId <= 0 and non-friends are ignored server-side; the client only renders checks on PRIVATE_CHAT CHAT groups).
Receipt marks ALL current own messages in the thread read (not a per-message timestamp diff) — correct for the 2-state model since a focus/read means everything visible is read; messages sent afterward start at ✓ again.
No renderer/client message-history persistence is added.
Do NOT push/merge automatically; the branch carries Phases 1–2 + the user's own parallel commits.
Messenger Phase 3 — Read Receipts (✓ / ✓✓) Implementation Plan
Goal: WhatsApp-style 2-state read receipts on your own sent messages —
✓sent,✓✓read — driven by a live relay (no persistence).Architecture: Two new CUSTOM packets. Client→server
MarkConsoleRead(peerId)is sent when you focus/read a conversation. The emulator relays it to the peer (if online and a friend) as server→clientConsoleReadReceipt(readerId). The recipient's client marks its own messages in that conversation as READ and renders✓✓.Design note — no DB, live-relay only (refinement of the spec): the spec proposed a
messenger_read_statetable + a login-time receipt batch. The Nitro client does NOT persist per-message history across sessions, so a persisted receipt would have no message to update on next login. Persistence is therefore omitted; receipts are a live in-session relay. This keeps Phase 3 simpler with no loss of user-visible behavior. (If cross-session receipts are ever wanted, they'd require persisting client-side message history first — out of scope.)Tech Stack: Arcturus (Java 21/Maven), Nitro_Render_V3 (TypeScript, Vitest), Nitro-V3 (React 19, Vitest).
Branches & rules
All repos on
feat/messenger-groups-receipts. Client commits usegit -c user.name=simoleo89 -c user.email=simoleo89@users.noreply.github.com. No Co-Authored-By / AI attribution. Emulator working tree has an UNRELATED modifiedsoundboard/SoundboardPlayEvent.java+ untracked jars — never stage those;git addonly the listed files.Header IDs (custom, verified free in all 4 files)
OutgoingHeader.MARK_CONSOLE_READIncoming.MarkConsoleReadEventIncomingHeader.CONSOLE_READ_RECEIPTOutgoing.ConsoleReadReceiptComposer(Renderer Outgoing N == Emulator Incoming N; Renderer Incoming N == Emulator Outgoing N — the verified convention.)
File map
Renderer (
Nitro_Render_V3/packages/communication/src/):messages/outgoing/OutgoingHeader.ts,messages/incoming/IncomingHeader.ts,NitroMessages.ts, the 3 friendlistindex.tsbarrels.messages/outgoing/friendlist/MarkConsoleReadComposer.tsmessages/incoming/friendlist/ConsoleReadReceiptEvent.tsmessages/parser/friendlist/ConsoleReadReceiptParser.tsmessages/parser/friendlist/__tests__/ConsoleReadReceiptParser.test.tsEmulator (
Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/):messages/incoming/Incoming.java,messages/outgoing/Outgoing.java,messages/PacketManager.javamessages/incoming/friends/MarkConsoleReadEvent.javamessages/outgoing/friends/ConsoleReadReceiptComposer.javaClient (
Nitro-V3/src/):api/friends/MessengerThreadChat.ts(+ test) andapi/friends/MessengerThread.ts(+ test)hooks/friends/useMessenger.tscomponents/friends/views/messenger/messenger-thread/FriendsMessengerThreadGroup.tsxsrc/css/friends/FriendsView.cssTask P3-1: Renderer — packets + registration + parser test
Files: see File map (renderer).
Create
packages/communication/src/messages/parser/friendlist/__tests__/ConsoleReadReceiptParser.test.ts(mirror the existing__tests__/FriendCategoryComposers.test.ts/ mentions parser test style with aTestWrapperoverBinaryReader/BinaryWriter):Run:
cd Nitro_Render_V3 && yarn test --run packages/communication/src/messages/parser/friendlist/__tests__/ConsoleReadReceiptParser.test.tsExpected: FAIL (module not found).packages/communication/src/messages/parser/friendlist/ConsoleReadReceiptParser.ts(mirrorNewConsoleMessageParser):packages/communication/src/messages/incoming/friendlist/ConsoleReadReceiptEvent.ts(mirrorNewConsoleMessageEvent):packages/communication/src/messages/outgoing/friendlist/MarkConsoleReadComposer.ts(mirrorSetRelationshipStatusComposer):In
OutgoingHeader.ts(near the friend headers):public static MARK_CONSOLE_READ = 4085;InIncomingHeader.ts(near the messenger headers):public static CONSOLE_READ_RECEIPT = 4086;Step 7: Barrel exports
messages/outgoing/friendlist/index.ts:export * from './MarkConsoleReadComposer';messages/incoming/friendlist/index.ts:export * from './ConsoleReadReceiptEvent';messages/parser/friendlist/index.ts:export * from './ConsoleReadReceiptParser';Step 8: Register in NitroMessages
In
NitroMessages.ts: add the two classes to the existing friendlist imports, then:in the events block (next to
this._events.set(IncomingHeader.MESSENGER_CHAT, NewConsoleMessageEvent);):this._events.set(IncomingHeader.CONSOLE_READ_RECEIPT, ConsoleReadReceiptEvent);in the composers block (next to
this._composers.set(OutgoingHeader.MESSENGER_CHAT, SendMessageComposer);):this._composers.set(OutgoingHeader.MARK_CONSOLE_READ, MarkConsoleReadComposer);Step 9: Compile + test
Run:
cd Nitro_Render_V3 && yarn compile:fast && yarn test --runExpected: compile clean; all tests pass (142 prior + 1 new = 143).Task P3-2: Emulator — handler + composer + registration
Files: see File map (emulator).
In
Incoming.java(near the friend constants):public static final int MarkConsoleReadEvent = 4085;InOutgoing.java(near the friend composers):public final static int ConsoleReadReceiptComposer = 4086;messages/outgoing/friends/ConsoleReadReceiptComposer.java:messages/incoming/friends/MarkConsoleReadEvent.java. The reader (me) tells the server it readpeerId's messages; the server relays a receipt topeerIdIFpeerIdis online AND a friend (anti-spoof). 1:1 only —peerId <= 0(e.g. StaffChat = -1) is ignored.Before writing, confirm
me.getMessenger().getFriend(int)exists (it's used inFriendPrivateMessageEvent) andEmulator.getGameServer().getGameClientManager().getHabbo(int)(used inMessengerBuddy.onMessageReceived). Adapt + report if a signature differs.In
PacketManager.registerFriends():this.registerHandler(Incoming.MarkConsoleReadEvent, MarkConsoleReadEvent.class);(registerFriendsuses a wildcardimport com.eu.habbo.messages.incoming.friends.*;— confirm withgrep -n "incoming.friends" PacketManager.java; if explicit imports are used instead, addimport ...MarkConsoleReadEvent;.)Run:
cd Arcturus-Morningstar-Extended/Emulator && mvn -q clean package -DskipTestsExpected: BUILD SUCCESS.git show --stat HEAD→ exactly 5 files (no soundboard, no jars).Task P3-3: Client — message status model (TDD)
Files:
Modify:
Nitro-V3/src/api/friends/MessengerThreadChat.ts+ TestMessengerThreadChat.test.ts(extend the existing test file from Phase 2)Modify:
Nitro-V3/src/api/friends/MessengerThread.ts+ TestNitro-V3/src/api/friends/MessengerThread.test.tsStep 1: Write the failing tests
Append to the existing
src/api/friends/MessengerThreadChat.test.ts:Create
src/api/friends/MessengerThread.test.ts:Run:
cd Nitro-V3 && yarn test --run src/api/friends/MessengerThreadChat.test.ts src/api/friends/MessengerThread.test.tsExpected: FAIL (SENT/READ/status/setStatus/setMessagesReadFromUser missing).In
MessengerThreadChat.ts, add the constants next to the existingCHAT/ROOM_INVITEstatics:Add the field next to the other private fields:
Add getter + setter (next to the
offlineDeliveredgetter):In
MessengerThread.ts, add (e.g. aftersetRead()):(
MessengerThreadChatis already imported in this file.)Step 5: Run, confirm PASS (Chat: 6 cases now; Thread: 1 case).
Step 6: typecheck + full suite
Run:
cd Nitro-V3 && yarn typecheck && yarn test --runExpected: only the known pre-existingFloorplanCanvasSVG.tsx(143,20): TS2503; no new failures (3 known floorplan failures remain).Task P3-4: Client — wire receipts into useMessenger
Files:
Modify:
Nitro-V3/src/hooks/friends/useMessenger.tsStep 1: Import the packets
In the top
@nitrots/nitro-rendererimport ofuseMessenger.ts, addConsoleReadReceiptEventandMarkConsoleReadComposer(alongsideNewConsoleMessageEvent,SendMessageComposer as SendMessageComposerPacket).GetSessionDataManageris already imported.The existing
useEffect([activeThreadId])marks the active thread read locally. Extend it to also tell the peer. Replace that effect's body so that, after computing the active thread, it sends the composer for a real 1:1 participant:In the
NewConsoleMessageEventhandler, aftersendMessage(...), notify the peer if this thread is the one currently open:Add a new event subscription (near the other
useMessageEventcalls).parser.readerIdis the friend who read MY messages; find the thread with that participant and mark my own messages READ:Run:
cd Nitro-V3 && yarn typecheck && yarn test --runExpected: only the pre-existing typecheck error; no new test failures.Task P3-5: Client — render ✓ / ✓✓ + CSS
Files:
Modify:
Nitro-V3/src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadGroup.tsxModify:
Nitro-V3/src/css/friends/FriendsView.cssStep 1: Render the status indicator on own private-chat bubbles
In
FriendsMessengerThreadGroup.tsx, the finalreturn (...)renders the message row; the own-message avatar is gated byisOwnChat.MessengerThreadChatandMessengerGroupTypeare already imported. After the.messenger-message-time<Base>(inside.messenger-message-body), add a status indicator shown only for own 1:1 CHAT groups. Compute the last chat once and render:(Insert this block immediately after the existing
messenger-message-timeline, still inside the.messenger-message-body<Base>.)Append to
src/css/friends/FriendsView.css:Run:
cd Nitro-V3 && yarn typecheck && yarn test --runExpected: only the pre-existing typecheck error; no new test failures.Task P3-6: Integration verification
Files: none (automated + manual; fix-ups only).
Expected: renderer tests green (143); client typecheck shows only the pre-existing
FloorplanCanvasSVG.tsx(143,20): TS2503, tests green except the 3 known floorplan failures (+ the new model cases pass); emulator BUILD SUCCESS.Run the new jar +
yarn start. Accounts A and B (friends), both online:✓.✓✓(blue) live.✓again; when B (thread still focused) receives it, A flips to✓✓(the active-thread mark-read path).✓; B logs in and opens the thread → A (if still online) sees✓✓.peerId <= 0).Scope boundaries
✓(sent) and✓✓(read). No separate "delivered" state.peerId <= 0and non-friends are ignored server-side; the client only renders checks onPRIVATE_CHATCHAT groups).✓again.