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()