Messenger Phase 2 — Offline Messages Implementation Plan
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: Messages sent to an offline friend are stored and delivered on the recipient's next login, tagged "sent while offline".
Architecture: No new packets. The emulator stores send-to-offline in the existing messenger_offline table; on login it replays them through the existing FriendChatMessageComposer (extended with an optional extraData marker) so the client's existing NewConsoleMessageEvent path renders them. The client adds an offlineDelivered flag (derived from extraData === "offline") and a subtle marker in the thread.
All repos are already on feat/messenger-groups-receipts (continuing the messenger initiative). Continue committing there. Client commits use the house-rule author override git -c user.name=simoleo89 -c user.email=simoleo89@users.noreply.github.com. No Co-Authored-By / AI attribution anywhere.
Pre-flight: how messages flow today (read once)
FriendPrivateMessageEvent (incoming) → buddy.onMessageReceived(sender, message) which delivers via FriendChatMessageComposer ONLY if the recipient is online (else the message is silently dropped — that's the gap we close).
On login the client sends MessengerInitComposer → emulator RequestInitFriendsEvent sends MessengerInitComposer + the friend list.
FriendChatMessageComposer.composeInternal() appends: toId (the sender id shown to the recipient), message text, secondsSinceSent (= now − message.timestamp). For group chat (toId < 0) it appends an extra name/look/id string. For 1:1 it appends nothing after secondsSinceSent.
Client: useMessenger subscribes to NewConsoleMessageEvent and calls sendMessage(thread, parser.senderId, parser.messageText, parser.secondsSinceSent, parser.extraData), which stores extraData on the created MessengerThreadChat.
messenger_offline columns (verified): id (PK auto), user_id (recipient), user_from_id (sender), message varchar(500), sended_on int (unix).
Emulator has no unit tests; verification is mvn compile.
Step 1: Add a timestamp constructor to Message
Message has private final int timestamp; set to Emulator.getIntUnixTimestamp() in the existing constructor. Add a second constructor that accepts an explicit timestamp (needed so a replayed offline message reports its original age). Add it right after the existing constructor (after the block ending at the line this.timestamp = Emulator.getIntUnixTimestamp(); }):
public Message(int fromId, int toId, String message, int timestamp) {
this.fromId = fromId;
this.toId = toId;
this.message = message;
this.timestamp = timestamp;
}
Step 2: Add optional extraData to FriendChatMessageComposer
Add an extraData field + a 4-arg constructor, and append it for the 1:1 path. Replace the field/constructor region and the composeInternal tail.
Add the field next to the existing fields:
private String extraData = null;
Add this constructor after the existing FriendChatMessageComposer(Message message, int toId, int fromId):
public FriendChatMessageComposer(Message message, int toId, int fromId, String extraData) {
this.message = message;
this.toId = toId;
this.fromId = fromId;
this.extraData = extraData;
}
In composeInternal(), the existing if (this.toId < 0) { ...group chat... } block stays. Immediately AFTER that if block (before return this.response;), add an else if so 1:1 messages with a marker append it (online 1:1 messages pass extraData == null and append nothing — wire unchanged):
else if (this.extraData != null) {
this.response.appendString(this.extraData);
}
The result is:
if (this.toId < 0) // group chat
{
// ... existing group block unchanged ...
this.response.appendString(name + "/" + look + "/" + this.fromId);
}
else if (this.extraData != null) {
this.response.appendString(this.extraData);
}
return this.response;
Step 3: Compile
Run: cd Arcturus-Morningstar-Extended/Emulator && mvn -q compile
Expected: BUILD SUCCESS.
Step 4: Commit (only these 2 files)
cd Arcturus-Morningstar-Extended
git add Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Message.java Emulator/src/main/java/com/eu/habbo/messages/outgoing/friends/FriendChatMessageComposer.java
git commit -m "feat(messenger): Message timestamp ctor + optional extraData on chat composer"
Verify with git show --stat HEAD that ONLY these 2 files are committed (the working tree also has an unrelated soundboard/SoundboardPlayEvent.java modification and untracked jars — never stage those).
Task 2: Emulator — offline message store + deliver helpers in Messenger
Messenger already has static DB methods (e.g. unfriend) using Emulator.getDatabase().getDataSource().getConnection() with try-with-resources, and a LOGGER. Message and MessengerCategory are in the same package (no import needed). You WILL need imports for java.sql.ResultSet, java.util.ArrayList, java.util.List, com.eu.habbo.habbohotel.gameclients.GameClient, and com.eu.habbo.messages.outgoing.friends.FriendChatMessageComposer — check the existing import block and add whichever are missing.
Step 1: Add the cap constant
Near the other Messenger constants (e.g. by MAXIMUM_FRIENDS), add:
public static final int MAXIMUM_OFFLINE_MESSAGES = 200;
Step 2: Add addOfflineMessage
Stores one offline message for toId, evicting the oldest if the per-user inbox is at the cap. Add as a static method:
public static void addOfflineMessage(int fromId, int toId, String message) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
try (PreparedStatement count = connection.prepareStatement("SELECT COUNT(*) FROM messenger_offline WHERE user_id = ?")) {
count.setInt(1, toId);
try (ResultSet set = count.executeQuery()) {
if (set.next() && set.getInt(1) >= MAXIMUM_OFFLINE_MESSAGES) {
try (PreparedStatement delete = connection.prepareStatement("DELETE FROM messenger_offline WHERE user_id = ? ORDER BY id ASC LIMIT 1")) {
delete.setInt(1, toId);
delete.execute();
}
}
}
}
try (PreparedStatement insert = connection.prepareStatement("INSERT INTO messenger_offline (user_id, user_from_id, message, sended_on) VALUES (?, ?, ?, ?)")) {
insert.setInt(1, toId);
insert.setInt(2, fromId);
insert.setString(3, message);
insert.setInt(4, Emulator.getIntUnixTimestamp());
insert.execute();
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
Step 3: Add deliverOfflineMessages
Loads any stored messages for the logging-in user (oldest first), replays each through FriendChatMessageComposer with the "offline" marker and the original timestamp, then deletes the delivered rows.
public static void deliverOfflineMessages(GameClient client) {
if (client == null || client.getHabbo() == null) return;
int userId = client.getHabbo().getHabboInfo().getId();
List<Message> messages = new ArrayList<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT user_from_id, message, sended_on FROM messenger_offline WHERE user_id = ? ORDER BY sended_on ASC, id ASC")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
messages.add(new Message(set.getInt("user_from_id"), userId, set.getString("message"), set.getInt("sended_on")));
}
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
if (messages.isEmpty()) return;
for (Message message : messages) {
client.sendResponse(new FriendChatMessageComposer(message, message.getFromId(), message.getFromId(), "offline"));
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM messenger_offline WHERE user_id = ?")) {
statement.setInt(1, userId);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
}
Step 4: Compile
Run: cd Arcturus-Morningstar-Extended/Emulator && mvn -q compile
Expected: BUILD SUCCESS. If it fails on a missing symbol, add the missing import (see the list at the top of this task).
Step 5: Commit (only Messenger.java)
cd Arcturus-Morningstar-Extended
git add Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Messenger.java
git commit -m "feat(messenger): store + deliver offline messages (capped inbox)"
Step 1: Branch online vs offline in FriendPrivateMessageEvent
Currently the handler ends with buddy.onMessageReceived(this.client.getHabbo(), message);. Replace that single line with a branch: deliver if the recipient is online, otherwise store (word-filtered, matching the filtering the online path applies before sending). Add imports com.eu.habbo.habbohotel.messenger.Messenger and com.eu.habbo.habbohotel.modtool.WordFilter.
(Emulator is already imported in this file. userId is the recipient read at the top of handle().)
Step 2: Deliver offline messages on login in RequestInitFriendsEvent
After the existing this.client.sendResponses(messages);, add a call to deliver any stored offline messages (sent AFTER the friend list so the client's thread lookup can resolve the sender as a known friend). Add import com.eu.habbo.habbohotel.messenger.Messenger.
The method becomes:
public void handle() throws Exception {
ArrayList<ServerMessage> messages = new ArrayList<>();
messages.add(new MessengerInitComposer(this.client.getHabbo()).compose());
messages.addAll(FriendsComposer.getMessagesForBuddyList(this.client.getHabbo().getMessenger().getFriends().values()));
this.client.sendResponses(messages);
Messenger.deliverOfflineMessages(this.client);
}
import { describe, expect, it } from 'vitest';
import { MessengerThreadChat } from './MessengerThreadChat';
describe('MessengerThreadChat.offlineDelivered', () =>
{
it('is true for a CHAT message with extraData "offline"', () =>
{
const chat = new MessengerThreadChat(5, 'hello', 60, 'offline', MessengerThreadChat.CHAT);
expect(chat.offlineDelivered).toBe(true);
});
it('is false for a normal CHAT message with no extraData', () =>
{
const chat = new MessengerThreadChat(5, 'hello', 0, null, MessengerThreadChat.CHAT);
expect(chat.offlineDelivered).toBe(false);
});
it('is false when extraData is some other value (e.g. group chat data)', () =>
{
const chat = new MessengerThreadChat(5, 'hi', 0, 'Bob/figurestr/5', MessengerThreadChat.CHAT);
expect(chat.offlineDelivered).toBe(false);
});
it('is false for a non-CHAT type even if extraData is "offline"', () =>
{
const chat = new MessengerThreadChat(5, 'hi', 0, 'offline', MessengerThreadChat.ROOM_INVITE);
expect(chat.offlineDelivered).toBe(false);
});
});
Step 2: Run it, confirm FAIL
Run: cd Nitro-V3 && yarn test --run src/api/friends/MessengerThreadChat.test.ts
Expected: FAIL — offlineDelivered is not a function/getter.
Step 3: Add the getter
In MessengerThreadChat.ts, add after the extraData getter:
public get offlineDelivered(): boolean
{
return (this._type === MessengerThreadChat.CHAT) && (this._extraData === 'offline');
}
Step 4: Run the test, confirm PASS (4 cases).
Step 5: Type-check + full suite
Run: cd Nitro-V3 && yarn typecheck && yarn test --run
Expected: typecheck shows only the known pre-existing FloorplanCanvasSVG.tsx(143,20): TS2503; tests green except the 3 known pre-existing floorplan failures.
Step 6: Commit
cd Nitro-V3
git add src/api/friends/MessengerThreadChat.ts src/api/friends/MessengerThreadChat.test.ts
git -c user.name=simoleo89 -c user.email=simoleo89@users.noreply.github.com commit -m "feat(messenger): offlineDelivered flag on thread chat"
Task 5: Client — render the "sent while offline" marker
In public/configuration/UITexts.example, add a key near other messenger.* entries (match the file's JSON format — confirm by reading an existing messenger. line):
"messenger.offline.delivered": "Sent while you were offline",
(The user can localize the value in their live UITexts later. LocalizeText falls back to the key string if absent, so this never crashes.)
Step 2: Render the marker in the message bubble
In FriendsMessengerThreadGroup.tsx, the bubble maps group.chats. LocalizeText is already imported. In the NON-translation branch (the if(!chat.showTranslation) return), append the marker when chat.offlineDelivered. Replace:
(Leave the translation branch as-is — an offline message that is also auto-translated is a rare combination and the marker on the plain branch covers the normal case.)
Step 3: Add the marker style
Find the CSS file that styles the messenger thread (search for an existing class used here, e.g. messenger-message-bubble or messenger-message-time):
Run: grep -rl "messenger-message-bubble" Nitro-V3/src/css
Append to that file a subtle style:
If the grep finds no file (the classes are global/elsewhere), append the same rule to src/css/friends/FriendsView.css instead, and note that in your report.
Step 4: Type-check + full suite
Run: cd Nitro-V3 && yarn typecheck && yarn test --run
Expected: only the known pre-existing typecheck error; no new test failures.
Step 5: Commit
cd Nitro-V3
git add src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadGroup.tsx public/configuration/UITexts.example
# plus the CSS file you edited:
git add <the css file from Step 3>
git -c user.name=simoleo89 -c user.email=simoleo89@users.noreply.github.com commit -m "feat(messenger): show 'sent while offline' marker in thread"
cd Nitro-V3 && yarn typecheck && yarn test --run
cd Arcturus-Morningstar-Extended/Emulator && mvn -q clean package -DskipTests
Expected: client typecheck shows only the pre-existing FloorplanCanvasSVG.tsx(143,20): TS2503; client tests green except the 3 known floorplan failures (+4 new MessengerThreadChat cases passing); emulator BUILD SUCCESS. (No renderer change this phase.)
Step 2: Live two-session manual test
Run the new jar + yarn start. With two accounts A and B who are friends:
Store while offline: B logs out. A opens the messenger thread with B and sends a message. (A's client shows the sent message as normal.)
Deliver on login: B logs in → the message appears in B's thread with A, carrying the "Sent while you were offline" marker.
Order + multiple: A sends 3 messages while B is offline → on B's login all 3 appear in order, each marked, and the messenger_offline rows for B are gone (delivered + deleted):
SELECT * FROM messenger_offline WHERE user_id = <B id>; → 0 rows after login.
Online still instant: with both online, messages deliver immediately and show NO offline marker (wire unchanged for online 1:1).
Cap: (optional) inserting >200 stored messages for one user evicts the oldest.
No regressions: room invites, group/staff chat, and normal messaging still work.
Word filter: offline messages are word-filtered at store time (sender online) so the recipient sees filtered text, matching the online path. They are NOT written to chatlogs_private (offline messages were never chat-logged before; adding that is out of scope).
Displayed time: the thread shows the client receive-time (existing behavior); secondsSinceSent is sent but the bubble timestamp is local. The "sent while offline" marker is what signals the message is delayed; back-dating the bubble timestamp is out of scope.
Read receipts (Phase 3) will mark these delivered-on-login messages as read once the recipient opens the thread — not part of Phase 2.
Do NOT push/merge automatically; the branch already carries Phase 1 + the user's own feat(chat) commit.
Messenger Phase 2 — Offline Messages Implementation Plan
Goal: Messages sent to an offline friend are stored and delivered on the recipient's next login, tagged "sent while offline".
Architecture: No new packets. The emulator stores send-to-offline in the existing
messenger_offlinetable; on login it replays them through the existingFriendChatMessageComposer(extended with an optionalextraDatamarker) so the client's existingNewConsoleMessageEventpath renders them. The client adds anofflineDeliveredflag (derived fromextraData === "offline") and a subtle marker in the thread.Tech Stack: Arcturus (Java 21/Maven/HikariCP), Nitro-V3 (React 19, Vite, Vitest). No renderer change.
Branches
All repos are already on
feat/messenger-groups-receipts(continuing the messenger initiative). Continue committing there. Client commits use the house-rule author overridegit -c user.name=simoleo89 -c user.email=simoleo89@users.noreply.github.com. No Co-Authored-By / AI attribution anywhere.Pre-flight: how messages flow today (read once)
FriendPrivateMessageEvent(incoming) →buddy.onMessageReceived(sender, message)which delivers viaFriendChatMessageComposerONLY if the recipient is online (else the message is silently dropped — that's the gap we close).MessengerInitComposer→ emulatorRequestInitFriendsEventsendsMessengerInitComposer+ the friend list.FriendChatMessageComposer.composeInternal()appends:toId(the sender id shown to the recipient), message text,secondsSinceSent(= now − message.timestamp). For group chat (toId < 0) it appends an extraname/look/idstring. For 1:1 it appends nothing aftersecondsSinceSent.useMessengersubscribes toNewConsoleMessageEventand callssendMessage(thread, parser.senderId, parser.messageText, parser.secondsSinceSent, parser.extraData), which storesextraDataon the createdMessengerThreadChat.messenger_offlinecolumns (verified):id(PK auto),user_id(recipient),user_from_id(sender),messagevarchar(500),sended_onint (unix).File map
Emulator (
Arcturus-Morningstar-Extended/Emulator/src/main/java/com/eu/habbo/):habbohotel/messenger/Message.java— add a(fromId, toId, message, timestamp)constructor.messages/outgoing/friends/FriendChatMessageComposer.java— optionalextraDataappended for 1:1.habbohotel/messenger/Messenger.java—addOfflineMessage(...)+deliverOfflineMessages(...)+ cap constant.messages/incoming/friends/FriendPrivateMessageEvent.java— branch online vs offline.messages/incoming/friends/RequestInitFriendsEvent.java— deliver offline on login.Client (
Nitro-V3/src/):api/friends/MessengerThreadChat.ts—offlineDeliveredgetter.api/friends/MessengerThreadChat.test.ts— getter test.components/friends/views/messenger/messenger-thread/FriendsMessengerThreadGroup.tsx— render marker.public/configuration/UITexts.example— addmessenger.offline.deliveredtext key..messenger-offline-tagstyle.Task 1: Emulator —
Messagetimestamp constructor + composerextraDataFiles:
Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Message.javaEmulator/src/main/java/com/eu/habbo/messages/outgoing/friends/FriendChatMessageComposer.javaMessageMessagehasprivate final int timestamp;set toEmulator.getIntUnixTimestamp()in the existing constructor. Add a second constructor that accepts an explicit timestamp (needed so a replayed offline message reports its original age). Add it right after the existing constructor (after the block ending at the linethis.timestamp = Emulator.getIntUnixTimestamp(); }):extraDatatoFriendChatMessageComposerAdd an
extraDatafield + a 4-arg constructor, and append it for the 1:1 path. Replace the field/constructor region and thecomposeInternaltail.Add the field next to the existing fields:
Add this constructor after the existing
FriendChatMessageComposer(Message message, int toId, int fromId):In
composeInternal(), the existingif (this.toId < 0) { ...group chat... }block stays. Immediately AFTER thatifblock (beforereturn this.response;), add anelse ifso 1:1 messages with a marker append it (online 1:1 messages passextraData == nulland append nothing — wire unchanged):The result is:
Run:
cd Arcturus-Morningstar-Extended/Emulator && mvn -q compileExpected: BUILD SUCCESS.Verify with
git show --stat HEADthat ONLY these 2 files are committed (the working tree also has an unrelatedsoundboard/SoundboardPlayEvent.javamodification and untracked jars — never stage those).Task 2: Emulator — offline message store + deliver helpers in
MessengerFiles:
Emulator/src/main/java/com/eu/habbo/habbohotel/messenger/Messenger.javaMessengeralready has static DB methods (e.g.unfriend) usingEmulator.getDatabase().getDataSource().getConnection()with try-with-resources, and aLOGGER.MessageandMessengerCategoryare in the same package (no import needed). You WILL need imports forjava.sql.ResultSet,java.util.ArrayList,java.util.List,com.eu.habbo.habbohotel.gameclients.GameClient, andcom.eu.habbo.messages.outgoing.friends.FriendChatMessageComposer— check the existing import block and add whichever are missing.Near the other
Messengerconstants (e.g. byMAXIMUM_FRIENDS), add:addOfflineMessageStores one offline message for
toId, evicting the oldest if the per-user inbox is at the cap. Add as a static method:deliverOfflineMessagesLoads any stored messages for the logging-in user (oldest first), replays each through
FriendChatMessageComposerwith the"offline"marker and the original timestamp, then deletes the delivered rows.Run:
cd Arcturus-Morningstar-Extended/Emulator && mvn -q compileExpected: BUILD SUCCESS. If it fails on a missing symbol, add the missing import (see the list at the top of this task).Task 3: Emulator — wire send-to-offline + deliver-on-login
Files:
Modify:
Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendPrivateMessageEvent.javaModify:
Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/RequestInitFriendsEvent.javaStep 1: Branch online vs offline in
FriendPrivateMessageEventCurrently the handler ends with
buddy.onMessageReceived(this.client.getHabbo(), message);. Replace that single line with a branch: deliver if the recipient is online, otherwise store (word-filtered, matching the filtering the online path applies before sending). Add importscom.eu.habbo.habbohotel.messenger.Messengerandcom.eu.habbo.habbohotel.modtool.WordFilter.Replace:
with:
(
Emulatoris already imported in this file.userIdis the recipient read at the top ofhandle().)RequestInitFriendsEventAfter the existing
this.client.sendResponses(messages);, add a call to deliver any stored offline messages (sent AFTER the friend list so the client's thread lookup can resolve the sender as a known friend). Add importcom.eu.habbo.habbohotel.messenger.Messenger.The method becomes:
Run:
cd Arcturus-Morningstar-Extended/Emulator && mvn -q clean package -DskipTestsExpected: BUILD SUCCESS.Verify
git show --stat HEADshows exactly 2 files.Task 4: Client —
offlineDeliveredgetter onMessengerThreadChat(TDD)Files:
Nitro-V3/src/api/friends/MessengerThreadChat.tsNitro-V3/src/api/friends/MessengerThreadChat.test.tsMessengerThreadChatalready stores_extraDataand_type, withCHAT = 0. The emulator sendsextraData === "offline"only for replayed 1:1 messages.Create
src/api/friends/MessengerThreadChat.test.ts:Run:
cd Nitro-V3 && yarn test --run src/api/friends/MessengerThreadChat.test.tsExpected: FAIL —offlineDeliveredis not a function/getter.In
MessengerThreadChat.ts, add after theextraDatagetter:Step 4: Run the test, confirm PASS (4 cases).
Step 5: Type-check + full suite
Run:
cd Nitro-V3 && yarn typecheck && yarn test --runExpected: typecheck shows only the known pre-existingFloorplanCanvasSVG.tsx(143,20): TS2503; tests green except the 3 known pre-existing floorplan failures.Task 5: Client — render the "sent while offline" marker
Files:
Modify:
Nitro-V3/src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadGroup.tsxModify:
Nitro-V3/public/configuration/UITexts.exampleModify: a messenger CSS file (see Step 3)
Step 1: Add the localization key
In
public/configuration/UITexts.example, add a key near othermessenger.*entries (match the file's JSON format — confirm by reading an existingmessenger.line):(The user can localize the value in their live
UITextslater.LocalizeTextfalls back to the key string if absent, so this never crashes.)In
FriendsMessengerThreadGroup.tsx, the bubble mapsgroup.chats.LocalizeTextis already imported. In the NON-translation branch (theif(!chat.showTranslation)return), append the marker whenchat.offlineDelivered. Replace:with:
(Leave the translation branch as-is — an offline message that is also auto-translated is a rare combination and the marker on the plain branch covers the normal case.)
Find the CSS file that styles the messenger thread (search for an existing class used here, e.g.
messenger-message-bubbleormessenger-message-time): Run:grep -rl "messenger-message-bubble" Nitro-V3/src/cssAppend to that file a subtle style:If the grep finds no file (the classes are global/elsewhere), append the same rule to
src/css/friends/FriendsView.cssinstead, and note that in your report.Run:
cd Nitro-V3 && yarn typecheck && yarn test --runExpected: only the known pre-existing typecheck error; no new test failures.Task 6: Integration verification
Files: none (manual + automated checks; fix-up commits only).
Expected: client typecheck shows only the pre-existing
FloorplanCanvasSVG.tsx(143,20): TS2503; client tests green except the 3 known floorplan failures (+4 new MessengerThreadChat cases passing); emulator BUILD SUCCESS. (No renderer change this phase.)Run the new jar +
yarn start. With two accounts A and B who are friends:messenger_offlinerows for B are gone (delivered + deleted):SELECT * FROM messenger_offline WHERE user_id = <B id>;→ 0 rows after login.Notes / scope boundaries
chatlogs_private(offline messages were never chat-logged before; adding that is out of scope).secondsSinceSentis sent but the bubble timestamp is local. The "sent while offline" marker is what signals the message is delayed; back-dating the bubble timestamp is out of scope.feat(chat)commit.