Implement Godot friends & presence UI (task af249a67f693)
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user