Skip to content

Commit

Permalink
Add notifications modal dialog for viewing notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
gizmo385 committed Nov 30, 2024
1 parent bf86fcd commit 0ac452a
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 4 deletions.
13 changes: 13 additions & 0 deletions lazy_github/lib/bindings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from textual.binding import Binding

from lazy_github.lib.utils import classproperty
from lazy_github.lib.context import LazyGithubContext


class LazyGithubBindings:
Expand Down Expand Up @@ -38,6 +39,18 @@ class LazyGithubBindings:
TABLE_PAGE_LEFT = Binding("^", "page_left", "Table page left", show=False, id="common.table.page_left")
TABLE_PAGE_RIGHT = Binding("$", "page_right", "Table page right", show=False, id="common.table.page_right")

# Notifications
OPEN_NOTIFICATIONS_MODAL = Binding(
"ctrl+n",
"view_notifications",
"View Notifications",
id="notifications.open",
show=LazyGithubContext.config.notifications.enabled,
)
MARK_NOTIFICATION_READ = Binding("R", "mark_read", "Mark as Read", id="notifications.mark_read")
VIEW_READ_NOTIFICATIONS = Binding("r", "view_read", "View Read Notifications", id="notifications.view_read")
VIEW_UNREAD_NOTIFICATIONS = Binding("u", "view_unread", "View Unread Notifications", id="notifications.view_unread")

# Dialog bindings
SUBMIT_DIALOG = Binding("shift+enter", "submit", "Submit", id="modal.submit")
CLOSE_DIALOG = Binding("q, ESC", "close", "Close", id="modal.close")
Expand Down
1 change: 0 additions & 1 deletion lazy_github/lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ class NotificationSettings(BaseModel):
"""Controls the settings for the optional notification feature, which relies on the standard GitHub CLI."""

enabled: bool = False
show_all_notifications: bool = True


class BindingsSettings(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions lazy_github/lib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
IS_NOT_FAVORITED = "☆"
CHECKMARK = "✔"
X_MARK = "✘"
BULLET_POINT = "•"

NOTIFICATION_REFRESH_INTERVAL = 60

Expand Down
13 changes: 10 additions & 3 deletions lazy_github/lib/github_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import json
from asyncio.subprocess import PIPE, Process

from lazy_github.models.github import Notification

NOTIFICATIONS_PAGE_COUNT = 30


Expand All @@ -20,18 +22,23 @@ async def is_logged_in() -> bool:
return False


async def fetch_notifications(all: bool) -> list[str]:
async def fetch_notifications(all: bool) -> list[Notification]:
"""Fetches notifications on GitHub. If all=True, then previously read notifications will also be returned"""
result = await _run_gh_cli_command(f'api "/notifications?all={str(all).lower()}"')
await result.wait()
notifications: list[str] = []
notifications: list[Notification] = []
if result.stdout:
stdout = await result.stdout.read()
parsed = json.loads(stdout.decode())
notifications = [n["subject"]["title"] for n in parsed]
notifications = [Notification(**n) for n in parsed]
return notifications


async def mark_notification_as_read(notification: Notification) -> None:
result = await _run_gh_cli_command(f"--method PATCH api /notifications/threads/{notification.id}")
await result.wait()


async def unread_notification_count() -> int:
"""Returns the number of currently unread notifications on GitHub"""
return len(await fetch_notifications(all=False))
159 changes: 159 additions & 0 deletions lazy_github/ui/screens/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
from textual import on, work
from textual.app import ComposeResult
from textual.containers import Container
from textual.coordinate import Coordinate
from textual.message import Message
from textual.screen import ModalScreen
from textual.widgets import Markdown, TabPane, TabbedContent


from lazy_github.lib.bindings import LazyGithubBindings
from lazy_github.lib.constants import BULLET_POINT, CHECKMARK
from lazy_github.lib.github_cli import fetch_notifications, mark_notification_as_read
from lazy_github.models.github import Notification
from lazy_github.ui.widgets.common import LazyGithubFooter, SearchableDataTable


class NotificationMarkedAsRead(Message):
def __init__(self, notification: Notification) -> None:
super().__init__()
self.notification = notification


class _NotificationsTableTabPane(TabPane):
def __init__(self, prefix: str, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.notifications: dict[int, Notification] = {}
self.searchable_table: SearchableDataTable = SearchableDataTable(
table_id=f"{prefix}_notifications_table",
search_input_id=f"{prefix}_notifications_table_search_input",
sort_key="updated_at",
)

def compose(self) -> ComposeResult:
yield self.searchable_table

def remove_notification(self, notification: Notification) -> None:
self.searchable_table.table.remove_row(row_key=str(notification.id))

def add_notification(self, notification: Notification) -> None:
self.notifications[notification.id] = notification
self.searchable_table.table.add_row(
notification.updated_at.strftime("%c"),
notification.subject.subject_type,
notification.subject.title,
notification.reason.title(),
notification.id,
key=str(notification.id),
)

def on_mount(self) -> None:
self.searchable_table.loading = True
self.searchable_table.table.cursor_type = "row"
self.searchable_table.table.add_column("Updated At", key="updated_at")
self.searchable_table.table.add_column("Subject", key="subject")
self.searchable_table.table.add_column("Title", key="title")
self.searchable_table.table.add_column("Reason", key="reason")
self.searchable_table.table.add_column("Thread ID", key="id")

self.id_column = self.searchable_table.table.get_column_index("id")


class ReadNotificationTabPane(_NotificationsTableTabPane):
def __init__(self) -> None:
super().__init__(id="read", prefix="read", title=f"[green]{CHECKMARK}Read[/green]")


class UnreadNotificationTabPane(_NotificationsTableTabPane):
BINDINGS = [LazyGithubBindings.MARK_NOTIFICATION_READ]

def __init__(self) -> None:
super().__init__(id="unread", prefix="unread", title=f"[red]{BULLET_POINT}Unread[/red]")

async def action_mark_read(self) -> None:
current_row = self.searchable_table.table.cursor_row
id_coord = Coordinate(current_row, self.id_column)
id = self.searchable_table.table.get_cell_at(id_coord)
notification_to_mark = self.notifications[int(id)]

self.post_message(NotificationMarkedAsRead(notification_to_mark))


class NotificationsContainer(Container):
DEFAULT_CSS = """
NotificationsContainer {
dock: top;
max-height: 80%;
align: center middle;
}
"""

BINDINGS = [LazyGithubBindings.VIEW_READ_NOTIFICATIONS, LazyGithubBindings.VIEW_UNREAD_NOTIFICATIONS]

def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.unread_tab = UnreadNotificationTabPane()
self.read_tab = ReadNotificationTabPane()

def compose(self) -> ComposeResult:
yield Markdown("# Notifications")
with TabbedContent():
yield self.unread_tab
yield self.read_tab

@on(NotificationMarkedAsRead)
async def notification_marked_read(self, message: NotificationMarkedAsRead) -> None:
await mark_notification_as_read(message.notification)
self.unread_tab.remove_notification(message.notification)
self.read_tab.add_notification(message.notification)

def action_view_read(self) -> None:
self.query_one(TabbedContent).active = "read"
self.read_tab.searchable_table.table.focus()

def action_view_unread(self) -> None:
self.query_one(TabbedContent).active = "unread"
self.unread_tab.searchable_table.table.focus()

@work
async def load_notifications(self) -> None:
notifications = await fetch_notifications(True)

for notification in notifications:
if notification.unread:
self.unread_tab.add_notification(notification)
else:
self.read_tab.add_notification(notification)

self.unread_tab.searchable_table.loading = False
self.read_tab.searchable_table.loading = False

def on_mount(self) -> None:
self.read_tab.searchable_table.loading = True
self.unread_tab.searchable_table.loading = True

self.load_notifications()


class NotificationsModal(ModalScreen[None]):
DEFAULT_CSS = """
NotificationsModal {
height: 80%;
}
NotificationsContainer {
width: 100;
max-height: 50;
border: thick $background 80%;
background: $surface-lighten-3;
}
"""

BINDINGS = [LazyGithubBindings.CLOSE_DIALOG]

def compose(self) -> ComposeResult:
yield NotificationsContainer(id="notifications")
yield LazyGithubFooter()

async def action_close(self) -> None:
self.dismiss()
7 changes: 7 additions & 0 deletions lazy_github/ui/screens/primary.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from lazy_github.models.github import Repository
from lazy_github.ui.screens.new_issue import NewIssueModal
from lazy_github.ui.screens.new_pull_request import NewPullRequestModal
from lazy_github.ui.screens.notifications import NotificationsModal
from lazy_github.ui.screens.settings import SettingsModal
from lazy_github.ui.widgets.command_log import CommandLogSection
from lazy_github.ui.widgets.common import LazyGithubContainer, LazyGithubFooter
Expand Down Expand Up @@ -322,6 +323,7 @@ async def search(self, query: str) -> Hits:


class LazyGithubMainScreen(Screen):
BINDINGS = [LazyGithubBindings.OPEN_NOTIFICATIONS_MODAL]
COMMANDS = {MainScreenCommandProvider}
notification_refresh_timer: Timer | None = None

Expand All @@ -331,6 +333,11 @@ def compose(self):
yield MainViewPane()
yield LazyGithubFooter()

@work
async def action_view_notifications(self) -> None:
await self.app.push_screen_wait(NotificationsModal())
self.refresh_notification_count()

async def on_mount(self) -> None:
if LazyGithubContext.config.notifications.enabled:
self.refresh_notification_count()
Expand Down

0 comments on commit 0ac452a

Please sign in to comment.