OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / addons / keet_social / keet_chat.gd

231 lines · gd ref:
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")