Skip to content

Commit

Permalink
feat: Add support for loading more threads
Browse files Browse the repository at this point in the history
  • Loading branch information
maxking committed Aug 2, 2023
1 parent 21a0d51 commit 34db36f
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 104 deletions.
33 changes: 19 additions & 14 deletions src/archive_reader/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,21 @@
from textual import log, work
from textual._node_list import DuplicateIds
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.containers import Vertical
from textual.css.query import NoMatches

from textual.reactive import var
from textual.screen import Screen
from textual.widgets import (
Footer,
Input,
ListItem,
ListView,
LoadingIndicator,
SelectionList,
Static,
)

from .models import initialize_database
from .widgets import (
Threads,
MailingListItem,
MailingListChoose,
MailingLists,
ThreadItem,
Header,
EmailItem,
)
from .core import ListManager, ThreadsManager
from .screens import ThreadReadScreen, MailingListAddScreen
Expand All @@ -46,6 +37,7 @@ class ArchiveApp(App):
('s', 'app.screenshot()', 'Screenshot'),
('q', 'quit', 'Quit'),
('u', 'load_new_threads', 'Update threads'),
('m', 'more_threads', 'More threads'),
]
TITLE = 'Archive Reader'
SUB_TITLE = 'An app to reach Hyperkitty archives in Terminal!'
Expand All @@ -57,6 +49,7 @@ def __init__(self, *args, **kw):
self._existing_threads = defaultdict(dict)
self.list_manager = ListManager()
self.thread_mgrs = {}
self.thread_page = defaultdict(int)

def watch_show_tree(self, show_tree: bool) -> None:
"""Called when show_tree is modified."""
Expand Down Expand Up @@ -150,7 +143,7 @@ def thread_mgr(self):
async def update_threads(self, ml):
header = self.query_one('#header', Header)
header.text = '{} ({})'.format(ml.display_name, ml.name)
await self._clear_threads()
# await self._clear_threads()
self._show_loading()
self.current_mailinglist = ml
mgr = self.thread_mgr()
Expand All @@ -162,11 +155,12 @@ async def update_threads(self, ml):
self._notify_update_complete()

@work()
async def action_load_new_threads(self):
async def action_load_new_threads(self, offset=0):
ml = self.current_mailinglist
mgr = self.thread_mgr()
await mgr.update_threads()
self.update_threads(ml)
new_threads = await mgr.update_threads(offset=offset)
for thread in new_threads:
await self._set_thread(thread)

async def on_list_view_selected(self, item):
# Handle the list item selected for MailingList.
Expand All @@ -188,6 +182,17 @@ async def on_list_view_selected(self, item):
)
)

async def action_more_threads(self):
"""Load more threads for the Current Mailinglist."""
self.thread_page[self.current_mailinglist.name] += 1
threads_container = self.query_one('#threads', ListView)
self.action_load_new_threads(
offset=len(threads_container.children),
)
self.notify(
f'Loading more threads for {self.current_mailinglist.name}.'
)

# @work
# async def on_thread_updated(self, item):
# self._existing_threads[self.current_mailinglist.name][
Expand Down
32 changes: 32 additions & 0 deletions src/archive_reader/archiver.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,36 @@ Screen {
}
.sender {
padding: 0 1;
}

ThreadItem {
height: 3;
width: 1fr;
layout: grid;
grid-size: 3;
grid-columns: 14fr 1fr 2fr;
content-align: left middle;
padding: 1 1;
}

.read {
background: gray;
}

EmailItem {
width: 1fr;
margin: 1 1;
height: auto;
padding: 2;
}
Label {
padding: 1 2;
}

ListView > ListItem.--highlight {
background: $secondary-background-lighten-3 20%;
}

ListView:focus > ListItem.--highlight {
background: $secondary-background-lighten-3 50%;
}
10 changes: 6 additions & 4 deletions src/archive_reader/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ async def threads(self):
"""
return await self._load_threads_from_db()

async def update_threads(self):
return await self._fetch_threads()
async def update_threads(self, offset):
return await self._fetch_threads(offset)

async def emails(self, thread):
"""Return all the Emails for a give Thread."""
Expand Down Expand Up @@ -75,12 +75,14 @@ async def _load_threads_from_db(self):
"""Load all the existing threads from the db."""
return await Thread.objects.filter(mailinglist=self.ml.url).all()

async def _fetch_threads(self, page: int = 1):
async def _fetch_threads(self, offset: int = 0):
"""Fetch threads from the remote server.
:param page: The page no. to fetch.
"""
threads = await hyperktty_client.threads(self.ml.threads)
threads = await hyperktty_client.threads(
self.ml.threads, offset=offset
)
thread_objs = []
for thread in threads.get('results'):
obj = await Thread.objects.update_or_create(
Expand Down
17 changes: 13 additions & 4 deletions src/archive_reader/hyperkitty.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
]


DEFAULT_PAGINATION_COUNT = 25


class HyperkittyAPI:
"""Hyperkitty is a client for Hyperkitty. It returns objects that can be
used in the UI elements.
Expand All @@ -28,12 +31,18 @@ async def lists(self, base_url):
url = f'{base_url}/api/lists?format=json'
return await self._call(url, MailingListPage)

async def threads(self, threads_url):
async def threads(
self, threads_url, offset=1, limit=DEFAULT_PAGINATION_COUNT
):
"""Given a ML object, return the threads for that Mailinglist."""
return await self._call(threads_url, ThreadsPage)
return await self._call(
f'{threads_url}&limit={limit}&offset={offset}', ThreadsPage
)

async def emails(self, thread):
return await self._call(thread.get('emails'), EmailsPage)
async def emails(self, thread, page=1, count=DEFAULT_PAGINATION_COUNT):
return await self._call(
f'{thread.emails}&page={page}&count={count}', EmailsPage
)


async def fetch_urls(urls, logger=None):
Expand Down
103 changes: 21 additions & 82 deletions src/archive_reader/widgets.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
from collections import defaultdict
from datetime import datetime
from zoneinfo import ZoneInfo
from textual.app import ComposeResult

import timeago
from rich.console import RenderableType
from textual import events, log
from textual import events
from textual.containers import ScrollableContainer
from textual.message import Message
from textual.reactive import reactive
Expand Down Expand Up @@ -83,6 +77,12 @@ def compose(self):
)

def on_button_pressed(self, event: Button.Pressed):
"""Handle MailingList selected event.
This will simply post a MailingListChoose.Selected message, which will
be then handled by the handler in the ArchiveApp() to subscribe the
lists chosen by the user.
"""
if event.button.id == 'select_mailinglist':
self.post_message(
self.Selected(self.query_one(SelectionList).selected)
Expand All @@ -91,37 +91,26 @@ def on_button_pressed(self, event: Button.Pressed):


class ThreadReplies(Widget):
"""Represents total no. of messages in the Threads."""

#: Represents if this thread has new messages since it was
#: last opened. Currently this is un-used.
has_new = reactive(0)
#: Total no of replies in the thread.
count = reactive(0)

def __init__(self, count, has_new, *args, **kw):
def __init__(self, count, has_new=0, *args, **kw):
super().__init__(*args, **kw)
self.count = count
self.has_new = has_new

def render(self):
return f':speech_balloon: {self.count} ({self.has_new})'
return f':speech_balloon: {self.count}'


class ThreadItem(ListItem):
"""Represents a thread on the Main screen."""

DEFAULT_CSS = """
ThreadItem {
height: 3;
width: 1fr;
layout: grid;
grid-size: 3;
grid-columns: 14fr 1fr 2fr;
content-align: left middle;
padding: 1 1;
}
.read {
background: gray;
}
"""
#: Represents whether this thread has been opened in the current
#: reader before. This is computed locally and turned to 'read'
#: as soon as the thread is opened.
Expand All @@ -145,72 +134,40 @@ def __init__(self, thread):
super().__init__()

class Updated(Message):
"""Represents an updated Event for the thread. This is sent
out so that any handlers that exist can refresh the view.
"""

def __init__(self, thread_data):
self.data = thread_data
super().__init__()

def __init__(self, *args, thread=None, mailinglist=None, **kw) -> None:
super().__init__(*args, **kw)
self.mailinglist = mailinglist
# self.is_new = thread_data.get('is_new', False)
# self.has_new = thread_data.get('has_new', 0)
# self.read = thread_data.get('read', False)
self.thread = thread
if self.read:
self.add_class('read')

def get(self, attr):
"""Get the attribute of the owned Thread object."""
return getattr(self.thread, attr)

@property
def subject(self):
return self.thread.subject

def time_format(self):
"""Return thread's active_time formatted properly to show in UI."""
return self.thread.date_active

# def watch_read(self, old, new):
# if old is False and new is True:
# self.add_class('read')
# # Regardless of the current value, just turn these two off since
# # they are not required anymore.
# self.is_new = False
# self.has_new = 0
# self.data['is_new'] = False
# self.data['has_new'] = 0
# self.data['read'] = True
# self.read = True
# log(f'Sending ThreadUpdated for {self}')
# self._notify_updated()
# self._save_read_status(new)

def _notify_updated(self):
"""Sends out Message for thread's updated event."""
self.post_message(self.Updated(self.thread))

def _save_read_status(self, new):
# Update the thread.read() status in the storage.
# XXX(abraj): This is a relatively complex operation. The reason
# for which is the fact that we are using a caching solution as a
# trivial json database in a way that doesn't provide tons of data
# access patterns that we want to have.
# This can be solved easily with a local Sqlite database in future,
# infact, the current caching solution utilizes sqlite underneath.
pass

# def watch_has_new(self, _, new):
# # It is possible that this is b
# try:
# self.query_one(ThreadReplies).has_new = new
# except Exception:
# log(f'Failed to Find & Update thread replies')

def compose(self):
yield Static(self.subject)
yield ThreadReplies(
count=self.thread.replies_count, has_new=self.has_new
)
# now = datetime.now(tz=ZoneInfo('Asia/Kolkata'))
thread_date = self.time_format()
yield ThreadReplies(count=self.thread.replies_count)
yield Static(':two-thirty: {}'.format(self.get('date_active')))

async def _on_click(self, _: events.Click) -> None:
Expand Down Expand Up @@ -261,24 +218,6 @@ class EmailItem(ListItem):
of the instance. You can get the values of those using the `.get()` method.
"""

DEFAULT_CSS = """
Email {
width: 1fr;
margin: 1 1;
height: auto;
padding: 2;
}
Label {
padding: 1 2;
}
ListView > ListItem.--highlight {
background: $secondary-background-lighten-3 20%;
}
ListView:focus > ListItem.--highlight {
background: $secondary-background-lighten-3 50%;
}
"""

def __init__(self, *args, email=None, **kw):
super().__init__(*args, **kw)
self.email = email
Expand Down

0 comments on commit 34db36f

Please sign in to comment.