mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-09-05 09:26:10 +00:00
Add support for components V2
Co-authored-by: Michael H <michael@michaelhall.tech> Co-authored-by: Soheab <33902984+Soheab@users.noreply.github.com> Co-authored-by: owocado <24418520+owocado@users.noreply.github.com> Co-authored-by: Jay3332 <40323796+jay3332@users.noreply.github.com> Co-authored-by: Danny <1695103+Rapptz@users.noreply.github.com>
This commit is contained in:
@ -23,7 +23,23 @@ DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any, Callable, ClassVar, Coroutine, Dict, Iterator, List, Optional, Sequence, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Coroutine,
|
||||
Dict,
|
||||
Generator,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
|
||||
@ -32,6 +48,7 @@ import logging
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
|
||||
from .item import Item, ItemCallbackType
|
||||
from .select import Select
|
||||
from .dynamic import DynamicItem
|
||||
@ -41,26 +58,37 @@ from ..components import (
|
||||
_component_factory,
|
||||
Button as ButtonComponent,
|
||||
SelectMenu as SelectComponent,
|
||||
SectionComponent,
|
||||
TextDisplay as TextDisplayComponent,
|
||||
MediaGalleryComponent,
|
||||
FileComponent,
|
||||
SeparatorComponent,
|
||||
ThumbnailComponent,
|
||||
Container as ContainerComponent,
|
||||
)
|
||||
from ..utils import get as _utils_get, find as _utils_find
|
||||
|
||||
# fmt: off
|
||||
__all__ = (
|
||||
'View',
|
||||
'LayoutView',
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
from typing_extensions import Self, TypeGuard
|
||||
import re
|
||||
|
||||
from ..interactions import Interaction
|
||||
from ..message import Message
|
||||
from ..types.components import Component as ComponentPayload
|
||||
from ..types.components import ComponentBase as ComponentBasePayload, Component as ComponentPayload
|
||||
from ..types.interactions import ModalSubmitComponentInteractionData as ModalSubmitComponentInteractionDataPayload
|
||||
from ..state import ConnectionState
|
||||
from .modal import Modal
|
||||
|
||||
ItemLike = Union[ItemCallbackType[Any, Any], Item[Any]]
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
@ -69,21 +97,61 @@ def _walk_all_components(components: List[Component]) -> Iterator[Component]:
|
||||
for item in components:
|
||||
if isinstance(item, ActionRowComponent):
|
||||
yield from item.children
|
||||
elif isinstance(item, ContainerComponent):
|
||||
yield from _walk_all_components(item.children)
|
||||
elif isinstance(item, SectionComponent):
|
||||
yield from item.children
|
||||
yield item.accessory
|
||||
else:
|
||||
yield item
|
||||
|
||||
|
||||
def _component_to_item(component: Component) -> Item:
|
||||
if isinstance(component, ButtonComponent):
|
||||
def _component_to_item(component: Component, parent: Optional[Item] = None) -> Item:
|
||||
if isinstance(component, ActionRowComponent):
|
||||
from .action_row import ActionRow
|
||||
|
||||
item = ActionRow.from_component(component)
|
||||
elif isinstance(component, ButtonComponent):
|
||||
from .button import Button
|
||||
|
||||
return Button.from_component(component)
|
||||
if isinstance(component, SelectComponent):
|
||||
item = Button.from_component(component)
|
||||
elif isinstance(component, SelectComponent):
|
||||
from .select import BaseSelect
|
||||
|
||||
return BaseSelect.from_component(component)
|
||||
item = BaseSelect.from_component(component)
|
||||
elif isinstance(component, SectionComponent):
|
||||
from .section import Section
|
||||
|
||||
return Item.from_component(component)
|
||||
item = Section.from_component(component)
|
||||
elif isinstance(component, TextDisplayComponent):
|
||||
from .text_display import TextDisplay
|
||||
|
||||
item = TextDisplay.from_component(component)
|
||||
elif isinstance(component, MediaGalleryComponent):
|
||||
from .media_gallery import MediaGallery
|
||||
|
||||
item = MediaGallery.from_component(component)
|
||||
elif isinstance(component, FileComponent):
|
||||
from .file import File
|
||||
|
||||
item = File.from_component(component)
|
||||
elif isinstance(component, SeparatorComponent):
|
||||
from .separator import Separator
|
||||
|
||||
item = Separator.from_component(component)
|
||||
elif isinstance(component, ThumbnailComponent):
|
||||
from .thumbnail import Thumbnail
|
||||
|
||||
item = Thumbnail.from_component(component)
|
||||
elif isinstance(component, ContainerComponent):
|
||||
from .container import Container
|
||||
|
||||
item = Container.from_component(component)
|
||||
else:
|
||||
item = Item.from_component(component)
|
||||
|
||||
item._parent = parent
|
||||
return item
|
||||
|
||||
|
||||
class _ViewWeights:
|
||||
@ -133,73 +201,66 @@ class _ViewWeights:
|
||||
class _ViewCallback:
|
||||
__slots__ = ('view', 'callback', 'item')
|
||||
|
||||
def __init__(self, callback: ItemCallbackType[Any, Any], view: View, item: Item[View]) -> None:
|
||||
def __init__(self, callback: ItemCallbackType[Any, Any], view: BaseView, item: Item[BaseView]) -> None:
|
||||
self.callback: ItemCallbackType[Any, Any] = callback
|
||||
self.view: View = view
|
||||
self.item: Item[View] = item
|
||||
self.view: BaseView = view
|
||||
self.item: Item[BaseView] = item
|
||||
|
||||
def __call__(self, interaction: Interaction) -> Coroutine[Any, Any, Any]:
|
||||
return self.callback(self.view, interaction, self.item)
|
||||
|
||||
|
||||
class View:
|
||||
"""Represents a UI view.
|
||||
|
||||
This object must be inherited to create a UI within Discord.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
timeout: Optional[:class:`float`]
|
||||
Timeout in seconds from last interaction with the UI before no longer accepting input.
|
||||
If ``None`` then there is no timeout.
|
||||
"""
|
||||
|
||||
__discord_ui_view__: ClassVar[bool] = True
|
||||
class BaseView:
|
||||
__discord_ui_view__: ClassVar[bool] = False
|
||||
__discord_ui_modal__: ClassVar[bool] = False
|
||||
__view_children_items__: ClassVar[List[ItemCallbackType[Any, Any]]] = []
|
||||
__view_children_items__: ClassVar[Dict[str, ItemLike]] = {}
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
super().__init_subclass__()
|
||||
|
||||
children: Dict[str, ItemCallbackType[Any, Any]] = {}
|
||||
for base in reversed(cls.__mro__):
|
||||
for name, member in base.__dict__.items():
|
||||
if hasattr(member, '__discord_ui_model_type__'):
|
||||
children[name] = member
|
||||
|
||||
if len(children) > 25:
|
||||
raise TypeError('View cannot have more than 25 children')
|
||||
|
||||
cls.__view_children_items__ = list(children.values())
|
||||
|
||||
def _init_children(self) -> List[Item[Self]]:
|
||||
children = []
|
||||
for func in self.__view_children_items__:
|
||||
item: Item = func.__discord_ui_model_type__(**func.__discord_ui_model_kwargs__)
|
||||
item.callback = _ViewCallback(func, self, item) # type: ignore
|
||||
item._view = self
|
||||
if isinstance(item, Select):
|
||||
item.options = [option.copy() for option in item.options]
|
||||
setattr(self, func.__name__, item)
|
||||
children.append(item)
|
||||
return children
|
||||
|
||||
def __init__(self, *, timeout: Optional[float] = 180.0):
|
||||
def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
|
||||
self.__timeout = timeout
|
||||
self._children: List[Item[Self]] = self._init_children()
|
||||
self.__weights = _ViewWeights(self._children)
|
||||
self.id: str = os.urandom(16).hex()
|
||||
self._cache_key: Optional[int] = None
|
||||
self.__cancel_callback: Optional[Callable[[View], None]] = None
|
||||
self.__cancel_callback: Optional[Callable[[BaseView], None]] = None
|
||||
self.__timeout_expiry: Optional[float] = None
|
||||
self.__timeout_task: Optional[asyncio.Task[None]] = None
|
||||
self.__stopped: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
|
||||
self._total_children: int = len(tuple(self.walk_children()))
|
||||
|
||||
def _is_layout(self) -> TypeGuard[LayoutView]: # type: ignore
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__} timeout={self.timeout} children={len(self._children)}>'
|
||||
|
||||
def _init_children(self) -> List[Item[Self]]:
|
||||
children = []
|
||||
parents = {}
|
||||
|
||||
for name, raw in self.__view_children_items__.items():
|
||||
if isinstance(raw, Item):
|
||||
item = raw.copy()
|
||||
setattr(self, name, item)
|
||||
item._update_view(self)
|
||||
parent = getattr(item, '__discord_ui_parent__', None)
|
||||
if parent and parent._view is None:
|
||||
parent._view = self
|
||||
children.append(item)
|
||||
parents[raw] = item
|
||||
else:
|
||||
item: Item = raw.__discord_ui_model_type__(**raw.__discord_ui_model_kwargs__)
|
||||
item.callback = _ViewCallback(raw, self, item) # type: ignore
|
||||
item._view = self
|
||||
if isinstance(item, Select):
|
||||
item.options = [option.copy() for option in item.options]
|
||||
setattr(self, raw.__name__, item)
|
||||
parent = getattr(raw, '__discord_ui_parent__', None)
|
||||
if parent:
|
||||
parents.get(parent, parent)._children.append(item)
|
||||
continue
|
||||
children.append(item)
|
||||
|
||||
return children
|
||||
|
||||
async def __timeout_task_impl(self) -> None:
|
||||
while True:
|
||||
# Guard just in case someone changes the value of the timeout at runtime
|
||||
@ -218,29 +279,16 @@ class View:
|
||||
await asyncio.sleep(self.__timeout_expiry - now)
|
||||
|
||||
def is_dispatchable(self) -> bool:
|
||||
# this is used by webhooks to check whether a view requires a state attached
|
||||
# or not, this simply is, whether a view has a component other than a url button
|
||||
return any(item.is_dispatchable() for item in self.children)
|
||||
# checks whether any interactable items (buttons or selects) are present
|
||||
# in this view, and check whether this requires a state attached in case
|
||||
# of webhooks and if the view should be stored in the view store
|
||||
return any(item.is_dispatchable() for item in self.walk_children())
|
||||
|
||||
def has_components_v2(self) -> bool:
|
||||
return any(c._is_v2() for c in self.children)
|
||||
|
||||
def to_components(self) -> List[Dict[str, Any]]:
|
||||
def key(item: Item) -> int:
|
||||
return item._rendered_row or 0
|
||||
|
||||
children = sorted(self._children, key=key)
|
||||
components: List[Dict[str, Any]] = []
|
||||
for _, group in groupby(children, key=key):
|
||||
children = [item.to_component_dict() for item in group]
|
||||
if not children:
|
||||
continue
|
||||
|
||||
components.append(
|
||||
{
|
||||
'type': 1,
|
||||
'components': children,
|
||||
}
|
||||
)
|
||||
|
||||
return components
|
||||
return NotImplemented
|
||||
|
||||
def _refresh_timeout(self) -> None:
|
||||
if self.__timeout:
|
||||
@ -271,13 +319,17 @@ class View:
|
||||
return self._children.copy()
|
||||
|
||||
@classmethod
|
||||
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View:
|
||||
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> Union[View, LayoutView]:
|
||||
"""Converts a message's components into a :class:`View`.
|
||||
|
||||
The :attr:`.Message.components` of a message are read-only
|
||||
and separate types from those in the ``discord.ui`` namespace.
|
||||
In order to modify and edit message components they must be
|
||||
converted into a :class:`View` first.
|
||||
converted into a :class:`View` or :class:`LayoutView` first.
|
||||
|
||||
If the message has any v2 components, then you must use
|
||||
:class:`LayoutView` in order for them to be converted into
|
||||
their respective items. :class:`View` does not support v2 components.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
@ -287,24 +339,43 @@ class View:
|
||||
The timeout of the converted view.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`View`
|
||||
The converted view. This always returns a :class:`View` and not
|
||||
one of its subclasses.
|
||||
-------
|
||||
Union[:class:`View`, :class:`LayoutView`]
|
||||
The converted view. This will always return one of :class:`View` or
|
||||
:class:`LayoutView`, and not one of its subclasses.
|
||||
"""
|
||||
view = View(timeout=timeout)
|
||||
|
||||
if issubclass(cls, View):
|
||||
view_cls = View
|
||||
elif issubclass(cls, LayoutView):
|
||||
view_cls = LayoutView
|
||||
else:
|
||||
raise TypeError('unreachable exception')
|
||||
|
||||
view = view_cls(timeout=timeout)
|
||||
row = 0
|
||||
|
||||
for component in message.components:
|
||||
if isinstance(component, ActionRowComponent):
|
||||
if not view._is_layout() and isinstance(component, ActionRowComponent):
|
||||
for child in component.children:
|
||||
item = _component_to_item(child)
|
||||
item.row = row
|
||||
# this error should never be raised, because ActionRows can only
|
||||
# contain items that View accepts, but check anyways
|
||||
if item._is_v2():
|
||||
raise ValueError(f'{item.__class__.__name__} cannot be added to {view.__class__.__name__}')
|
||||
view.add_item(item)
|
||||
row += 1
|
||||
else:
|
||||
item = _component_to_item(component)
|
||||
item.row = row
|
||||
view.add_item(item)
|
||||
row += 1
|
||||
continue
|
||||
|
||||
item = _component_to_item(component)
|
||||
item.row = row
|
||||
|
||||
if item._is_v2() and not view._is_layout():
|
||||
raise ValueError(f'{item.__class__.__name__} cannot be added to {view.__class__.__name__}')
|
||||
|
||||
view.add_item(item)
|
||||
row += 1
|
||||
|
||||
return view
|
||||
|
||||
@ -324,19 +395,25 @@ class View:
|
||||
TypeError
|
||||
An :class:`Item` was not passed.
|
||||
ValueError
|
||||
Maximum number of children has been exceeded (25)
|
||||
or the row the item is trying to be added to is full.
|
||||
Maximum number of children has been exceeded, the
|
||||
row the item is trying to be added to is full or the item
|
||||
you tried to add is not allowed in this View.
|
||||
"""
|
||||
|
||||
if len(self._children) >= 25:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
|
||||
if not isinstance(item, Item):
|
||||
raise TypeError(f'expected Item not {item.__class__.__name__}')
|
||||
if item._is_v2() and not self._is_layout():
|
||||
raise ValueError('v2 items cannot be added to this view')
|
||||
|
||||
self.__weights.add_item(item)
|
||||
item._update_view(self)
|
||||
added = 1
|
||||
|
||||
item._view = self
|
||||
if item._has_children():
|
||||
added += len(tuple(item.walk_children())) # type: ignore
|
||||
|
||||
if self._is_layout() and self._total_children + added > 40:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
self._total_children += added
|
||||
self._children.append(item)
|
||||
return self
|
||||
|
||||
@ -357,7 +434,15 @@ class View:
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self.__weights.remove_item(item)
|
||||
removed = 1
|
||||
if item._has_children():
|
||||
removed += len(tuple(item.walk_children())) # type: ignore
|
||||
|
||||
if self._total_children - removed < 0:
|
||||
self._total_children = 0
|
||||
else:
|
||||
self._total_children -= removed
|
||||
|
||||
return self
|
||||
|
||||
def clear_items(self) -> Self:
|
||||
@ -367,9 +452,31 @@ class View:
|
||||
chaining.
|
||||
"""
|
||||
self._children.clear()
|
||||
self.__weights.clear()
|
||||
self._total_children = 0
|
||||
return self
|
||||
|
||||
def find_item(self, id: int, /) -> Optional[Item[Self]]:
|
||||
"""Gets an item with :attr:`Item.id` set as ``id``, or ``None`` if
|
||||
not found.
|
||||
|
||||
.. warning::
|
||||
|
||||
This is **not the same** as ``custom_id``.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
id: :class:`int`
|
||||
The ID of the component.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[:class:`Item`]
|
||||
The item found, or ``None``.
|
||||
"""
|
||||
return _utils_get(self.walk_children(), id=id)
|
||||
|
||||
async def interaction_check(self, interaction: Interaction, /) -> bool:
|
||||
"""|coro|
|
||||
|
||||
@ -428,7 +535,7 @@ class View:
|
||||
try:
|
||||
item._refresh_state(interaction, interaction.data) # type: ignore
|
||||
|
||||
allow = await item.interaction_check(interaction) and await self.interaction_check(interaction)
|
||||
allow = await item._run_checks(interaction) and await self.interaction_check(interaction)
|
||||
if not allow:
|
||||
return
|
||||
|
||||
@ -440,7 +547,7 @@ class View:
|
||||
return await self.on_error(interaction, e, item)
|
||||
|
||||
def _start_listening_from_store(self, store: ViewStore) -> None:
|
||||
self.__cancel_callback = partial(store.remove_view)
|
||||
self.__cancel_callback = partial(store.remove_view) # type: ignore
|
||||
if self.timeout:
|
||||
if self.__timeout_task is not None:
|
||||
self.__timeout_task.cancel()
|
||||
@ -469,7 +576,7 @@ class View:
|
||||
# fmt: off
|
||||
old_state: Dict[str, Item[Any]] = {
|
||||
item.custom_id: item # type: ignore
|
||||
for item in self._children
|
||||
for item in self.walk_children()
|
||||
if item.is_dispatchable()
|
||||
}
|
||||
# fmt: on
|
||||
@ -536,13 +643,193 @@ class View:
|
||||
"""
|
||||
return await self.__stopped
|
||||
|
||||
def walk_children(self) -> Generator[Item[Any], None, None]:
|
||||
"""An iterator that recursively walks through all the children of this view
|
||||
and its children, if applicable.
|
||||
|
||||
Yields
|
||||
------
|
||||
:class:`Item`
|
||||
An item in the view.
|
||||
"""
|
||||
|
||||
for child in self.children:
|
||||
yield child
|
||||
|
||||
if child._has_children():
|
||||
yield from child.walk_children() # type: ignore
|
||||
|
||||
|
||||
class View(BaseView):
|
||||
"""Represents a UI view.
|
||||
|
||||
This object must be inherited to create a UI within Discord.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
timeout: Optional[:class:`float`]
|
||||
Timeout in seconds from last interaction with the UI before no longer accepting input.
|
||||
If ``None`` then there is no timeout.
|
||||
"""
|
||||
|
||||
__discord_ui_view__: ClassVar[bool] = True
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: List[ComponentPayload], *, timeout: Optional[float] = 180.0) -> View:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> View:
|
||||
...
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
super().__init_subclass__()
|
||||
|
||||
children: Dict[str, ItemLike] = {}
|
||||
for base in reversed(cls.__mro__):
|
||||
for name, member in base.__dict__.items():
|
||||
if hasattr(member, '__discord_ui_model_type__'):
|
||||
children[name] = member
|
||||
elif isinstance(member, Item) and member._is_v2():
|
||||
raise ValueError(f'{name} cannot be added to this View')
|
||||
|
||||
if len(children) > 25:
|
||||
raise TypeError('View cannot have more than 25 children')
|
||||
|
||||
cls.__view_children_items__ = children
|
||||
|
||||
def __init__(self, *, timeout: Optional[float] = 180.0):
|
||||
super().__init__(timeout=timeout)
|
||||
self.__weights = _ViewWeights(self._children)
|
||||
|
||||
def to_components(self) -> List[Dict[str, Any]]:
|
||||
def key(item: Item) -> int:
|
||||
return item._rendered_row or 0
|
||||
|
||||
children = sorted(self._children, key=key)
|
||||
components: List[Dict[str, Any]] = []
|
||||
for _, group in groupby(children, key=key):
|
||||
children = [item.to_component_dict() for item in group]
|
||||
if not children:
|
||||
continue
|
||||
|
||||
components.append(
|
||||
{
|
||||
'type': 1,
|
||||
'components': children,
|
||||
}
|
||||
)
|
||||
|
||||
return components
|
||||
|
||||
def add_item(self, item: Item[Any]) -> Self:
|
||||
if len(self._children) >= 25:
|
||||
raise ValueError('maximum number of children exceeded')
|
||||
|
||||
super().add_item(item)
|
||||
try:
|
||||
self.__weights.add_item(item)
|
||||
except ValueError as e:
|
||||
# if the item has no space left then remove it from _children
|
||||
self._children.remove(item)
|
||||
raise e
|
||||
|
||||
return self
|
||||
|
||||
def remove_item(self, item: Item[Any]) -> Self:
|
||||
try:
|
||||
self._children.remove(item)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self.__weights.remove_item(item)
|
||||
return self
|
||||
|
||||
def clear_items(self) -> Self:
|
||||
super().clear_items()
|
||||
self.__weights.clear()
|
||||
return self
|
||||
|
||||
|
||||
class LayoutView(BaseView):
|
||||
"""Represents a layout view for components.
|
||||
|
||||
This object must be inherited to create a UI within Discord.
|
||||
|
||||
You can find usage examples in the :resource:`repository <examples>`
|
||||
|
||||
.. versionadded:: 2.6
|
||||
|
||||
Parameters
|
||||
----------
|
||||
timeout: Optional[:class:`float`]
|
||||
Timeout in seconds from last interaction with the UI before no longer accepting input.
|
||||
If ``None`` then there is no timeout.
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: List[ComponentPayload], *, timeout: Optional[float] = 180.0) -> LayoutView:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def from_message(cls, message: Message, /, *, timeout: Optional[float] = 180.0) -> LayoutView:
|
||||
...
|
||||
|
||||
def __init__(self, *, timeout: Optional[float] = 180.0) -> None:
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
if self._total_children > 40:
|
||||
raise ValueError('maximum number of children exceeded (40)')
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
super().__init_subclass__()
|
||||
|
||||
children: Dict[str, ItemLike] = {}
|
||||
callback_children: Dict[str, ItemCallbackType[Any, Any]] = {}
|
||||
|
||||
for base in reversed(cls.__mro__):
|
||||
for name, member in base.__dict__.items():
|
||||
if isinstance(member, Item):
|
||||
if member._parent is not None:
|
||||
continue
|
||||
|
||||
member._rendered_row = member._row
|
||||
children[name] = member
|
||||
elif hasattr(member, '__discord_ui_model_type__') and getattr(member, '__discord_ui_parent__', None):
|
||||
callback_children[name] = member
|
||||
|
||||
children.update(callback_children)
|
||||
cls.__view_children_items__ = children
|
||||
|
||||
def _is_layout(self) -> TypeGuard[LayoutView]: # type: ignore
|
||||
return True
|
||||
|
||||
def to_components(self):
|
||||
components: List[Dict[str, Any]] = []
|
||||
for i in self._children:
|
||||
components.append(i.to_component_dict())
|
||||
|
||||
return components
|
||||
|
||||
def add_item(self, item: Item[Any]) -> Self:
|
||||
if self._total_children >= 40:
|
||||
raise ValueError('maximum number of children exceeded (40)')
|
||||
super().add_item(item)
|
||||
return self
|
||||
|
||||
|
||||
class ViewStore:
|
||||
def __init__(self, state: ConnectionState):
|
||||
# entity_id: {(component_type, custom_id): Item}
|
||||
self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[View]]] = {}
|
||||
self._views: Dict[Optional[int], Dict[Tuple[int, str], Item[BaseView]]] = {}
|
||||
# message_id: View
|
||||
self._synced_message_views: Dict[int, View] = {}
|
||||
self._synced_message_views: Dict[int, BaseView] = {}
|
||||
# custom_id: Modal
|
||||
self._modals: Dict[str, Modal] = {}
|
||||
# component_type is the key
|
||||
@ -550,7 +837,7 @@ class ViewStore:
|
||||
self._state: ConnectionState = state
|
||||
|
||||
@property
|
||||
def persistent_views(self) -> Sequence[View]:
|
||||
def persistent_views(self) -> Sequence[BaseView]:
|
||||
# fmt: off
|
||||
views = {
|
||||
item.view.id: item.view
|
||||
@ -571,7 +858,7 @@ class ViewStore:
|
||||
pattern = item.__discord_ui_compiled_template__
|
||||
self._dynamic_items.pop(pattern, None)
|
||||
|
||||
def add_view(self, view: View, message_id: Optional[int] = None) -> None:
|
||||
def add_view(self, view: BaseView, message_id: Optional[int] = None) -> None:
|
||||
view._start_listening_from_store(self)
|
||||
if view.__discord_ui_modal__:
|
||||
self._modals[view.custom_id] = view # type: ignore
|
||||
@ -579,7 +866,7 @@ class ViewStore:
|
||||
|
||||
dispatch_info = self._views.setdefault(message_id, {})
|
||||
is_fully_dynamic = True
|
||||
for item in view._children:
|
||||
for item in view.walk_children():
|
||||
if isinstance(item, DynamicItem):
|
||||
pattern = item.__discord_ui_compiled_template__
|
||||
self._dynamic_items[pattern] = item.__class__
|
||||
@ -621,15 +908,16 @@ class ViewStore:
|
||||
if interaction.message is None:
|
||||
return
|
||||
|
||||
view = View.from_message(interaction.message, timeout=None)
|
||||
view_cls = View if not interaction.message.flags.components_v2 else LayoutView
|
||||
view = view_cls.from_message(interaction.message, timeout=None)
|
||||
|
||||
try:
|
||||
base_item_index, base_item = next(
|
||||
(index, child)
|
||||
for index, child in enumerate(view._children)
|
||||
if child.type.value == component_type and getattr(child, 'custom_id', None) == custom_id
|
||||
)
|
||||
except StopIteration:
|
||||
base_item = _utils_find(
|
||||
lambda i: i.type.value == component_type and getattr(i, 'custom_id', None) == custom_id,
|
||||
view.walk_children(),
|
||||
)
|
||||
|
||||
# if the item is not found then return
|
||||
if not base_item:
|
||||
return
|
||||
|
||||
try:
|
||||
@ -638,8 +926,17 @@ class ViewStore:
|
||||
_log.exception('Ignoring exception in dynamic item creation for %r', factory)
|
||||
return
|
||||
|
||||
# Swap the item in the view with our new dynamic item
|
||||
view._children[base_item_index] = item
|
||||
# Swap the item in the view or parent with our new dynamic item
|
||||
# Prioritize the item parent:
|
||||
parent = base_item._parent or view
|
||||
|
||||
try:
|
||||
child_index = parent._children.index(base_item) # type: ignore
|
||||
except ValueError:
|
||||
return
|
||||
else:
|
||||
parent._children[child_index] = item # type: ignore
|
||||
|
||||
item._view = view
|
||||
item._rendered_row = base_item._rendered_row
|
||||
item._refresh_state(interaction, interaction.data) # type: ignore
|
||||
@ -681,7 +978,7 @@ class ViewStore:
|
||||
key = (component_type, custom_id)
|
||||
|
||||
# The entity_id can either be message_id, interaction_id, or None in that priority order.
|
||||
item: Optional[Item[View]] = None
|
||||
item: Optional[Item[BaseView]] = None
|
||||
if message_id is not None:
|
||||
item = self._views.get(message_id, {}).get(key)
|
||||
|
||||
@ -733,14 +1030,14 @@ class ViewStore:
|
||||
def is_message_tracked(self, message_id: int) -> bool:
|
||||
return message_id in self._synced_message_views
|
||||
|
||||
def remove_message_tracking(self, message_id: int) -> Optional[View]:
|
||||
def remove_message_tracking(self, message_id: int) -> Optional[BaseView]:
|
||||
return self._synced_message_views.pop(message_id, None)
|
||||
|
||||
def update_from_message(self, message_id: int, data: List[ComponentPayload]) -> None:
|
||||
def update_from_message(self, message_id: int, data: List[ComponentBasePayload]) -> None:
|
||||
components: List[Component] = []
|
||||
|
||||
for component_data in data:
|
||||
component = _component_factory(component_data)
|
||||
component = _component_factory(component_data, self._state) # type: ignore
|
||||
|
||||
if component is not None:
|
||||
components.append(component)
|
||||
|
Reference in New Issue
Block a user