class_name LobbyChat extends RefCounted var lobby: Control const GLOBAL_CHAT_ROOM := "social_global" var _chat_channel = null var _chat_messages: Array = [] var _active_chat_context: String = "global" var _dm_tabs: Dictionary = {} var _dm_messages: Dictionary = {} var _chat_config: Dictionary = {"prefix": "", "max_messages": 50, "max_age_days": 0} func _init(p_lobby: Control): lobby = p_lobby if lobby.chat_send_btn: lobby.chat_send_btn.pressed.connect(_on_chat_send_pressed) if lobby.chat_input: lobby.chat_input.text_submitted.connect(func(_t): _on_chat_send_pressed()) lobby.chat_input.text_changed.connect(_on_chat_input_changed) _setup_friend_suggest_ui() func join_global_chat() -> void: if _chat_channel != null: return var socket = NakamaManager.socket if not socket or not socket.is_connected_to_host(): push_warning("[Chat] Socket not connected, skipping chat join.") return var result = await socket.join_chat_async(GLOBAL_CHAT_ROOM, NakamaSocket.ChannelType.Room, true, false) if result.is_exception(): push_warning("[Chat] Failed to join global chat: " + result.get_exception().message) return _chat_channel = result print("[Chat] Joined global channel: ", _chat_channel.id) if not socket.received_channel_message.is_connected(_on_chat_message_received): socket.received_channel_message.connect(_on_chat_message_received) # Fetch admin chat config (prefix, max_messages, etc.) if BackendService.has_method("admin_get_chat_config"): var cfg_res = await BackendService.admin_get_chat_config() if cfg_res.has("config"): _chat_config = cfg_res["config"] _chat_messages.clear() var limit: int = _chat_config.get("max_messages", 50) var history_result = await NakamaManager.client.list_channel_messages_async(NakamaManager.session, _chat_channel.id, limit, false) if not history_result.is_exception() and history_result.messages: var msgs = history_result.messages.duplicate() msgs.reverse() for msg in msgs: _add_chat_message(msg, false) # Inject admin system prefix if configured var prefix: String = _chat_config.get("prefix", "") if not prefix.is_empty(): _chat_messages.insert(0, { "sender": "SYSTEM", "content": prefix, "ts": _get_local_time(), "date": Time.get_date_string_from_system() }) _trim_old_messages() _refresh_chat_display() func _on_chat_message_received(message) -> void: if _chat_channel == null or message.channel_id != _chat_channel.id: return if NakamaManager.session and message.sender_id == NakamaManager.session.user_id: return _add_chat_message(message, true) func _add_chat_message(message, refresh_display: bool) -> void: var text: String = "" var parsed = JSON.parse_string(message.content) if typeof(parsed) == TYPE_DICTIONARY: text = parsed.get("msg", message.content) else: text = message.content var sender: String = message.username if sender.is_empty() and message.sender_id: sender = message.sender_id.substr(0, 8) elif sender.is_empty(): sender = "Unknown" var ts_str: String = _format_nakama_time(message.create_time) _chat_messages.append({ "sender": sender, "content": text, "ts": ts_str, "date": message.create_time.substr(0, 10) if message.create_time else Time.get_date_string_from_system() }) if refresh_display: _trim_old_messages() _refresh_chat_display() func _on_chat_send_pressed() -> void: if lobby.chat_input == null or lobby.chat_input.text.strip_edges().is_empty(): return var text = lobby.chat_input.text.strip_edges() lobby.chat_input.text = "" lobby.chat_input.grab_focus() if lobby._friend_suggest_panel and lobby._friend_suggest_panel.visible: lobby._friend_suggest_panel.hide() if text.begins_with("@"): var space_idx = text.find(" ") var target_username = "" var dm_message = "" if space_idx > 0: target_username = text.substr(1, space_idx - 1) dm_message = text.substr(space_idx + 1).strip_edges() else: target_username = text.substr(1).strip_edges() if not target_username.is_empty(): var target_friend = null for f in FriendManager.friends: if f.get("username", "") == target_username: target_friend = f break if target_friend: var user_id = target_friend.get("user_id", "") var username = target_friend.get("username", "") _open_dm_tab(user_id, username) if not dm_message.is_empty(): _send_dm_message(user_id, dm_message) return elif text.begins_with("@"): _inject_local_message("User %s not found in friends." % target_username) return if _active_chat_context != "global": if text == "/clear": _dm_messages[_active_chat_context] = [] _refresh_chat_display() return _send_dm_message(_active_chat_context, text) return if text == "/clear": var is_admin = await AdminManager._check_admin_status() if is_admin: _chat_messages.clear() _refresh_chat_display() if _chat_channel and NakamaManager.session and NakamaManager.client: var payload = JSON.stringify({"channel_id": _chat_channel.id}) var rpc_result = await BackendService.admin_clear_global_chat(payload) if rpc_result.get("success", false) == false: push_warning("[Chat] admin_clear_global_chat RPC failed: " + str(rpc_result.get("message", ""))) else: _inject_local_message("[SYSTEM] : Global chat cleared by admin.") else: _inject_local_message("[SYSTEM] : Unknown cmd, /clear only usable on DM between user.") return _inject_local_message(text) var socket = NakamaManager.socket if not socket or _chat_channel == null: return var content := {"msg": text} var result = await socket.write_chat_message_async(_chat_channel.id, content) if result.is_exception(): push_warning("[Chat] Failed to send message: " + result.get_exception().message) func _send_dm_message(user_id: String, text: String) -> void: var sent = await FriendManager.send_dm(user_id, text) if sent: if not _dm_messages.has(user_id): _dm_messages[user_id] = [] _dm_messages[user_id].append({"sender": "You", "content": text, "ts": _get_local_time()}) if _active_chat_context == user_id: _refresh_chat_display() func on_lobby_dm_received(from_user_id: String, from_name: String, message: String) -> void: if not _dm_messages.has(from_user_id): _dm_messages[from_user_id] = [] _dm_messages[from_user_id].append({"sender": from_name, "content": message, "ts": _get_local_time()}) if not _dm_tabs.has(from_user_id): _create_dm_tab(from_user_id, from_name) if _active_chat_context == from_user_id: _refresh_chat_display() func _open_dm_tab(user_id: String, username: String) -> void: if not _dm_tabs.has(user_id): _create_dm_tab(user_id, username) _dm_messages[user_id] = [] switch_chat_tab(user_id) var history = await FriendManager.get_dm_history(user_id) var my_id = NakamaManager.session.user_id if NakamaManager.session else "" for entry in history: var is_self = entry.get("from") == my_id var sender_name = "You" if is_self else username var ts = _format_nakama_time(entry.get("create_time", "")) _dm_messages[user_id].append({"sender": sender_name, "content": entry.get("msg", ""), "ts": ts}) if _active_chat_context == user_id: _refresh_chat_display() else: switch_chat_tab(user_id) func _create_dm_tab(user_id: String, username: String) -> void: var tabs_container = lobby.get_node_or_null("%ChatTabsContainer") var template = lobby.get_node_or_null("%DMTabTemplate") if not tabs_container or not template: return var hbox = template.duplicate() hbox.visible = true var btn = hbox.get_node("DMTabBtn") if btn: btn.text = username btn.pressed.connect(func(): switch_chat_tab(user_id)) var close_btn = hbox.get_node("DMTabCloseBtn") if close_btn: close_btn.pressed.connect(func(): _close_dm_tab(user_id)) tabs_container.add_child(hbox) _dm_tabs[user_id] = hbox func _close_dm_tab(user_id: String) -> void: if _dm_tabs.has(user_id): var tab = _dm_tabs[user_id] tab.queue_free() _dm_tabs.erase(user_id) if _active_chat_context == user_id: switch_chat_tab("global") func switch_chat_tab(context_id: String) -> void: _active_chat_context = context_id _refresh_chat_display() var tabs_container = lobby.get_node_or_null("%ChatTabsContainer") if tabs_container: var global_btn = lobby.get_node_or_null("%GlobalChatTabBtn") if global_btn: global_btn.modulate = Color(1.0, 1.0, 1.0) if context_id == "global" else Color(0.6, 0.6, 0.6) for u_id in _dm_tabs: var tab_hbox = _dm_tabs[u_id] tab_hbox.modulate = Color(1.0, 1.0, 1.0) if context_id == u_id else Color(0.6, 0.6, 0.6) func _setup_friend_suggest_ui() -> void: lobby._friend_suggest_panel = lobby.get_node_or_null("%FriendSuggestPanel") lobby._friend_suggest_list = lobby.get_node_or_null("%FriendSuggestList") if not lobby._friend_suggest_panel or not lobby._friend_suggest_list: push_warning("[Lobby] FriendSuggestPanel or FriendSuggestList not found in scene") return lobby._friend_suggest_panel.visible = false lobby._friend_suggest_list.item_activated.connect(_on_friend_suggest_activated) lobby._friend_suggest_list.item_selected.connect(_on_friend_suggest_activated) func _on_chat_input_changed(new_text: String) -> void: if not lobby._friend_suggest_panel or not lobby._friend_suggest_list: return if new_text.begins_with("@") and not " " in new_text: var search = new_text.substr(1).to_lower() lobby._friend_suggest_list.clear() for f in FriendManager.friends: if f.get("state", -1) == FriendManager.STATE_FRIEND: var uname = f.get("username", "") if uname.to_lower().begins_with(search) or search.is_empty(): lobby._friend_suggest_list.add_item(uname) lobby._friend_suggest_panel.visible = lobby._friend_suggest_list.item_count > 0 else: lobby._friend_suggest_panel.visible = false func _on_friend_suggest_activated(index: int) -> void: if not lobby._friend_suggest_list: return var uname = lobby._friend_suggest_list.get_item_text(index) lobby.chat_input.text = "@%s " % uname lobby.chat_input.caret_column = lobby.chat_input.text.length() lobby.chat_input.grab_focus() lobby._friend_suggest_panel.visible = false func _inject_local_message(text: String) -> void: var display_name = UserProfileManager.get_display_name("You") var ts_str = _get_local_time() _chat_messages.append({ "sender": display_name, "content": text, "ts": ts_str, "date": Time.get_date_string_from_system() }) _refresh_chat_display() func _trim_old_messages() -> void: if _chat_messages.size() > 100: _chat_messages = _chat_messages.slice(-100) func _refresh_chat_display() -> void: if not lobby.chat_display: return lobby.chat_display.clear() var messages_to_show = _chat_messages if _active_chat_context != "global": if _dm_messages.has(_active_chat_context): messages_to_show = _dm_messages[_active_chat_context] else: messages_to_show = [] for msg in messages_to_show: var ts: String = msg.get("ts", "") var sender: String = msg.get("sender", "?") var text: String = msg.get("content", "") lobby.chat_display.append_text("[color=#888888][%s][/color] [b]%s:[/b] %s\n" % [ts, sender, text]) if lobby.chat_display: var scrollbar = lobby.chat_display.get_v_scroll_bar() if scrollbar: lobby.chat_display.scroll_to_line(lobby.chat_display.get_line_count()) func _format_nakama_time(time_str: String) -> String: if time_str.is_valid_int(): var unix_time = time_str.to_int() var d = Time.get_date_dict_from_unix_time(unix_time) var t = Time.get_time_dict_from_unix_time(unix_time) return "%02d-%02d-%02d - %02d:%02d" % [d.day, d.month, d.year % 100, t.hour, t.minute] if time_str.length() >= 19: var date_part = time_str.substr(0, 10).split("-") var time_part = time_str.substr(11, 5) # HH:MM if date_part.size() == 3: var year = date_part[0].substr(2, 2) var month = date_part[1] var day = date_part[2] return "%s-%s-%s - %s" % [day, month, year, time_part] return _get_local_time() func _get_local_time() -> String: var d = Time.get_date_dict_from_system() var t = Time.get_time_dict_from_system() return "%02d-%02d-%02d - %02d:%02d" % [d.day, d.month, d.year % 100, t.hour, t.minute] func leave_global_chat() -> void: var socket = NakamaManager.socket if socket and _chat_channel: socket.received_channel_message.disconnect(_on_chat_message_received) await socket.leave_chat_async(_chat_channel.id) _chat_channel = null