Implement Godot chat UI with DMs, world chat, real-time WebSocket receipt, and reporting (task 9b05df0c6869)
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/chat_ui.gd" id="1_cu"]
|
||||
|
||||
[node name="ChatUI" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_cu")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 32.0
|
||||
offset_top = 32.0
|
||||
offset_right = -32.0
|
||||
offset_bottom = -32.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="HeaderHBox" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 16
|
||||
|
||||
[node name="BackButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
||||
custom_minimum_size = Vector2(100, 36)
|
||||
layout_mode = 2
|
||||
text = "< Back"
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="VBoxContainer/HeaderHBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "Chat & Messaging"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="RefreshButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
||||
custom_minimum_size = Vector2(100, 36)
|
||||
layout_mode = 2
|
||||
text = "Refresh"
|
||||
|
||||
[node name="StatusMessageLabel" type="Label" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="TabContainer" type="TabContainer" parent="VBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
current_tab = 0
|
||||
|
||||
[node name="WorldChat" type="VBoxContainer" parent="VBoxContainer/TabContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
metadata/_tab_index = 0
|
||||
|
||||
[node name="ControlsHBox" type="HBoxContainer" parent="VBoxContainer/TabContainer/WorldChat"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="WorldPrefix" type="Label" parent="VBoxContainer/TabContainer/WorldChat/ControlsHBox"]
|
||||
layout_mode = 2
|
||||
text = "World ID:"
|
||||
|
||||
[node name="WorldIdEdit" type="LineEdit" parent="VBoxContainer/TabContainer/WorldChat/ControlsHBox"]
|
||||
custom_minimum_size = Vector2(120, 32)
|
||||
layout_mode = 2
|
||||
text = "1"
|
||||
|
||||
[node name="JoinChatButton" type="Button" parent="VBoxContainer/TabContainer/WorldChat/ControlsHBox"]
|
||||
custom_minimum_size = Vector2(110, 32)
|
||||
layout_mode = 2
|
||||
text = "Switch Channel"
|
||||
|
||||
[node name="Scroll" type="ScrollContainer" parent="VBoxContainer/TabContainer/WorldChat"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="MessagesList" type="VBoxContainer" parent="VBoxContainer/TabContainer/WorldChat/Scroll"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="SendHBox" type="HBoxContainer" parent="VBoxContainer/TabContainer/WorldChat"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="MessageInput" type="LineEdit" parent="VBoxContainer/TabContainer/WorldChat/SendHBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Type a message in world chat..."
|
||||
|
||||
[node name="SendButton" type="Button" parent="VBoxContainer/TabContainer/WorldChat/SendHBox"]
|
||||
custom_minimum_size = Vector2(100, 36)
|
||||
layout_mode = 2
|
||||
text = "Send"
|
||||
|
||||
[node name="DirectMessages" type="VBoxContainer" parent="VBoxContainer/TabContainer"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
metadata/_tab_index = 1
|
||||
|
||||
[node name="StartHBox" type="HBoxContainer" parent="VBoxContainer/TabContainer/DirectMessages"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="TargetUserEdit" type="LineEdit" parent="VBoxContainer/TabContainer/DirectMessages/StartHBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Enter User ID to start conversation..."
|
||||
|
||||
[node name="StartDmButton" type="Button" parent="VBoxContainer/TabContainer/DirectMessages/StartHBox"]
|
||||
custom_minimum_size = Vector2(140, 32)
|
||||
layout_mode = 2
|
||||
text = "Start Direct Chat"
|
||||
|
||||
[node name="HSplit" type="HSplitContainer" parent="VBoxContainer/TabContainer/DirectMessages"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
split_offset = 260
|
||||
|
||||
[node name="ConversationsVBox" type="VBoxContainer" parent="VBoxContainer/TabContainer/DirectMessages/HSplit"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 0.35
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="ConvHeader" type="Label" parent="VBoxContainer/TabContainer/DirectMessages/HSplit/ConversationsVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "Conversations"
|
||||
|
||||
[node name="Scroll" type="ScrollContainer" parent="VBoxContainer/TabContainer/DirectMessages/HSplit/ConversationsVBox"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ConvList" type="VBoxContainer" parent="VBoxContainer/TabContainer/DirectMessages/HSplit/ConversationsVBox/Scroll"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="ChatVBox" type="VBoxContainer" parent="VBoxContainer/TabContainer/DirectMessages/HSplit"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="DmHeaderLabel" type="Label" parent="VBoxContainer/TabContainer/DirectMessages/HSplit/ChatVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "Select a conversation"
|
||||
|
||||
[node name="Scroll" type="ScrollContainer" parent="VBoxContainer/TabContainer/DirectMessages/HSplit/ChatVBox"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="MessagesList" type="VBoxContainer" parent="VBoxContainer/TabContainer/DirectMessages/HSplit/ChatVBox/Scroll"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="SendHBox" type="HBoxContainer" parent="VBoxContainer/TabContainer/DirectMessages/HSplit/ChatVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="MessageInput" type="LineEdit" parent="VBoxContainer/TabContainer/DirectMessages/HSplit/ChatVBox/SendHBox"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Type a direct message..."
|
||||
|
||||
[node name="SendButton" type="Button" parent="VBoxContainer/TabContainer/DirectMessages/HSplit/ChatVBox/SendHBox"]
|
||||
custom_minimum_size = Vector2(100, 36)
|
||||
layout_mode = 2
|
||||
text = "Send"
|
||||
|
||||
[node name="ReportDialog" type="ConfirmationDialog" parent="."]
|
||||
title = "Report Inappropriate Message"
|
||||
size = Vector2i(420, 200)
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="ReportDialog"]
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = 412.0
|
||||
offset_bottom = 151.0
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="DetailsLabel" type="Label" parent="ReportDialog/VBox"]
|
||||
layout_mode = 2
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="ReasonPrompt" type="Label" parent="ReportDialog/VBox"]
|
||||
layout_mode = 2
|
||||
text = "Reason for report:"
|
||||
|
||||
[node name="ReasonEdit" type="LineEdit" parent="ReportDialog/VBox"]
|
||||
layout_mode = 2
|
||||
text = "Inappropriate content"
|
||||
@@ -0,0 +1,330 @@
|
||||
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()
|
||||
@@ -0,0 +1,105 @@
|
||||
extends SceneTree
|
||||
## Headless smoke test for ChatUI scene and script.
|
||||
|
||||
func _initialize() -> void:
|
||||
_run()
|
||||
|
||||
func _run() -> void:
|
||||
await process_frame
|
||||
|
||||
print("SMOKE: Starting Chat UI verification...")
|
||||
var scene_res := load("res://scenes/chat_ui.tscn")
|
||||
if scene_res == null:
|
||||
printerr("FAIL: Could not load res://scenes/chat_ui.tscn")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var ui: Control = scene_res.instantiate()
|
||||
if ui == null:
|
||||
printerr("FAIL: Could not instantiate ChatUI")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
root.add_child(ui)
|
||||
await process_frame
|
||||
|
||||
# Verify essential nodes
|
||||
if ui.world_messages_container == null:
|
||||
printerr("FAIL: world_messages_container is null")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
if ui.dm_messages_container == null:
|
||||
printerr("FAIL: dm_messages_container is null")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
if ui.conv_list_container == null:
|
||||
printerr("FAIL: conv_list_container is null")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
if ui.report_dialog == null:
|
||||
printerr("FAIL: report_dialog is null")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# Verify signal declaration
|
||||
if not ui.has_signal("back_requested"):
|
||||
printerr("FAIL: back_requested signal missing")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# Test message row creation with Report button
|
||||
var mock_msg := {
|
||||
"id": 101,
|
||||
"author": {"username": "ChattyUser"},
|
||||
"content": "Hello world! This is a test message."
|
||||
}
|
||||
ui._add_message_row(ui.world_messages_container, mock_msg, "world")
|
||||
|
||||
if ui.world_messages_container.get_child_count() == 0:
|
||||
printerr("FAIL: Message row was not added to world_messages_container")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var row: HBoxContainer = ui.world_messages_container.get_child(0)
|
||||
var found_report := false
|
||||
for child in row.get_children():
|
||||
if child is Button and child.text == "Report":
|
||||
found_report = true
|
||||
break
|
||||
|
||||
if not found_report:
|
||||
printerr("FAIL: Report button not found on message row")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("PASS: Message row with Report button verified.")
|
||||
|
||||
# Test report dialog preparation
|
||||
ui._open_report_dialog(101, "world", "Hello world! This is a test message.")
|
||||
if ui._reporting_message_id != 101 or ui._reporting_conv_type != "world":
|
||||
printerr("FAIL: Report dialog state not set correctly")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("PASS: Report dialog setup verified.")
|
||||
|
||||
# Test real-time WS chat message hook
|
||||
var mock_ws_event := {
|
||||
"id": 102,
|
||||
"author": {"username": "LiveUser"},
|
||||
"content": "Real-time incoming message!"
|
||||
}
|
||||
ui._on_ws_chat_message("world", ui._current_world_id, mock_ws_event)
|
||||
if ui.world_messages_container.get_child_count() < 2:
|
||||
printerr("FAIL: Live WS message was not appended to world chat")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("PASS: Real-time WS chat message append verified.")
|
||||
|
||||
ui.queue_free()
|
||||
print("SMOKE RESULT: PASS")
|
||||
quit(0)
|
||||
Reference in New Issue
Block a user