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 _requests_tab_btn: Button = %RequestsTabBtn @onready var _friends_tab_btn: Button = %FriendsTabBtn @onready var _dm_tab_btn: Button = %DMTabBtn # Views @onready var _search_view: VBoxContainer = %SearchView @onready var _requests_view: VBoxContainer = %RequestsView @onready var _friends_view: VBoxContainer = %FriendsView @onready var _dm_view: VBoxContainer = %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: VBoxContainer = %SearchResultsList @onready var _search_result_tmpl: HBoxContainer = %SearchResultTemplate # Requests tab nodes @onready var _no_requests_label: Label = %NoRequestsLabel @onready var _requests_list: VBoxContainer = %RequestsList @onready var _request_row_tmpl: HBoxContainer = %RequestRowTemplate # Friends tab nodes @onready var _no_friends_label: Label = %NoFriendsLabel @onready var _friend_list: VBoxContainer = %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")) _requests_tab_btn.pressed.connect(func(): _show_tab("requests")) _friends_tab_btn.pressed.connect(func(): _show_tab("friends")) _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" _requests_view.visible = tab == "requests" _friends_view.visible = tab == "friends" _dm_view.visible = 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 "" for u in users: if u.user_id == my_id: continue # skip self var row: HBoxContainer = _search_result_tmpl.duplicate() row.show() row.get_node("SRNameLabel").text = u.display_name + " (@" + u.username + ")" var add_btn: Button = row.get_node("SRAddBtn") add_btn.pressed.connect(func(): FriendManager.add_friend_by_id(u.user_id) add_btn.text = "Sent ✓" add_btn.disabled = true ) _search_results_list.add_child(row) # ─── Requests Tab ──────────────────────────────────────────────────────────── func _populate_requests(incoming: Array) -> void: for ch in _requests_list.get_children(): ch.queue_free() _no_requests_label.visible = incoming.is_empty() for f in incoming: var row: HBoxContainer = _request_row_tmpl.duplicate() row.show() row.get_node("RRNameLabel").text = f.username var accept_btn: Button = row.get_node("RRAcceptBtn") var decline_btn: Button = row.get_node("RRDeclineBtn") var uid: String = f.user_id accept_btn.pressed.connect(func(): FriendManager.add_friend_by_id(uid) row.queue_free() ) decline_btn.pressed.connect(func(): FriendManager.remove_friend(uid) row.queue_free() ) _requests_list.add_child(row) # Badge on tab button if incoming.is_empty(): _requests_tab_btn.text = "Requests" else: _requests_tab_btn.text = "Requests (%d)" % incoming.size() # ─── Friends Tab ───────────────────────────────────────────────────────────── func _populate_friends(mutual: Array) -> void: for ch in _friend_list.get_children(): ch.queue_free() _no_friends_label.visible = mutual.is_empty() var friend_row_scene := preload("res://scenes/ui/friend_row.tscn") for f in mutual: 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()) var incoming := friends.filter(func(f): return f.state == FriendManager.STATE_INVITE_IN) var mutual := friends.filter(func(f): return f.state == FriendManager.STATE_FRIEND) print("[SocialPanel] incoming=%d mutual=%d" % [incoming.size(), mutual.size()]) _populate_requests(incoming) _populate_friends(mutual) # ─── DM ────────────────────────────────────────────────────────────────────── func open_dm(user_id: String, username: String) -> void: _active_dm_user_id = user_id _dm_username_label.text = "DM: %s" % username _dm_tab_btn.visible = true _dm_log.clear() _show_tab("dm") _dm_log.append_text("[i]Loading history...[/i]\n") var history = await FriendManager.get_dm_history(user_id) _dm_log.clear() _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 _dm_history[user_id].append({"from": "me" if is_self else entry.get("from"), "msg": entry.get("msg")}) _dm_log.append_text("[b]%s:[/b] %s\n" % [sender_name, entry.get("msg", "")]) FriendManager.open_dm(user_id) 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])