Lexi Winter: 5 api.fetch_timeline(): add since_id parameter api.timeline: rename create_generator() to fetch() StatusTimeline: make fetch() a StatusTimeline member Timeline: add update() method TimelineTab: use update() to refresh 7 files changed, 145 insertions(+), 61 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~ihabunek/toot-discuss/patches/48468/mbox | git am -3Learn more about email & git
--- tooi/api/timeline.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tooi/api/timeline.py b/tooi/api/timeline.py index d93e46b..d8c0d07 100644 --- a/tooi/api/timeline.py +++ b/tooi/api/timeline.py @@ -40,10 +40,12 @@ async def fetch_timeline( instance: InstanceInfo, path: str, params: Params | None = None, - limit: int | None = None): + limit: int | None = None, + since_id: str | None = None): _params = dict(params or {}) _params["limit"] = limit or DEFAULT_LIMIT + _params["since_id"] = since_id next_path = path while next_path: -- 2.43.0
Thanks, applied. -- Ivan
This more accurately describes what the function does. --- tooi/api/timeline.py | 16 ++++++++-------- tooi/tabs/timeline.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tooi/api/timeline.py b/tooi/api/timeline.py index d8c0d07..ccb3d30 100644 --- a/tooi/api/timeline.py +++ b/tooi/api/timeline.py @@ -64,7 +64,7 @@ class Timeline(ABC): self.name = name @abstractmethod - def create_generator(self, limit: int | None = None) -> EventGenerator: + def fetch(self, limit: int | None = None) -> EventGenerator: ... @@ -95,7 +95,7 @@ class HomeTimeline(StatusTimeline): def __init__(self, instance: InstanceInfo): super().__init__("Home", instance) - def create_generator(self, limit: int | None = None): + def fetch(self, limit: int | None = None): return self.status_generator("/api/v1/timelines/home", limit=limit) @@ -117,7 +117,7 @@ class LocalTimeline(PublicTimeline): def __init__(self, instance: InstanceInfo): super().__init__("Local", instance, True) - def create_generator(self, limit: int | None = None): + def fetch(self, limit: int | None = None): return self.public_timeline_generator(limit=limit) @@ -129,7 +129,7 @@ class FederatedTimeline(PublicTimeline): def __init__(self, instance: InstanceInfo): super().__init__("Federated", instance, False) - def create_generator(self, limit: int | None = None): + def fetch(self, limit: int | None = None): return self.public_timeline_generator(limit=limit) @@ -159,7 +159,7 @@ class AccountTimeline(StatusTimeline): account = await get_account_by_name(account_name) return AccountTimeline(instance, account_name, account.id, replies, reblogs) - def create_generator(self, limit: int | None = None): + def fetch(self, limit: int | None = None): path = f"/api/v1/accounts/{self.account_id}/statuses" params = { "exclude_replies": not self.replies, @@ -201,7 +201,7 @@ class NotificationTimeline(Timeline): async for events in fetch_timeline(self.instance, path, params, limit): yield [e for e in map(self.make_notification_event, events) if e is not None] - def create_generator(self, limit: int | None = None): + def fetch(self, limit: int | None = None): # TODO: not included: follow_request, poll, update, admin.sign_up, admin.report types = ["mention", "status", "reblog", "favourite", "follow"] params = {"types[]": types} @@ -226,7 +226,7 @@ class TagTimeline(StatusTimeline): self.hashtag = hashtag super().__init__(f"#{self.hashtag}", instance) - def create_generator(self, limit: int = 40): + def fetch(self, limit: int = 40): path = f"/api/v1/timelines/tag/{quote(self.hashtag)}" return self.status_generator(path, limit=limit) @@ -248,7 +248,7 @@ class ContextTimeline(Timeline): all_statuses = ancestors + [status] + descendants yield [StatusEvent(self.instance, s) for s in all_statuses] - def create_generator(self, limit: int | None = None): + def fetch(self, limit: int | None = None): return self.context_timeline_generator(self._status, limit) diff --git a/tooi/tabs/timeline.py b/tooi/tabs/timeline.py index 6d950f5..59cee63 100644 --- a/tooi/tabs/timeline.py +++ b/tooi/tabs/timeline.py @@ -81,7 +81,7 @@ class TimelineTab(TabPane): return make_event_detail(event) async def refresh_timeline(self): - self.generator = self.timeline.create_generator() + self.generator = self.timeline.fetch() events = await anext(self.generator) self.event_list.replace(events) self.query_one("#main_window").mount(self.event_detail) -- 2.43.0
StatusTimeline now takes path and params as constructor parameters and provides an implementation of fetch() based on those. This removes the need to reimplement fetch() in every derived class. There should be no reason for these parameters to change after the class is constructed. --- tooi/api/timeline.py | 48 ++++++++++++++++---------------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/tooi/api/timeline.py b/tooi/api/timeline.py index ccb3d30..af5b105 100644 --- a/tooi/api/timeline.py +++ b/tooi/api/timeline.py @@ -73,16 +73,13 @@ class StatusTimeline(Timeline): StatusTimeline is the base class for timelines which only return statuses. """ - def __init__(self, name: str, instance: InstanceInfo): + def __init__(self, name: str, instance: InstanceInfo, path: str, params: Params | None = None): super().__init__(name, instance) + self.path = path + self.params = params - async def status_generator( - self, - path: str, - params: Params = None, - limit: int | None = None) -> EventGenerator: - - async for events in fetch_timeline(self.instance, path, params, limit): + async def fetch(self, limit: int | None = None) -> EventGenerator: + async for events in fetch_timeline(self.instance, self.path, self.params, limit): yield [StatusEvent(self.instance, from_dict(Status, s)) for s in events] @@ -93,21 +90,15 @@ class HomeTimeline(StatusTimeline): """ def __init__(self, instance: InstanceInfo): - super().__init__("Home", instance) - - def fetch(self, limit: int | None = None): - return self.status_generator("/api/v1/timelines/home", limit=limit) + super().__init__("Home", instance, "/api/v1/timelines/home") class PublicTimeline(StatusTimeline): """PublicTimeline loads events from the public timeline.""" def __init__(self, name: str, instance: InstanceInfo, local: bool): - super().__init__(name, instance) + super().__init__(name, instance, "/api/v1/timelines/public", {"local": local}) self.local = local - def public_timeline_generator(self, limit: int | None = None): - return self.status_generator("/api/v1/timelines/public", {"local": self.local}, limit) - class LocalTimeline(PublicTimeline): """ @@ -117,9 +108,6 @@ class LocalTimeline(PublicTimeline): def __init__(self, instance: InstanceInfo): super().__init__("Local", instance, True) - def fetch(self, limit: int | None = None): - return self.public_timeline_generator(limit=limit) - class FederatedTimeline(PublicTimeline): """ @@ -129,9 +117,6 @@ class FederatedTimeline(PublicTimeline): def __init__(self, instance: InstanceInfo): super().__init__("Federated", instance, False) - def fetch(self, limit: int | None = None): - return self.public_timeline_generator(limit=limit) - class AccountTimeline(StatusTimeline): """ @@ -145,11 +130,20 @@ class AccountTimeline(StatusTimeline): account_id: str, replies=True, reblogs=True): - super().__init__(title, instance) + self.account_id = account_id self.replies = replies self.reblogs = reblogs + super().__init__( + title, + instance, + f"/api/v1/accounts/{self.account_id}/statuses", + { + "exclude_replies": not self.replies, + "exclude_reblogs": not self.reblogs + }) + @staticmethod async def from_name( instance: InstanceInfo, @@ -159,14 +153,6 @@ class AccountTimeline(StatusTimeline): account = await get_account_by_name(account_name) return AccountTimeline(instance, account_name, account.id, replies, reblogs) - def fetch(self, limit: int | None = None): - path = f"/api/v1/accounts/{self.account_id}/statuses" - params = { - "exclude_replies": not self.replies, - "exclude_reblogs": not self.reblogs - } - return self.status_generator(path, params, limit) - class NotificationTimeline(Timeline): """ -- 2.43.0
update() is similar to fetch(), but it only fetches events which are new since the previous call to update() (or the initial call to fetch()). --- tooi/api/timeline.py | 97 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 14 deletions(-) diff --git a/tooi/api/timeline.py b/tooi/api/timeline.py index af5b105..d494e64 100644 --- a/tooi/api/timeline.py +++ b/tooi/api/timeline.py @@ -50,8 +50,13 @@ async def fetch_timeline( next_path = path while next_path: response = await request("GET", next_path, params=_params) - yield response.json() - next_path = _get_next_path(response.headers) + events = response.json() + + if len(events) == 0: + next_path = None + else: + yield events + next_path = _get_next_path(response.headers) class Timeline(ABC): @@ -77,10 +82,36 @@ class StatusTimeline(Timeline): super().__init__(name, instance) self.path = path self.params = params + self._most_recent_id = None async def fetch(self, limit: int | None = None) -> EventGenerator: - async for events in fetch_timeline(self.instance, self.path, self.params, limit): - yield [StatusEvent(self.instance, from_dict(Status, s)) for s in events] + async for eventlist in fetch_timeline(self.instance, self.path, self.params, limit): + events = [StatusEvent(self.instance, from_dict(Status, s)) for s in eventlist] + + # Track the most recent id we've fetched, which will be the first, for update(). + if self._most_recent_id is None and len(events) > 0: + self._most_recent_id = events[0].status.id + + yield events + + async def update(self, limit: int | None = None) -> EventGenerator: + eventslist = fetch_timeline( + self.instance, + self.path, + self.params, + limit, + self._most_recent_id) + + updated_most_recent = False + + async for eventlist in eventslist: + events = [StatusEvent(self.instance, from_dict(Status, s)) for s in eventlist] + + if updated_most_recent == False and len(events) > 0: + updated_most_recent = True + self._most_recent_id = events[0].status.id + + yield events class HomeTimeline(StatusTimeline): @@ -159,8 +190,13 @@ class NotificationTimeline(Timeline): NotificationTimeline loads events from the user's notifications. https://docs.joinmastodon.org/methods/notifications/ """ + + # TODO: not included: follow_request, poll, update, admin.sign_up, admin.report + TYPES = [ "mention", "follow", "favourite", "reblog"] + def __init__(self, instance: InstanceInfo): super().__init__("Notifications", instance) + self._most_recent_id = None def make_notification_event(self, response: dict) -> Event | None: notification = from_dict(Notification, response) @@ -185,13 +221,35 @@ class NotificationTimeline(Timeline): path = "/api/v1/notifications" async for events in fetch_timeline(self.instance, path, params, limit): - yield [e for e in map(self.make_notification_event, events) if e is not None] + events = [e for e in map(self.make_notification_event, events) if e is not None] + + # Track the most recent id we've fetched, which will be the first, for update(). + if self._most_recent_id is None and len(events) > 0: + self._most_recent_id = events[0].notification.id + + yield events def fetch(self, limit: int | None = None): - # TODO: not included: follow_request, poll, update, admin.sign_up, admin.report - types = ["mention", "status", "reblog", "favourite", "follow"] - params = {"types[]": types} - return self.notification_generator(params, limit) + return self.notification_generator({ "types[]": self.TYPES}, limit) + + async def update(self, limit: int | None = None) -> EventGenerator: + eventslist = fetch_timeline( + self.instance, + "/api/v1/notifications", + params={ "types[]": self.TYPES }, + limit=limit, + since_id=self._most_recent_id) + + updated_most_recent = False + + async for eventlist in eventslist: + events = [e for e in map(self.make_notification_event, eventlist) if e is not None] + + if updated_most_recent == False and len(events) > 0: + updated_most_recent = True + self._most_recent_id = events[0].notification.id + + yield events class TagTimeline(StatusTimeline): @@ -199,7 +257,14 @@ class TagTimeline(StatusTimeline): TagTimeline loads events from the given hashtag. This timeline only ever returns events of type StatusEvent. """ - def __init__(self, instance: InstanceInfo, hashtag: str, local: bool = False): + def __init__( + self, + instance: + InstanceInfo, + hashtag: str, + local: bool = False, + remote: bool=False): + self.local = local # Normalise the hashtag to not begin with a hash @@ -210,11 +275,15 @@ class TagTimeline(StatusTimeline): raise (ValueError("TagTimeline: tag is empty")) self.hashtag = hashtag - super().__init__(f"#{self.hashtag}", instance) - def fetch(self, limit: int = 40): - path = f"/api/v1/timelines/tag/{quote(self.hashtag)}" - return self.status_generator(path, limit=limit) + super().__init__( + f"#{self.hashtag}", + instance, + f"/api/v1/timelines/tag/{quote(self.hashtag)}", + params={ + "local": local, + "remote": remote, + }) class ContextTimeline(Timeline): -- 2.43.0
When refreshing a timeline, instead of calling fetch() again, call update() and prepend the new events to the event_list. This has three benefits: - it's quicker - it creates less network traffic - it leaves the EventList cursor where it is, so the user can easily see which statuses are new. --- tooi/tabs/timeline.py | 19 +++++++++++++++++-- tooi/widgets/event_list.py | 20 ++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/tooi/tabs/timeline.py b/tooi/tabs/timeline.py index 59cee63..5cc56c4 100644 --- a/tooi/tabs/timeline.py +++ b/tooi/tabs/timeline.py @@ -65,7 +65,7 @@ class TimelineTab(TabPane): async def on_mount(self, message): self.event_detail.focus() - await self.refresh_timeline() + await self.fetch_timeline() if self.initial_focus: self.event_list.focus_event(self.initial_focus) @@ -81,6 +81,21 @@ class TimelineTab(TabPane): return make_event_detail(event) async def refresh_timeline(self): + # Handle timelines that don't support updating. + if not hasattr(self.timeline, 'update'): + await self.fetch_timeline() + return + + newevents = [] + + async for eventslist in self.timeline.update(): + newevents += eventslist + + # The updates are returned in inverse chronological order, so reverse them before adding. + newevents.reverse() + self.event_list.prepend_events(newevents) + + async def fetch_timeline(self): self.generator = self.timeline.fetch() events = await anext(self.generator) self.event_list.replace(events) @@ -151,7 +166,7 @@ class TimelineTab(TabPane): # TODO: handle exceptions try: next_events = await anext(self.generator) - self.event_list.update(next_events) + self.event_list.append_events(next_events) finally: self.fetching = False self.status_bar.update() diff --git a/tooi/widgets/event_list.py b/tooi/widgets/event_list.py index e6bf8e6..d7dee17 100644 --- a/tooi/widgets/event_list.py +++ b/tooi/widgets/event_list.py @@ -31,15 +31,14 @@ class EventList(ListView): super().__init__() self.events = [] self.current = None - self.update(events) + self.append_events(events) def replace(self, next_events: list[Event]): - self.events = [] self.clear() self.current = None - self.update(next_events) + self.append_events(next_events) - def update(self, next_events: list[Event]): + def append_events(self, next_events: list[Event]): self.events += next_events for event in next_events: @@ -52,6 +51,19 @@ class EventList(ListView): if self.current is not None: self.post_message(EventHighlighted(self.current)) + def prepend_events(self, next_events: list[Event]): + self.events = next_events + self.events + + for event in next_events: + self.mount(EventListItem(event), before=0) + + if self.current is None and len(self.events) > 0: + self.index = 0 + self.current = self.events[0] + + if self.current is not None: + self.post_message(EventHighlighted(self.current)) + def focus_event(self, event_id: str): for i, event in enumerate(self.events): if event.id == event_id: -- 2.43.0