extends Control class_name FriendsPresenceUI ## Friends & Presence UI panel. ## ## Displays: ## - Friends list with real-time online/offline presence & custom status ## - Friend requests inbox (with Accept / Decline / Send actions) ## - Following list (with Unfollow / Follow actions) ## - Blocked users list (with Unblock / Block actions) ## - Custom status updater signal back_requested @onready var back_button: Button = $VBoxContainer/HeaderHBox/BackButton @onready var refresh_button: Button = $VBoxContainer/HeaderHBox/RefreshButton @onready var status_label: Label = $VBoxContainer/StatusMessageLabel @onready var tab_container: TabContainer = $VBoxContainer/TabContainer @onready var friends_container: VBoxContainer = $VBoxContainer/TabContainer/Friends/Scroll/FriendsList @onready var requests_container: VBoxContainer = $VBoxContainer/TabContainer/Requests/Scroll/RequestsList @onready var following_container: VBoxContainer = $VBoxContainer/TabContainer/Following/Scroll/FollowingList @onready var blocked_container: VBoxContainer = $VBoxContainer/TabContainer/Blocked/Scroll/BlockedList @onready var custom_status_input: LineEdit = $VBoxContainer/MyStatusHBox/StatusLineEdit @onready var set_status_button: Button = $VBoxContainer/MyStatusHBox/SetStatusButton # Friend request input @onready var send_req_input: LineEdit = $VBoxContainer/TabContainer/Requests/SendHBox/TargetUserIdEdit @onready var send_req_button: Button = $VBoxContainer/TabContainer/Requests/SendHBox/SendButton # Follow input @onready var follow_input: LineEdit = $VBoxContainer/TabContainer/Following/FollowHBox/FollowUserIdEdit @onready var follow_button: Button = $VBoxContainer/TabContainer/Following/FollowHBox/FollowButton # Block input @onready var block_input: LineEdit = $VBoxContainer/TabContainer/Blocked/BlockHBox/BlockUserIdEdit @onready var block_button: Button = $VBoxContainer/TabContainer/Blocked/BlockHBox/BlockButton var _api_client: Node = null var _friend_presence_map: Dictionary = {} # user_id (int) -> {"status": String, "custom_status": String, "last_seen": String} func _ready() -> void: _api_client = _get_api() _wire_signals() if _api_client != null and _api_client.has_token(): refresh_all() 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_all) if set_status_button: set_status_button.pressed.connect(_on_set_status_pressed) if send_req_button: send_req_button.pressed.connect(_on_send_req_pressed) if follow_button: follow_button.pressed.connect(_on_follow_pressed) if block_button: block_button.pressed.connect(_on_block_pressed) if _api_client != null and _api_client.ws != null: if not _api_client.ws.presence_changed.is_connected(_on_presence_changed): _api_client.ws.presence_changed.connect(_on_presence_changed) 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_all() -> void: if _api_client == null or not _api_client.has_token(): show_message("Please log in to view social features.", true) return await refresh_friends() await refresh_requests() await refresh_following() await refresh_blocked() # ── Friends & Presence ── func refresh_friends() -> void: if _api_client == null: return # Fetch mutual presence first var pres_res: Dictionary = await _api_client.get_friends_presence() _friend_presence_map.clear() if pres_res.ok and pres_res.data is Dictionary and pres_res.data.has("friends"): for p in pres_res.data["friends"]: var uid: int = int(p.get("user_id", 0)) _friend_presence_map[uid] = p # Fetch friends list var res: Dictionary = await _api_client.get_friends() _clear_children(friends_container) if not res.ok: show_message("Failed to load friends: " + res.detail, true) return var friends_list: Array = [] if res.data is Dictionary and res.data.has("friends"): friends_list = res.data["friends"] if friends_list.is_empty(): var empty_label := Label.new() empty_label.text = "No friends yet. Send a friend request to get started!" empty_label.modulate = Color(0.7, 0.7, 0.7) friends_container.add_child(empty_label) return for friend in friends_list: var uid: int = int(friend.get("id", 0)) var uname: String = str(friend.get("username", "Unknown")) _build_friend_row(uid, uname) func _build_friend_row(uid: int, uname: String) -> void: var hbox := HBoxContainer.new() hbox.name = "FriendRow_%d" % uid hbox.add_theme_constant_override("separation", 12) var pres: Dictionary = _friend_presence_map.get(uid, {}) var is_online: bool = pres.get("status", "offline") == "online" var custom_status: String = str(pres.get("custom_status", "")) var last_seen: String = str(pres.get("last_seen", "")) # Status indicator dot var status_dot := Label.new() status_dot.text = "●" status_dot.modulate = Color(0.2, 0.9, 0.2) if is_online else Color(0.5, 0.5, 0.5) hbox.add_child(status_dot) # Username label var name_label := Label.new() name_label.text = uname name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL hbox.add_child(name_label) # Presence info var pres_label := Label.new() if is_online: pres_label.text = "Online" + ((" - " + custom_status) if custom_status != "" else "") pres_label.modulate = Color(0.3, 0.9, 0.3) else: pres_label.text = "Offline" + ((" (" + last_seen.left(10) + ")") if last_seen != "" else "") pres_label.modulate = Color(0.6, 0.6, 0.6) pres_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL hbox.add_child(pres_label) # Action: Unfriend var unfriend_btn := Button.new() unfriend_btn.text = "Unfriend" unfriend_btn.pressed.connect(func(): _on_unfriend(uid)) hbox.add_child(unfriend_btn) # Action: Block var block_btn := Button.new() block_btn.text = "Block" block_btn.pressed.connect(func(): _on_block_friend(uid)) hbox.add_child(block_btn) friends_container.add_child(hbox) func _on_unfriend(uid: int) -> void: var res: Dictionary = await _api_client.unfriend(uid) if res.ok: show_message("Unfriended user.") refresh_friends() else: show_message("Failed to unfriend: " + res.detail, true) func _on_block_friend(uid: int) -> void: var res: Dictionary = await _api_client.block_user(uid) if res.ok: show_message("Blocked user.") refresh_friends() refresh_blocked() else: show_message("Failed to block user: " + res.detail, true) func _on_presence_changed(uid: int, p_status: String) -> void: if not _friend_presence_map.has(uid): _friend_presence_map[uid] = {} _friend_presence_map[uid]["status"] = p_status # Refresh UI if row exists var row_name := "FriendRow_%d" % uid if friends_container.has_node(row_name): refresh_friends() func _on_set_status_pressed() -> void: if _api_client == null: return var new_status := custom_status_input.text.strip_edges() var res: Dictionary = await _api_client.send_presence_heartbeat(new_status) if res.ok: show_message("Status updated.") else: show_message("Failed to update status: " + res.detail, true) # ── Requests ── func refresh_requests() -> void: if _api_client == null: return var res: Dictionary = await _api_client.get_friend_requests() _clear_children(requests_container) if not res.ok: show_message("Failed to load requests: " + res.detail, true) return var req_list: Array = [] if res.data is Dictionary and res.data.has("requests"): req_list = res.data["requests"] if req_list.is_empty(): var empty_label := Label.new() empty_label.text = "No pending friend requests." empty_label.modulate = Color(0.7, 0.7, 0.7) requests_container.add_child(empty_label) return for req in req_list: var req_id: int = int(req.get("id", 0)) var from_id: int = int(req.get("requester_id", 0)) var created_at: String = str(req.get("created_at", "")).left(10) var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 12) var lbl := Label.new() lbl.text = "Request from User #%d (%s)" % [from_id, created_at] lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL hbox.add_child(lbl) var accept_btn := Button.new() accept_btn.text = "Accept" accept_btn.pressed.connect(func(): _on_accept_request(req_id)) hbox.add_child(accept_btn) var decline_btn := Button.new() decline_btn.text = "Decline" decline_btn.pressed.connect(func(): _on_decline_request(req_id)) hbox.add_child(decline_btn) requests_container.add_child(hbox) func _on_accept_request(req_id: int) -> void: var res: Dictionary = await _api_client.accept_friend_request(req_id) if res.ok: show_message("Friend request accepted!") refresh_requests() refresh_friends() else: show_message("Failed to accept: " + res.detail, true) func _on_decline_request(req_id: int) -> void: var res: Dictionary = await _api_client.decline_friend_request(req_id) if res.ok: show_message("Friend request declined.") refresh_requests() else: show_message("Failed to decline: " + res.detail, true) func _on_send_req_pressed() -> void: var target_text := send_req_input.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.send_friend_request(target_id) if res.ok: show_message("Friend request sent to User #%d!" % target_id) send_req_input.text = "" else: show_message("Failed to send request: " + res.detail, true) # ── Following ── func refresh_following() -> void: if _api_client == null: return var res: Dictionary = await _api_client.get_following() _clear_children(following_container) if not res.ok: return var list: Array = [] if res.data is Dictionary and res.data.has("following"): list = res.data["following"] if list.is_empty(): var empty_label := Label.new() empty_label.text = "You are not following anyone." empty_label.modulate = Color(0.7, 0.7, 0.7) following_container.add_child(empty_label) return for u in list: var uid: int = int(u.get("id", 0)) var uname: String = str(u.get("username", "Unknown")) var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 12) var lbl := Label.new() lbl.text = "%s (ID: %d)" % [uname, uid] lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL hbox.add_child(lbl) var unfollow_btn := Button.new() unfollow_btn.text = "Unfollow" unfollow_btn.pressed.connect(func(): _on_unfollow(uid)) hbox.add_child(unfollow_btn) following_container.add_child(hbox) func _on_unfollow(uid: int) -> void: var res: Dictionary = await _api_client.unfollow_user(uid) if res.ok: show_message("Unfollowed User #%d." % uid) refresh_following() else: show_message("Failed to unfollow: " + res.detail, true) func _on_follow_pressed() -> void: var target_text := follow_input.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.follow_user(target_id) if res.ok: show_message("Following User #%d." % target_id) follow_input.text = "" refresh_following() else: show_message("Failed to follow: " + res.detail, true) # ── Blocked ── func refresh_blocked() -> void: if _api_client == null: return var res: Dictionary = await _api_client.get_blocked() _clear_children(blocked_container) if not res.ok: return var list: Array = [] if res.data is Dictionary and res.data.has("blocked"): list = res.data["blocked"] if list.is_empty(): var empty_label := Label.new() empty_label.text = "No blocked users." empty_label.modulate = Color(0.7, 0.7, 0.7) blocked_container.add_child(empty_label) return for u in list: var uid: int = int(u.get("id", 0)) var uname: String = str(u.get("username", "Unknown")) var hbox := HBoxContainer.new() hbox.add_theme_constant_override("separation", 12) var lbl := Label.new() lbl.text = "%s (ID: %d)" % [uname, uid] lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL hbox.add_child(lbl) var unblock_btn := Button.new() unblock_btn.text = "Unblock" unblock_btn.pressed.connect(func(): _on_unblock(uid)) hbox.add_child(unblock_btn) blocked_container.add_child(hbox) func _on_unblock(uid: int) -> void: var res: Dictionary = await _api_client.unblock_user(uid) if res.ok: show_message("Unblocked User #%d." % uid) refresh_blocked() else: show_message("Failed to unblock: " + res.detail, true) func _on_block_pressed() -> void: var target_text := block_input.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.block_user(target_id) if res.ok: show_message("Blocked User #%d." % target_id) block_input.text = "" refresh_blocked() refresh_friends() else: show_message("Failed to block: " + res.detail, true) func _clear_children(node: Node) -> void: if node == null: return for c in node.get_children(): c.queue_free()