feat(ui): Implement persistent views

This commit is contained in:
Slipstream 2025-06-11 02:06:15 -06:00
parent eb38ecf671
commit 152c0f12be
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
2 changed files with 53 additions and 6 deletions

View File

@ -1490,6 +1490,37 @@ class Client:
data = await self._http.get_channel_invites(channel_id)
return [self.parse_invite(inv) for inv in data]
def add_persistent_view(self, view: "View") -> None:
"""
Registers a persistent view with the client.
Persistent views have a timeout of `None` and their components must have a `custom_id`.
This allows the view to be re-instantiated across bot restarts.
Args:
view (View): The view instance to register.
Raises:
ValueError: If the view is not persistent (timeout is not None) or if a component's
custom_id is already registered.
"""
if self.is_ready():
print(
"Warning: Adding a persistent view after the client is ready. "
"This view will only be available for interactions on this session."
)
if view.timeout is not None:
raise ValueError("Persistent views must have a timeout of None.")
for item in view.children:
if item.custom_id: # Ensure custom_id is not None
if item.custom_id in self._persistent_views:
raise ValueError(
f"A component with custom_id '{item.custom_id}' is already registered."
)
self._persistent_views[item.custom_id] = view
# --- Application Command Methods ---
async def process_interaction(self, interaction: Interaction) -> None:
"""Internal method to process an interaction from the gateway."""
@ -1500,11 +1531,25 @@ class Client:
if (
interaction.type == InteractionType.MESSAGE_COMPONENT
and interaction.message
and interaction.data
):
view = self._views.get(interaction.message.id)
if view:
asyncio.create_task(view._dispatch(interaction))
return
else:
# No active view found, check for persistent views
custom_id = interaction.data.custom_id
if custom_id:
registered_view = self._persistent_views.get(custom_id)
if registered_view:
# Create a new instance of the persistent view
new_view = registered_view.__class__()
await new_view._start(self)
new_view.message_id = interaction.message.id
self._views[interaction.message.id] = new_view
asyncio.create_task(new_view._dispatch(interaction))
return
await self.app_command_handler.process_interaction(interaction)

View File

@ -28,6 +28,8 @@ class View:
self._client: Optional[Client] = None
self._message_id: Optional[str] = None
# The below is a bit of a hack to support items defined as class members
# e.g. button = Button(...)
for item in self.__class__.__dict__.values():
if isinstance(item, Item):
self.add_item(item)
@ -44,6 +46,11 @@ class View:
if len(self.__children) >= 25:
raise ValueError("A view can only have a maximum of 25 components.")
if self.timeout is None and item.custom_id is None:
raise ValueError(
"All components in a persistent view must have a 'custom_id'."
)
item._view = self
self.__children.append(item)
@ -65,12 +72,7 @@ class View:
rows: List[ActionRow] = []
for item in self.children:
if item.custom_id is None:
item.custom_id = (
f"{self.id}:{item.__class__.__name__}:{len(self.__children)}"
)
rows.append(ActionRow(components=[item]))
rows.append(ActionRow(components=[item]))
return rows