diff --git a/discord/ui/view.py b/discord/ui/view.py index 8dc6b01aa..d7168d95c 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -207,6 +207,24 @@ class _ViewWeights: self.weights = [0, 0, 0, 0, 0] +class _ViewCacheSnapshot: + __slots__ = ('items', 'dynamic_items') + + def __init__(self) -> None: + self.items: Set[Tuple[int, str]] = set() + self.dynamic_items: Set[re.Pattern[str]] = set() + + @classmethod + def diff(cls, older: _ViewCacheSnapshot, newer: _ViewCacheSnapshot) -> Self: + self = cls() + self.items = older.items - newer.items + self.dynamic_items = older.dynamic_items - newer.dynamic_items + return self + + def __repr__(self) -> str: + return f'<_ViewCacheSnapshot items={self.items!r} dynamic_items={self.dynamic_items!r}>' + + class BaseView: __discord_ui_view__: ClassVar[bool] = False __discord_ui_modal__: ClassVar[bool] = False @@ -220,6 +238,7 @@ class BaseView: self.__cancel_callback: Optional[Callable[[BaseView], None]] = None self.__timeout_expiry: Optional[float] = None self.__timeout_task: Optional[asyncio.Task[None]] = None + self.__snapshot: Optional[_ViewCacheSnapshot] = None try: loop = asyncio.get_running_loop() @@ -326,6 +345,31 @@ class BaseView: def _add_count(self, value: int) -> None: self._total_children = max(0, self._total_children + value) + @property + def _snapshot(self) -> Optional[_ViewCacheSnapshot]: + return self.__snapshot + + def _get_snapshot_diff(self) -> Optional[_ViewCacheSnapshot]: + if self.__snapshot is None: + self.__snapshot = self._get_snapshot() + return None + + newer = self._get_snapshot() + diff = _ViewCacheSnapshot.diff(older=self.__snapshot, newer=newer) + # Update our snapshot to the newer version after diffing it + self.__snapshot = newer + return diff + + def _get_snapshot(self) -> _ViewCacheSnapshot: + snapshot = _ViewCacheSnapshot() + for item in self.walk_children(): + if isinstance(item, DynamicItem): + snapshot.dynamic_items.add(item.__discord_ui_compiled_template__) + elif item.is_dispatchable(): + custom_id = item.custom_id # type: ignore + snapshot.items.add((item.type.value, custom_id)) + return snapshot + @property def children(self) -> List[Item[Self]]: """List[:class:`Item`]: The list of children attached to this view.""" @@ -901,6 +945,7 @@ class ViewStore: dispatch_info = self._views.get(message_id, {}) is_fully_dynamic = True + snapshot = view._get_snapshot_diff() for item in view.walk_children(): if isinstance(item, DynamicItem): pattern = item.__discord_ui_compiled_template__ @@ -909,6 +954,12 @@ class ViewStore: dispatch_info[(item.type.value, item.custom_id)] = item # type: ignore is_fully_dynamic = False + if snapshot is not None: + for key in snapshot.items: + dispatch_info.pop(key, None) + for key in snapshot.dynamic_items: + self._dynamic_items.pop(key, None) + view._cache_key = message_id if dispatch_info: self._views[message_id] = dispatch_info @@ -922,13 +973,12 @@ class ViewStore: return dispatch_info = self._views.get(view._cache_key) - if dispatch_info: - for item in view.walk_children(): - if isinstance(item, DynamicItem): - pattern = item.__discord_ui_compiled_template__ - self._dynamic_items.pop(pattern, None) - elif item.is_dispatchable(): - dispatch_info.pop((item.type.value, item.custom_id), None) # type: ignore + snapshot = view._snapshot + if dispatch_info and snapshot: + for key in snapshot.items: + dispatch_info.pop(key, None) + for key in snapshot.dynamic_items: + self._dynamic_items.pop(key, None) if dispatch_info is not None and len(dispatch_info) == 0: self._views.pop(view._cache_key, None)