class_name KeetChat
extends Node
## Chat messaging for a single Keet room via UDS push events.
## No polling — uses subscribe/unsubscribe for live message delivery.
## Supports pagination via load_older() for scroll-back.
signal message_received(message: Dictionary)
signal messages_loaded(messages: Array)
signal older_messages_loaded(messages: Array)
signal message_sent
signal no_more_history
signal error_occurred(message: String)
var client: KeetUdsClient
var room_id: String
var message_limit: int = 50
var _known_ids: Dictionary = {}
var _high_seq: int = -1
var _low_seq: int = -1 # Lowest seq seen — used for pagination
var _loading_older: bool = false
var _has_more: bool = true # False when we've reached the beginning
func _ready() -> void:
pass
## Start watching a room for new messages via push subscription.
func watch(rid: String) -> void:
if room_id != "" and room_id != rid:
unwatch()
room_id = rid
_high_seq = -1
_low_seq = -1
_known_ids.clear()
_loading_older = false
_has_more = true
_require_client()
# Connect to push events if not already connected
if not client.event_received.is_connected(_on_push_event):
client.event_received.connect(_on_push_event)
# Subscribe to room via daemon
client.call_rpc("subscribe", {"roomId": room_id}, func(_result, err):
if err != null:
error_occurred.emit("subscribe: %s" % str(err))
)
## Stop watching for messages.
func unwatch() -> void:
if room_id != "" and client != null and client.is_connected:
client.call_rpc("unsubscribe", {"roomId": room_id})
room_id = ""
_high_seq = -1
_low_seq = -1
_known_ids.clear()
_loading_older = false
_has_more = true
## Send a text message to the current room.
func send(text: String, callback: Callable = Callable()) -> void:
if room_id == "":
error_occurred.emit("No room_id set")
return
_require_client()
client.call_rpc("send", {"roomId": room_id, "text": text}, func(result, err):
if err != null:
error_occurred.emit("send: %s" % str(err))
return
message_sent.emit()
if callback.is_valid():
callback.call(result)
)
## Load message history for the current room.
func load_history(limit: int = 0, callback: Callable = Callable()) -> void:
if room_id == "":
error_occurred.emit("No room_id set")
return
_require_client()
var lim := limit if limit > 0 else message_limit
client.call_rpc("read", {"roomId": room_id, "limit": lim}, func(result, err):
if err != null:
error_occurred.emit("load_history: %s" % str(err))
return
var msgs: Array = result if result is Array else []
_track_seqs(msgs)
messages_loaded.emit(msgs)
if callback.is_valid():
callback.call(msgs)
)
## Load older messages (for scroll-back pagination).
## Uses _low_seq - 1 as lte to fetch messages before the oldest loaded.
func load_older(limit: int = 0, callback: Callable = Callable()) -> void:
if room_id == "":
return
if _loading_older or not _has_more:
if not _has_more:
no_more_history.emit()
return
if _low_seq <= 0:
_has_more = false
no_more_history.emit()
return
_loading_older = true
_require_client()
var lim := limit if limit > 0 else message_limit
var params: Dictionary = {"roomId": room_id, "limit": lim, "lte": _low_seq - 1}
client.call_rpc("read", params, func(result, err):
_loading_older = false
if err != null:
error_occurred.emit("load_older: %s" % str(err))
return
var msgs: Array = result if result is Array else []
if msgs.is_empty():
_has_more = false
no_more_history.emit()
else:
_track_seqs(msgs)
older_messages_loaded.emit(msgs)
if callback.is_valid():
callback.call(msgs)
)
## Edit a message.
func edit(message_id: Variant, text: String, callback: Callable = Callable()) -> void:
_require_client()
client.call_rpc("msg.edit", {"roomId": room_id, "messageId": message_id, "text": text}, func(result, err):
if err != null:
error_occurred.emit("edit: %s" % str(err))
return
if callback.is_valid():
callback.call(result)
)
## Delete a message.
func delete(message_id: Variant, callback: Callable = Callable()) -> void:
_require_client()
client.call_rpc("msg.delete", {"roomId": room_id, "messageId": message_id}, func(result, err):
if err != null:
error_occurred.emit("delete: %s" % str(err))
return
if callback.is_valid():
callback.call(result)
)
## React to a message.
func react(message_id: Variant, emoji: String, callback: Callable = Callable()) -> void:
_require_client()
client.call_rpc("react", {"roomId": room_id, "messageId": message_id, "emoji": emoji}, func(result, err):
if err != null:
error_occurred.emit("react: %s" % str(err))
return
if callback.is_valid():
callback.call(result)
)
## Search messages in the current room.
func search(query: String, callback: Callable = Callable()) -> void:
_require_client()
client.call_rpc("msg.search", {"roomId": room_id, "query": query}, func(result, err):
if err != null:
error_occurred.emit("search: %s" % str(err))
return
if callback.is_valid():
callback.call(result)
)
func _track_seqs(msgs: Array) -> void:
for msg: Variant in msgs:
if msg is Dictionary:
var mid: String = _msg_id(msg)
_known_ids[mid] = true
var seq: int = int(msg.get("seq", -1))
if seq >= 0:
if seq > _high_seq or _high_seq == -1:
_high_seq = seq
if seq < _low_seq or _low_seq == -1:
_low_seq = seq
func _on_push_event(event_name: String, data: Dictionary) -> void:
if event_name != "message":
return
# Filter by room - push events include roomId
var event_room: String = data.get("roomId", "")
if event_room != "" and event_room != room_id:
return
var mid: String = _msg_id(data)
if _known_ids.has(mid):
return
_known_ids[mid] = true
var seq: int = int(data.get("seq", -1))
if seq > _high_seq:
_high_seq = seq
message_received.emit(data)
func _msg_id(msg: Dictionary) -> String:
var id: Variant = msg.get("id", null)
if id is Dictionary:
return "%s:%d" % [id.get("deviceId", ""), int(id.get("seq", 0))]
if id is String:
return id
return "%s:%d" % [msg.get("memberId", ""), int(msg.get("seq", 0))]
func _require_client() -> void:
if client == null:
push_error("KeetChat: client not set")