extends Control ## SocialPanel — Search, Requests, Friends, DM tabs. ## All UI nodes are defined in social_panel.tscn. signal closed # ─── Node references ───────────────────────────────────────────────────────── @onready var _close_btn: Button = %CloseBtn # Tab buttons @onready var _search_tab_btn: Button = %SearchTabBtn @onready var _friends_tab_btn: Button = %FriendsTabBtn @onready var _dm_tab_btn: Button = get_node_or_null("%DMTabBtn") # Views @onready var _search_view: VBoxContainer = %SearchView @onready var _friends_view: VBoxContainer = %FriendsView @onready var _dm_view: PanelContainer = %DMView # Search tab nodes @onready var _search_input: LineEdit = %SearchInput @onready var _search_btn: Button = %SearchBtn @onready var _no_search_results: Label = %NoSearchResultsLabel @onready var _search_results_list: GridContainer = %SearchResultsList # Friends tab nodes @onready var _no_friends_label: Label = %NoFriendsLabel @onready var _friend_list: GridContainer = %FriendList # DM tab nodes @onready var _dm_back_btn: Button = %DMBackBtn @onready var _dm_username_label: Label = %DMUsernameLabel @onready var _dm_log: RichTextLabel = %DMLog @onready var _dm_input: LineEdit = %DMInput @onready var _dm_send_btn: Button = %DMSendBtn # ─── State ─────────────────────────────────────────────────────────────────── var _current_tab: String = "search" var _active_dm_user_id: String = "" var _dm_history: Dictionary = {} # ─── Lifecycle ─────────────────────────────────────────────────────────────── func _ready() -> void: _close_btn.pressed.connect(func(): emit_signal("closed"); hide()) # Tab buttons _search_tab_btn.pressed.connect(func(): _show_tab("search")) _friends_tab_btn.pressed.connect(func(): _show_tab("friends")) if _dm_tab_btn: _dm_tab_btn.pressed.connect(func(): _show_tab("dm")) _dm_back_btn.pressed.connect(func(): _show_tab("friends")) # Search _search_btn.pressed.connect(_on_search_pressed) _search_input.text_submitted.connect(func(_t): _on_search_pressed()) # DM _dm_send_btn.pressed.connect(_send_dm) _dm_input.text_submitted.connect(func(_t): _send_dm()) # FriendManager signals FriendManager.friends_updated.connect(_on_friends_updated) FriendManager.dm_message_received.connect(_on_dm_received) # Replay already-loaded friends in case FriendManager loaded before this panel was ready if FriendManager.friends.size() > 0: _on_friends_updated(FriendManager.friends) else: FriendManager.load_friends() _show_tab("search") # ─── Tab Switching ─────────────────────────────────────────────────────────── func _show_tab(tab: String) -> void: _current_tab = tab _search_view.visible = tab == "search" _friends_view.visible = tab == "friends" _dm_view.visible = tab == "dm" _search_tab_btn.set_pressed_no_signal(tab == "search") _friends_tab_btn.set_pressed_no_signal(tab == "friends") if _dm_tab_btn: _dm_tab_btn.set_pressed_no_signal(tab == "dm") # Auto-load search results on first open if tab == "search" and _search_results_list.get_child_count() == 0: _on_search_pressed() # ─── Search Tab ────────────────────────────────────────────────────────────── func _on_search_pressed() -> void: var query := _search_input.text.strip_edges() # UUID → add directly if query.length() == 36 and query.count("-") == 4: FriendManager.add_friend_by_id(query) _search_input.text = "" return _search_btn.disabled = true var payload = JSON.stringify({"query": query}) var result = await NakamaManager.client.rpc_async(NakamaManager.session, "search_users", payload) _search_btn.disabled = false if result.is_exception(): push_warning("[Social] Search failed: " + result.get_exception().message) return var response = JSON.parse_string(result.payload) if not response or not response.has("users"): return _populate_search_results(response.users) func _populate_search_results(users: Array) -> void: for ch in _search_results_list.get_children(): ch.queue_free() _no_search_results.visible = users.is_empty() var my_id = NakamaManager.session.user_id if NakamaManager.session else "" var friend_row_scene := preload("res://scenes/ui/friend_row.tscn") for u in users: if u.user_id == my_id: continue # skip self var row: Control = friend_row_scene.instantiate() _search_results_list.add_child(row) row.setup(u.user_id, u.username, -1, self) # ─── Friends Tab ───────────────────────────────────────────────────────────── func _populate_friends(friends_array: Array) -> void: for ch in _friend_list.get_children(): ch.queue_free() _no_friends_label.visible = friends_array.is_empty() var friend_row_scene := preload("res://scenes/ui/friend_row.tscn") for f in friends_array: var row: Control = friend_row_scene.instantiate() _friend_list.add_child(row) row.setup(f.user_id, f.username, f.state, self) # ─── FriendManager Callbacks ───────────────────────────────────────────────── func _on_friends_updated(friends: Array) -> void: print("[SocialPanel] _on_friends_updated: total=%d" % friends.size()) # Pass both incoming and mutual friends to the friends list var display_list := friends.filter(func(f): return f.state == FriendManager.STATE_INVITE_IN or f.state == FriendManager.STATE_FRIEND) # Also update the badge on the Friends tab if there are incoming requests var incoming_count = friends.filter(func(f): return f.state == FriendManager.STATE_INVITE_IN).size() if incoming_count > 0: _friends_tab_btn.text = "FRIENDS (%d)" % incoming_count else: _friends_tab_btn.text = "FRIENDS" _populate_friends(display_list) signal dm_requested(user_id: String, username: String) # ─── DM ────────────────────────────────────────────────────────────────────── func open_dm(user_id: String, username: String) -> void: emit_signal("dm_requested", user_id, username) func _send_dm() -> void: var text = _dm_input.text.strip_edges() if text.is_empty() or _active_dm_user_id.is_empty(): return _dm_input.text = "" var sent = await FriendManager.send_dm(_active_dm_user_id, text) if sent: if not _dm_history.has(_active_dm_user_id): _dm_history[_active_dm_user_id] = [] _dm_history[_active_dm_user_id].append({"from": "me", "msg": text}) _dm_log.append_text("[b]You:[/b] %s\n" % text) func _on_dm_received(from_user_id: String, from_name: String, message: String) -> void: if not _dm_history.has(from_user_id): _dm_history[from_user_id] = [] _dm_history[from_user_id].append({"from": from_user_id, "msg": message}) if _active_dm_user_id == from_user_id and _current_tab == "dm": _dm_log.append_text("[b]%s:[/b] %s\n" % [from_name, message])