extends Control class_name ChatUI ## Chat UI for Direct Messages, World/Room Chat, and Message Reporting. ## ## Features: ## - DM Conversations list + message thread ## - In-experience World/Room chat thread ## - Real-time WebSocket event dispatch for new messages ## - Inline "Report" button on each message with reporting popup ## - Rate limiting and error feedback display signal back_requested @onready var back_button: Button = $VBoxContainer/HeaderHBox/BackButton @onready var title_label: Label = $VBoxContainer/HeaderHBox/TitleLabel @onready var refresh_button: Button = $VBoxContainer/HeaderHBox/RefreshButton @onready var status_label: Label = $VBoxContainer/StatusMessageLabel @onready var tab_container: TabContainer = $VBoxContainer/TabContainer # World chat controls @onready var world_id_edit: LineEdit = $VBoxContainer/TabContainer/WorldChat/ControlsHBox/WorldIdEdit @onready var join_world_chat_btn: Button = $VBoxContainer/TabContainer/WorldChat/ControlsHBox/JoinChatButton @onready var world_messages_container: VBoxContainer = $VBoxContainer/TabContainer/WorldChat/Scroll/MessagesList @onready var world_msg_input: LineEdit = $VBoxContainer/TabContainer/WorldChat/SendHBox/MessageInput @onready var world_send_btn: Button = $VBoxContainer/TabContainer/WorldChat/SendHBox/SendButton # DM controls @onready var target_user_edit: LineEdit = $VBoxContainer/TabContainer/DirectMessages/StartHBox/TargetUserEdit @onready var start_dm_btn: Button = $VBoxContainer/TabContainer/DirectMessages/StartHBox/StartDmButton @onready var conv_list_container: VBoxContainer = $VBoxContainer/TabContainer/DirectMessages/HSplit/ConversationsVBox/Scroll/ConvList @onready var dm_header_label: Label = $VBoxContainer/TabContainer/DirectMessages/HSplit/ChatVBox/DmHeaderLabel @onready var dm_messages_container: VBoxContainer = $VBoxContainer/TabContainer/DirectMessages/HSplit/ChatVBox/Scroll/MessagesList @onready var dm_msg_input: LineEdit = $VBoxContainer/TabContainer/DirectMessages/HSplit/ChatVBox/SendHBox/MessageInput @onready var dm_send_btn: Button = $VBoxContainer/TabContainer/DirectMessages/HSplit/ChatVBox/SendHBox/SendButton # Report Popup @onready var report_dialog: ConfirmationDialog = $ReportDialog @onready var report_reason_edit: LineEdit = $ReportDialog/VBox/ReasonEdit @onready var report_details_label: Label = $ReportDialog/VBox/DetailsLabel var _api_client: Node = null var _current_world_id: int = 1 var _active_conversation_id: int = 0 var _reporting_message_id: int = 0 var _reporting_conv_type: String = "" var _reporting_context: String = "" func _ready() -> void: _api_client = _get_api() _wire_signals() if _api_client != null and _api_client.has_token(): refresh_world_chat() refresh_dm_conversations() func _get_api() -> Node: if has_node("/root/ApiClient"): return get_node("/root/ApiClient") return null func _wire_signals() -> void: if back_button: back_button.pressed.connect(_on_back_pressed) if refresh_button: refresh_button.pressed.connect(refresh_active_tab) # World chat if join_world_chat_btn: join_world_chat_btn.pressed.connect(_on_join_world_chat) if world_send_btn: world_send_btn.pressed.connect(_on_send_world_message) if world_msg_input: world_msg_input.text_submitted.connect(func(_t): _on_send_world_message()) # DM chat if start_dm_btn: start_dm_btn.pressed.connect(_on_start_dm_pressed) if dm_send_btn: dm_send_btn.pressed.connect(_on_send_dm_message) if dm_msg_input: dm_msg_input.text_submitted.connect(func(_t): _on_send_dm_message()) # Report dialog if report_dialog: report_dialog.confirmed.connect(_on_report_confirmed) # WebSocket real-time hook if _api_client != null and _api_client.ws != null: if not _api_client.ws.chat_message_received.is_connected(_on_ws_chat_message): _api_client.ws.chat_message_received.connect(_on_ws_chat_message) func _on_back_pressed() -> void: back_requested.emit() if get_tree().current_scene == self: get_tree().change_scene_to_file("res://scenes/main_menu.tscn") func show_message(text: String, is_error: bool = false) -> void: if status_label: status_label.text = text status_label.modulate = Color(1.0, 0.4, 0.4) if is_error else Color(0.4, 1.0, 0.4) func refresh_active_tab() -> void: if tab_container == null: return if tab_container.current_tab == 0: refresh_world_chat() else: refresh_dm_conversations() if _active_conversation_id > 0: refresh_dm_messages(_active_conversation_id) # ── World Chat ── func _on_join_world_chat() -> void: var wid_str := world_id_edit.text.strip_edges() if not wid_str.is_valid_int(): show_message("Enter a valid integer world ID.", true) return _current_world_id = int(wid_str) if _api_client != null and _api_client.ws != null: _api_client.ws.subscribe_world(_current_world_id) show_message("Joined world %d chat." % _current_world_id) refresh_world_chat() func refresh_world_chat() -> void: if _api_client == null or not _api_client.has_token(): return var res: Dictionary = await _api_client.get_world_chat_messages(_current_world_id) _clear_children(world_messages_container) if not res.ok: show_message("Failed to load world chat: " + res.detail, true) return var msgs: Array = [] if res.data is Dictionary and res.data.has("messages"): msgs = res.data["messages"] elif res.data is Array: msgs = res.data for m in msgs: _add_message_row(world_messages_container, m, "world") func _on_send_world_message() -> void: var content := world_msg_input.text.strip_edges() if content == "": return var res: Dictionary = await _api_client.send_world_chat_message(_current_world_id, content) if res.ok: world_msg_input.text = "" refresh_world_chat() else: show_message("Send failed: " + res.detail, true) # ── Direct Messages (DMs) ── func refresh_dm_conversations() -> void: if _api_client == null or not _api_client.has_token(): return var res: Dictionary = await _api_client.get_dm_conversations() _clear_children(conv_list_container) if not res.ok: show_message("Failed to load DMs: " + res.detail, true) return var convs: Array = [] if res.data is Dictionary and res.data.has("conversations"): convs = res.data["conversations"] elif res.data is Array: convs = res.data if convs.is_empty(): var empty_lbl := Label.new() empty_lbl.text = "No conversations yet." empty_lbl.modulate = Color(0.6, 0.6, 0.6) conv_list_container.add_child(empty_lbl) return for c in convs: var cid: int = int(c.get("id", 0)) var other_name: String = "Conversation #%d" % cid if c.has("other_user") and c["other_user"] is Dictionary: other_name = str(c["other_user"].get("username", other_name)) var btn := Button.new() btn.text = other_name btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL btn.pressed.connect(func(): _select_dm_conversation(cid, other_name)) conv_list_container.add_child(btn) func _select_dm_conversation(cid: int, name_hint: String) -> void: _active_conversation_id = cid if dm_header_label: dm_header_label.text = "Chat with " + name_hint if _api_client != null and _api_client.ws != null: _api_client.ws.subscribe_dm(cid) refresh_dm_messages(cid) func refresh_dm_messages(cid: int) -> void: if _api_client == null or cid <= 0: return var res: Dictionary = await _api_client.get_dm_messages(cid) _clear_children(dm_messages_container) if not res.ok: show_message("Failed to load messages: " + res.detail, true) return var msgs: Array = [] if res.data is Dictionary and res.data.has("messages"): msgs = res.data["messages"] elif res.data is Array: msgs = res.data for m in msgs: _add_message_row(dm_messages_container, m, "dm") func _on_start_dm_pressed() -> void: var target_text := target_user_edit.text.strip_edges() if not target_text.is_valid_int(): show_message("Enter a valid integer user ID.", true) return var target_id := int(target_text) var res: Dictionary = await _api_client.start_dm_conversation(target_id) if res.ok and res.data is Dictionary: var cid: int = int(res.data.get("id", 0)) target_user_edit.text = "" show_message("Conversation started!") await refresh_dm_conversations() _select_dm_conversation(cid, "User #%d" % target_id) else: show_message("Failed to start conversation: " + res.detail, true) func _on_send_dm_message() -> void: if _active_conversation_id <= 0: show_message("Select or start a conversation first.", true) return var content := dm_msg_input.text.strip_edges() if content == "": return var res: Dictionary = await _api_client.send_dm_message(_active_conversation_id, content) if res.ok: dm_msg_input.text = "" refresh_dm_messages(_active_conversation_id) else: show_message("Send failed: " + res.detail, true) # ── Message Row & Reporting ── func _add_message_row(container: VBoxContainer, msg_data: Dictionary, conv_type: String) -> void: var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 8) var mid: int = int(msg_data.get("id", 0)) var author: String = "User #%s" % str(msg_data.get("author_id", msg_data.get("sender_id", "?"))) if msg_data.has("author") and msg_data["author"] is Dictionary: author = str(msg_data["author"].get("username", author)) elif msg_data.has("sender") and msg_data["sender"] is Dictionary: author = str(msg_data["sender"].get("username", author)) var content: String = str(msg_data.get("content", "")) var label := Label.new() label.text = "[%s]: %s" % [author, content] label.size_flags_horizontal = Control.SIZE_EXPAND_FILL label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART hbox.add_child(label) var report_btn := Button.new() report_btn.text = "Report" report_btn.pressed.connect(func(): _open_report_dialog(mid, conv_type, content)) hbox.add_child(report_btn) container.add_child(hbox) func _open_report_dialog(msg_id: int, conv_type: String, context_text: String) -> void: _reporting_message_id = msg_id _reporting_conv_type = conv_type _reporting_context = context_text if report_details_label: report_details_label.text = "Reporting message: \"%s\"" % context_text.left(60) if report_reason_edit: report_reason_edit.text = "Inappropriate content" if report_dialog: report_dialog.popup_centered() func _on_report_confirmed() -> void: if _reporting_message_id <= 0 or _api_client == null: return var reason := report_reason_edit.text.strip_edges() if report_reason_edit else "Inappropriate content" var res: Dictionary = await _api_client.report_message(_reporting_message_id, _reporting_conv_type, reason, _reporting_context) if res.ok: show_message("Message reported to moderation queue. Thank you!") else: show_message("Failed to report message: " + res.detail, true) # ── Real-Time WS Hook ── func _on_ws_chat_message(channel: String, id: int, data: Dictionary) -> void: if channel == "world" and id == _current_world_id: _add_message_row(world_messages_container, data, "world") elif channel == "dm" and id == _active_conversation_id: _add_message_row(dm_messages_container, data, "dm") func _clear_children(node: Node) -> void: if node == null: return for c in node.get_children(): c.queue_free()