Skip to content

Commit

Permalink
✨ filter messages by tags
Browse files Browse the repository at this point in the history
  • Loading branch information
haliphax committed Mar 27, 2024
1 parent 5440c0d commit 5a51480
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 18 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ character sets (UTF-8) and [terminal capabilities][] are taken advantage of.
- [x] Post messages
- [x] Reply to messages
- [x] Tag system
- [x] Filter by tag(s)
- [ ] Search messages
- [ ] Filter by tag(s)
- [ ] Private messages
- [ ] Door games
- [x] Subprocess redirect for terminal apps
Expand Down
49 changes: 43 additions & 6 deletions userland/scripts/messages/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

# 3rd party
from textual import events
from textual.app import ComposeResult
from textual.binding import Binding
from textual.css.query import NoMatches
from textual.widgets import Footer, Label, ListItem, ListView
Expand All @@ -16,8 +17,9 @@
from xthulu.ssh.context import SSHContext

# local
from userland.models import Message
from userland.models import Message, MessageTags
from .editor_screen import EditorScreen
from .filter_modal import FilterModal
from .view_screen import ViewScreen

db = Resources().db
Expand Down Expand Up @@ -49,6 +51,7 @@ class MessagesApp(BannerApp):
"""Message bases Textual app"""

BINDINGS = [
Binding("f", "filter", "Filter"),
Binding("n", "compose", "Compose"),
Binding("r", "reply", "Reply"),
]
Expand Down Expand Up @@ -95,6 +98,9 @@ class MessagesApp(BannerApp):
_last_query_empty = False
"""If last query had no results"""

_tags = []
"""List of tags to filter by"""

def __init__(
self,
context: SSHContext,
Expand Down Expand Up @@ -122,19 +128,32 @@ async def _load_messages(self, newer=False) -> None:

lv: ListView = self.query_one(ListView)
select = Message.select("id", "title")
filter = (
select
if len(self._tags) == 0
else select.select_from(
Message.join(
MessageTags,
db.and_(
MessageTags.message_id == Message.id,
MessageTags.tag_name.in_(self._tags),
),
)
)
)
first = len(lv.children) == 0
limit = min(round(lv.size.height / 2), LOAD_AT_ONCE)

if first:
# app startup; load most recent messages
limit = min(lv.size.height, LIMIT)
query = select.order_by(Message.id.desc())
query = filter.order_by(Message.id.desc())
elif newer:
# load newer messages
query = select.where(Message.id > self._last).order_by(Message.id)
query = filter.where(Message.id > self._last).order_by(Message.id)
else:
# load older messages
query = select.where(Message.id < self._first).order_by(
query = filter.where(Message.id < self._first).order_by(
Message.id.desc()
)

Expand Down Expand Up @@ -210,7 +229,15 @@ async def _load_messages(self, newer=False) -> None:
# keep selected item in view
lv.scroll_to_widget(lv.children[lv.index])

def compose(self):
async def _update_tags(self, tags: list[str]) -> None:
lv = self.query_one(ListView)
await lv.clear()
self._tags = [t for t in tags if t != ""]
await self._load_messages()
lv.index = 0
lv.focus()

def compose(self) -> ComposeResult:
# load widgets from BannerApp
for widget in super().compose():
yield widget
Expand All @@ -232,6 +259,16 @@ async def action_compose(self) -> None:

await self.push_screen(EditorScreen())

async def action_filter(self) -> None:
try:
self.query_one(ListView)
except NoMatches:
# not in message list screen; pop screen first
self.pop_screen()
return await self.action_filter()

await self.push_screen(FilterModal(tags=self._tags), self._update_tags)

async def action_reply(self) -> None:
try:
lv: ListView = self.query_one(ListView)
Expand All @@ -252,7 +289,7 @@ async def action_reply(self) -> None:
await self.push_screen(
EditorScreen(
content=(
f"\n\n---\n{message.author.name} wrote:"
f"\n\n---\n\n{message.author.name} wrote:"
f"\n\n{message.content}"
),
reply_to=message,
Expand Down
8 changes: 3 additions & 5 deletions userland/scripts/messages/details_modal.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Message details screen"""

# 3rd party
from textual.binding import Binding
from textual.containers import Horizontal, Vertical
from textual.screen import ModalScreen
from textual import validation
Expand All @@ -17,10 +16,6 @@
class DetailsModal(ModalScreen):
"""Message details screen"""

BINDINGS = [
Binding("escape", "app.pop_screen", show=False),
]

CSS = """
DetailsModal {
align: center middle;
Expand Down Expand Up @@ -154,3 +149,6 @@ async def on_button_pressed(self, event: Button.Pressed) -> None:

self.app.pop_screen() # pop this modal
self.app.pop_screen() # pop the editor

async def key_escape(self, _):
self.app.pop_screen() # pop this modal
8 changes: 6 additions & 2 deletions userland/scripts/messages/editor_screen.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Message compose/reply screen"""

# 3rd party
from textual import events
from textual.screen import ModalScreen
from textual.widgets import TextArea

Expand All @@ -23,7 +24,10 @@ def __init__(
super().__init__(*args, **kwargs)

def compose(self):
yield TextArea(self._content)
yield TextArea(text=self._content, show_line_numbers=True)

async def key_escape(self, key: events.Key) -> None:
if isinstance(self.app.screen_stack[-1], SaveModal):
return

async def key_escape(self):
await self.app.push_screen(SaveModal(reply_to=self.reply_to))
73 changes: 73 additions & 0 deletions userland/scripts/messages/filter_modal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Filter messages screen"""

# 3rd party
from textual.containers import Horizontal, Vertical
from textual.screen import ModalScreen
from textual.widgets import Button, Input, Label


class FilterModal(ModalScreen[list[str]]):
"""Filter messages screen"""

CSS = """
FilterModal {
align: center middle;
background: rgba(0, 0, 0, 0.5);
}
Button {
margin: 1;
width: 50%;
}
Label {
margin-top: 1;
}
Input {
width: 54;
}
#filter {
margin-left: 0;
margin-top: 1;
}
#wrapper {
background: $primary-background;
height: 9;
padding: 1;
width: 60;
}
"""

_tags: list[str]

def __init__(self, *args, tags: list[str] | None = None, **kwargs):
super().__init__(*args, **kwargs)
self._tags = tags or []

def compose(self):
yield Vertical(
Horizontal(
Label("Tags"),
Input(" ".join(self._tags)),
),
Horizontal(
Button("Filter", variant="success", id="filter", name="filter"),
Button("Cancel", variant="error", id="cancel", name="cancel"),
),
id="wrapper",
)

async def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.name == "cancel":
self.app.pop_screen() # pop this modal
return

tags = self.query_one(Input)
assert tags
self.dismiss(tags.value.split(" "))

async def key_escape(self, _):
self.app.pop_screen() # pop this modal
3 changes: 3 additions & 0 deletions userland/scripts/messages/save_modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,6 @@ async def on_button_pressed(self, event: Button.Pressed) -> None:

self.app.pop_screen() # pop this modal
self.app.pop_screen() # pop the editor

async def key_escape(self, _):
self.app.pop_screen() # pop this modal
5 changes: 1 addition & 4 deletions userland/scripts/messages/view_screen.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Message viewer screen"""

# 3rd party
from textual.binding import Binding
from textual.screen import Screen
from textual.widgets import Footer, MarkdownViewer

Expand All @@ -12,9 +11,7 @@
class ViewScreen(Screen):
"""Message viewer screen"""

BINDINGS = [
Binding("escape", "app.pop_screen", show=False),
]
BINDINGS = [("escape", "app.pop_screen", "Exit")]

message: Message

Expand Down

0 comments on commit 5a51480

Please sign in to comment.