OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / scripts / group_settings.gd

417 lines · gd ref:
extends Window
## Group Settings — manage registry room: name, avatar, members, invites, listed rooms.
## A "group" is a keet room tagged as type "registry" via _hs:room-meta.

@onready var group_title_label: Label = %GroupTitle
@onready var avatar_preview: TextureRect = %AvatarPreview
@onready var change_avatar_btn: Button = %ChangeAvatarBtn
@onready var name_field: LineEdit = %NameField
@onready var desc_field: TextEdit = %DescField
@onready var save_btn: Button = %SaveBtn
@onready var save_status: Label = %SaveStatus
@onready var error_label: Label = %ErrorLabel

@onready var member_list: VBoxContainer = %MemberList
@onready var member_status: Label = %MemberStatus
@onready var invite_role_select: OptionButton = %InviteRoleSelect
@onready var invite_max_uses: SpinBox = %InviteMaxUses
@onready var create_invite_btn: Button = %CreateInviteBtn
@onready var invite_list: VBoxContainer = %InviteList

@onready var ban_field: LineEdit = %BanField
@onready var ban_btn: Button = %BanBtn
@onready var banned_list: VBoxContainer = %BannedList

@onready var disc_key_field: LineEdit = %DiscKeyField
@onready var copy_disc_key_btn: Button = %CopyDiscKeyBtn
@onready var writer_key_field: LineEdit = %WriterKeyField
@onready var copy_writer_key_btn: Button = %CopyWriterKeyBtn
@onready var role_value: Label = %RoleValue
@onready var delete_group_btn: Button = %DeleteGroupBtn

signal group_deleted(room_id: String)

var _room_id: String = ""
var _config: Dictionary = {}
var _avatar_picker: AvatarPicker = null
var _my_member_id: String = ""
var _my_role: String = ""


func _ready() -> void:
	close_requested.connect(_on_close_requested)
	save_btn.pressed.connect(_on_save)
	change_avatar_btn.pressed.connect(_on_change_avatar)
	create_invite_btn.pressed.connect(_on_create_invite)
	ban_btn.pressed.connect(_on_remove_listing)
	copy_disc_key_btn.pressed.connect(func(): DisplayServer.clipboard_set(disc_key_field.text))
	copy_writer_key_btn.pressed.connect(func(): DisplayServer.clipboard_set(writer_key_field.text))
	delete_group_btn.pressed.connect(_on_delete_group)
	save_status.text = ""
	error_label.text = ""

	# Hide Autobase-specific widgets (keet room invites don't have role/maxUses)
	invite_role_select.visible = false
	invite_max_uses.visible = false

	# Placeholder avatar
	var placeholder := Image.create(64, 64, false, Image.FORMAT_RGBA8)
	placeholder.fill(Color(0.25, 0.25, 0.3))
	avatar_preview.texture = ImageTexture.create_from_image(placeholder)


func open_for_group(room_id: String) -> void:
	_room_id = room_id
	save_status.text = ""
	error_label.text = ""
	member_status.text = "Loading..."
	writer_key_field.text = "Loading..."
	role_value.text = "..."

	# Load room config (title, description, avatar)
	KeetService.room_mgr.get_config(room_id, func(config):
		_config = config if config is Dictionary else {}
		_populate_general()
	)

	# Load members of the registry room
	KeetService.identity.get_identity(func(ident):
		_my_member_id = ident.get("memberId", "") if ident is Dictionary else ""
		writer_key_field.text = _my_member_id if _my_member_id != "" else "(unknown)"
		KeetService.room_mgr.list_members(room_id, func(members):
			_populate_members(members)
		)
	)

	# Load listed rooms (moderation tab)
	_load_listed_rooms()

	# Show invite placeholder
	_show_invite_placeholder()

	# Info tab
	disc_key_field.text = room_id

	popup_centered()


# --- General Tab ---

func _populate_general() -> void:
	var name_val: String = str(_config.get("title", ""))
	var desc_val: String = str(_config.get("description", ""))
	name_field.text = name_val
	desc_field.text = desc_val
	group_title_label.text = "Group: %s" % name_val if name_val != "" else "Group Settings"
	_load_avatar()


func _load_avatar() -> void:
	var avatar: Variant = _config.get("avatar", null)
	if avatar is Dictionary:
		var b64: String = str(avatar.get("medium", ""))
		if b64 == "":
			b64 = str(avatar.get("small", ""))
		if b64 != "":
			var img_data: PackedByteArray = Marshalls.base64_to_raw(b64)
			var img := Image.new()
			if img.load_png_from_buffer(img_data) == OK:
				avatar_preview.texture = ImageTexture.create_from_image(img)
				return
			if img.load_webp_from_buffer(img_data) == OK:
				avatar_preview.texture = ImageTexture.create_from_image(img)


func _on_close_requested() -> void:
	hide()


func _on_save() -> void:
	save_status.text = "Saving..."
	save_status.remove_theme_color_override("font_color")
	error_label.text = ""
	var counter := {"pending": 0}

	var new_name: String = name_field.text.strip_edges()
	if new_name != str(_config.get("title", "")):
		counter["pending"] += 1
		KeetService.room_mgr.set_config(_room_id, "title", new_name, func(_r):
			_config["title"] = new_name
			counter["pending"] -= 1
			if counter["pending"] <= 0:
				_on_save_done()
		)

	var new_desc: String = desc_field.text.strip_edges()
	if new_desc != str(_config.get("description", "")):
		counter["pending"] += 1
		KeetService.room_mgr.set_config(_room_id, "description", new_desc, func(_r):
			_config["description"] = new_desc
			counter["pending"] -= 1
			if counter["pending"] <= 0:
				_on_save_done()
		)

	if counter["pending"] == 0:
		save_status.text = "No changes"


func _on_save_done() -> void:
	save_status.text = "Saved!"
	save_status.add_theme_color_override("font_color", Color(0.3, 0.9, 0.4))
	KeetService.discovery.list_registries()
	get_tree().create_timer(2.0).timeout.connect(func():
		if save_status.text == "Saved!":
			save_status.text = ""
	)


func _on_change_avatar() -> void:
	if _avatar_picker != null:
		_avatar_picker.queue_free()
	_avatar_picker = AvatarPicker.new()
	_avatar_picker.avatar_selected.connect(func(small_b64: String, large_b64: String):
		# Scale to keet room avatar sizes
		var source_img := Image.new()
		var raw: PackedByteArray = Marshalls.base64_to_raw(large_b64)
		if source_img.load_png_from_buffer(raw) != OK:
			return
		var small_img: Image = source_img.duplicate()
		small_img.resize(165, 165, Image.INTERPOLATE_LANCZOS)
		var medium_img: Image = source_img.duplicate()
		medium_img.resize(288, 288, Image.INTERPOLATE_LANCZOS)
		var small_val: String = Marshalls.raw_to_base64(small_img.save_png_to_buffer())
		var medium_val: String = Marshalls.raw_to_base64(medium_img.save_png_to_buffer())
		var avatar_val := {"small": small_val, "medium": medium_val}
		KeetService.room_mgr.set_config(_room_id, "avatar", avatar_val, func(_r):
			_config["avatar"] = avatar_val
			_load_avatar()
			KeetService.discovery.list_registries()
		)
	)
	add_child(_avatar_picker)
	_avatar_picker.popup_centered()


# --- Members Tab ---

func _populate_members(members: Array) -> void:
	for child in member_list.get_children():
		member_list.remove_child(child)
		child.queue_free()

	# Find our role
	_my_role = ""
	for m: Variant in members:
		if m is Dictionary and m.get("memberId", "") == _my_member_id:
			_my_role = m.get("role", "peer")
			break
	var is_admin: bool = _my_role == "admin"
	role_value.text = _my_role.capitalize() if _my_role != "" else "Member"

	if not is_admin:
		error_label.text = "Admin role required to manage this group"
		error_label.add_theme_color_override("font_color", Color(0.9, 0.6, 0.3))
	else:
		error_label.text = ""

	member_status.text = "%d member(s)" % members.size()

	# Sort: admins first, then mods, then peers
	var role_order := {"admin": 0, "mod": 1, "peer": 2}
	var sorted: Array = members.duplicate()
	sorted.sort_custom(func(a: Variant, b: Variant) -> bool:
		var ra: int = role_order.get(a.get("role", "peer") if a is Dictionary else "peer", 2)
		var rb: int = role_order.get(b.get("role", "peer") if b is Dictionary else "peer", 2)
		return ra < rb
	)

	for m: Variant in sorted:
		if not m is Dictionary:
			continue
		var hbox := HBoxContainer.new()
		hbox.add_theme_constant_override("separation", 6)
		var member_id: String = m.get("memberId", "?")
		var name_val: String = m.get("name", "")
		var role: String = m.get("role", "peer")
		var online: bool = m.get("online", false)
		var is_me: bool = member_id == _my_member_id

		# Role badge
		var role_badge := Label.new()
		role_badge.text = "[%s]" % role.to_upper()
		role_badge.add_theme_font_size_override("font_size", 10)
		var badge_color: Color
		match role:
			"admin": badge_color = Color(0.9, 0.7, 0.2)
			"mod": badge_color = Color(0.4, 0.7, 0.9)
			_: badge_color = Color(0.6, 0.65, 0.7)
		role_badge.add_theme_color_override("font_color", badge_color)
		role_badge.custom_minimum_size.x = 60
		hbox.add_child(role_badge)

		# Name
		var display: String = name_val if name_val != "" else member_id.left(12) + "..."
		if is_me:
			display += " (you)"
		if online:
			display += " [ON]"
		var name_label := Label.new()
		name_label.text = display
		name_label.add_theme_font_size_override("font_size", 12)
		name_label.add_theme_color_override("font_color", Color(0.8, 0.9, 1.0) if is_me else Color(0.7, 0.75, 0.8))
		name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		name_label.clip_text = true
		name_label.tooltip_text = member_id
		hbox.add_child(name_label)

		# Role change (admin only, not for self)
		if is_admin and not is_me:
			var role_btn := OptionButton.new()
			role_btn.add_item("Admin", 1)
			role_btn.add_item("Mod", 2)
			role_btn.add_item("Peer", 3)
			var role_map := {"admin": 0, "mod": 1, "peer": 2}
			role_btn.selected = role_map.get(role, 2)
			role_btn.add_theme_font_size_override("font_size", 10)
			role_btn.custom_minimum_size.x = 70
			role_btn.item_selected.connect(func(idx: int, _mid = member_id):
				var new_role: int = role_btn.get_item_id(idx)
				role_btn.disabled = true
				KeetService.room_mgr.change_role(_room_id, _mid, new_role, func(_r):
					role_btn.disabled = false
					KeetService.room_mgr.list_members(_room_id, func(updated_members):
						_populate_members(updated_members)
					)
				)
			)
			hbox.add_child(role_btn)

		member_list.add_child(hbox)


# --- Invites ---

func _show_invite_placeholder() -> void:
	for child in invite_list.get_children():
		invite_list.remove_child(child)
		child.queue_free()
	var lbl := Label.new()
	lbl.text = "Click 'Create' to generate an invite link"
	lbl.add_theme_color_override("font_color", Color(0.5, 0.55, 0.6))
	lbl.add_theme_font_size_override("font_size", 12)
	invite_list.add_child(lbl)


func _on_create_invite() -> void:
	create_invite_btn.disabled = true
	create_invite_btn.text = "..."
	KeetService.room_mgr.create_invite(_room_id, func(token):
		create_invite_btn.disabled = false
		create_invite_btn.text = "Create"
		for child in invite_list.get_children():
			invite_list.remove_child(child)
			child.queue_free()
		if token is String and token != "":
			var hbox := HBoxContainer.new()
			var info := Label.new()
			info.text = token.left(24) + "..."
			info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
			info.add_theme_font_size_override("font_size", 12)
			info.tooltip_text = token
			hbox.add_child(info)
			var copy_btn := Button.new()
			copy_btn.text = "Copy"
			copy_btn.add_theme_font_size_override("font_size", 11)
			copy_btn.pressed.connect(func(): DisplayServer.clipboard_set(token))
			hbox.add_child(copy_btn)
			invite_list.add_child(hbox)
		else:
			var lbl := Label.new()
			lbl.text = "Failed to create invite"
			lbl.add_theme_color_override("font_color", Color(0.9, 0.4, 0.3))
			lbl.add_theme_font_size_override("font_size", 12)
			invite_list.add_child(lbl)
	)


# --- Listed Rooms (was Moderation/Ban tab) ---

func _load_listed_rooms() -> void:
	KeetService.discovery.list_rooms(_room_id, "", func(rooms: Array):
		_populate_listed_rooms(rooms)
	)


func _populate_listed_rooms(rooms: Array) -> void:
	for child in banned_list.get_children():
		banned_list.remove_child(child)
		child.queue_free()

	if rooms.is_empty():
		var lbl := Label.new()
		lbl.text = "No rooms listed in this group"
		lbl.add_theme_color_override("font_color", Color(0.5, 0.55, 0.6))
		lbl.add_theme_font_size_override("font_size", 12)
		banned_list.add_child(lbl)
		return

	for r: Variant in rooms:
		if not r is Dictionary:
			continue
		var hbox := HBoxContainer.new()
		var room_id: String = r.get("roomId", "?")
		var room_title: String = r.get("title", room_id.left(16))

		var info := Label.new()
		info.text = room_title
		info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		info.add_theme_font_size_override("font_size", 12)
		info.tooltip_text = room_id
		hbox.add_child(info)

		var remove_btn := Button.new()
		remove_btn.text = "Remove"
		remove_btn.add_theme_font_size_override("font_size", 11)
		remove_btn.pressed.connect(func(_rid = room_id):
			KeetService.discovery.unregister_room(_room_id, _rid, func(_r):
				_load_listed_rooms()
			)
		)
		hbox.add_child(remove_btn)

		banned_list.add_child(hbox)


func _on_remove_listing() -> void:
	var room_id: String = ban_field.text.strip_edges()
	if room_id == "":
		return
	ban_field.text = ""
	KeetService.discovery.unregister_room(_room_id, room_id, func(_r):
		_load_listed_rooms()
	)


# --- Delete Group ---

func _on_delete_group() -> void:
	var group_name: String = name_field.text.strip_edges()
	if group_name == "":
		group_name = _room_id.left(12) + "..."

	var dialog := ConfirmationDialog.new()
	dialog.title = "Leave Group"
	dialog.dialog_text = "Leave '%s'?\n\nYou can rejoin later with an invite." % group_name
	dialog.min_size = Vector2i(360, 120)
	dialog.confirmed.connect(func():
		KeetService.room_mgr.leave_room(_room_id, func(_r):
			group_deleted.emit(_room_id)
			hide()
		)
		dialog.queue_free()
	)
	dialog.canceled.connect(func(): dialog.queue_free())
	add_child(dialog)
	dialog.popup_centered()