diff --git a/CHANGELOG.md b/CHANGELOG.md index 57bc3d5..df7a6c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ Changes: * Add button to scroll down when chat is scrolled up(#56) +* Message timestamps(#55) diff --git a/hasherino/__main__.py b/hasherino/__main__.py index 75fe6d8..610b959 100644 --- a/hasherino/__main__.py +++ b/hasherino/__main__.py @@ -31,11 +31,13 @@ class Hasherino: def __init__( self, font_size_pubsub: PubSub, + ts_pubsub: PubSub, memory_storage: AsyncKeyValueStorage, persistent_storage: AsyncKeyValueStorage, page: ft.Page, ) -> None: self.font_size_pubsub = font_size_pubsub + self.ts_pubsub = ts_pubsub self.memory_storage = memory_storage self.persistent_storage = persistent_storage self.page = page @@ -85,7 +87,9 @@ async def login_click(self, _): async def settings_click(self, _): logging.debug("Clicked on settings") - sv = SettingsView(self.font_size_pubsub, self.persistent_storage) + sv = SettingsView( + self.font_size_pubsub, self.ts_pubsub, self.persistent_storage + ) await sv.init() self.page.views.append(sv) await self.page.update_async() @@ -256,7 +260,10 @@ async def run(self): self.status_column = StatusColumn(self.memory_storage, self.persistent_storage) chat_container = ChatContainer( - self.persistent_storage, self.memory_storage, self.font_size_pubsub + self.persistent_storage, + self.memory_storage, + self.font_size_pubsub, + self.ts_pubsub, ) self.new_message_row = NewMessageRow( self.memory_storage, @@ -379,11 +386,12 @@ async def main(page: ft.Page): tg.create_task(persistent_storage.set("color_switcher", False)) tg.create_task(persistent_storage.set("max_messages_per_chat", 100)) tg.create_task(persistent_storage.set("not_first_run", True)) + tg.create_task(persistent_storage.set("show_timestamp", True)) tg.create_task(persistent_storage.set("theme", "System")) tg.create_task(persistent_storage.set("window_width", 500)) tg.create_task(persistent_storage.set("window_height", 800)) - hasherino = Hasherino(PubSub(), memory_storage, persistent_storage, page) + hasherino = Hasherino(PubSub(), PubSub(), memory_storage, persistent_storage, page) await hasherino.run() diff --git a/hasherino/components/chat_container.py b/hasherino/components/chat_container.py index 5d9b951..ed541b9 100644 --- a/hasherino/components/chat_container.py +++ b/hasherino/components/chat_container.py @@ -22,10 +22,12 @@ def __init__( persistent_storage: AsyncKeyValueStorage, memory_storage: AsyncKeyValueStorage, font_size_pubsub: PubSub, + ts_pubsub: PubSub, ): self.persistent_storage = persistent_storage self.memory_storage = memory_storage self.font_size_pubsub = font_size_pubsub + self.ts_pubsub = ts_pubsub self.is_chat_scrolled_down = False self.chat = ft.ListView( expand=True, @@ -98,6 +100,7 @@ async def on_message(self, message: Message): ) await self.add_author_to_user_set(message.user.name) await m.subscribe_to_font_size_change(self.font_size_pubsub) + await m.subscribe_to_show_timestamp_change(self.ts_pubsub) elif message.message_type == "login_message": m = ft.Text( diff --git a/hasherino/components/chat_message.py b/hasherino/components/chat_message.py index a63d162..f143a50 100644 --- a/hasherino/components/chat_message.py +++ b/hasherino/components/chat_message.py @@ -12,6 +12,11 @@ async def on_font_size_changed(self, new_font_size: int): ... +class ShowTimestampSubscriber(ABC): + async def on_show_timestamp_changed(self, show_timestamp: bool): + ... + + class ChatText(ft.Container, FontSizeSubscriber): def __init__(self, text: str, color: str, size: int, weight=""): try: @@ -48,6 +53,17 @@ async def on_font_size_changed(self, new_font_size: int): self.height = new_font_size * 2 +class ChatTimestamp(ft.Text, ShowTimestampSubscriber): + def __init__(self, text: str, color: str, size: int): + super().__init__(text, size=size, color=color, selectable=False) + + async def on_show_timestamp_changed(self, show_timestamp: bool): + if show_timestamp: + self.visible = True + else: + self.visible = False + + class ChatMessage(ft.Row): def __init__(self, message: Message, page: ft.Page, font_size: int): super().__init__() @@ -63,9 +79,18 @@ def __init__(self, message: Message, page: ft.Page, font_size: int): self.add_control_elements(message) def add_control_elements(self, message): - self.controls = [ - ChatBadge(badge.url, self.font_size) for badge in message.user.badges - ] + if message.timestamp is not None: + self.controls.append( + ChatTimestamp( + text=f"{message.timestamp.strftime('%H:%M')} ", + color=ft.colors.GREY, + size=max(self.font_size - 4, 4), + ) + ) + + self.controls.extend( + [ChatBadge(badge.url, self.font_size) for badge in message.user.badges] + ) self.controls.append( ChatText( @@ -97,3 +122,12 @@ async def subscribe_to_font_size_change(self, pubsub: PubSub): if isinstance(control, FontSizeSubscriber) ] ) + + async def subscribe_to_show_timestamp_change(self, pubsub: PubSub): + await pubsub.subscribe_all( + [ + control.on_show_timestamp_changed + for control in self.controls + if isinstance(control, ShowTimestampSubscriber) + ] + ) diff --git a/hasherino/components/settings_view.py b/hasherino/components/settings_view.py index b21d0b8..af53ff4 100644 --- a/hasherino/components/settings_view.py +++ b/hasherino/components/settings_view.py @@ -10,8 +10,11 @@ class SettingsView(ft.View): - def __init__(self, font_size_pubsub: PubSub, storage: AsyncKeyValueStorage): + def __init__( + self, font_size_pubsub: PubSub, ts_pubsub: PubSub, storage: AsyncKeyValueStorage + ): self.font_size_pubsub = font_size_pubsub + self.ts_pubsub = ts_pubsub self.storage = storage async def init(self): @@ -72,6 +75,16 @@ async def _get_general_tab(self) -> ft.Tab: ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN, ), + ft.Row( + controls=[ + ft.Text("Show message timestamp", size=16), + ft.Checkbox( + on_change=self._show_timestamp_click, + value=await self.storage.get("show_timestamp"), + ), + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), ], ), ) @@ -175,6 +188,10 @@ async def _theme_select(self, e): await self.storage.set("theme", e.data) await self.page.update_async() + async def _show_timestamp_click(self, e): + await self.storage.set("show_timestamp", e.control.value) + await self.ts_pubsub.send(e.control.value) + async def _log_path_copy_click(self, _): await self.page.set_clipboard_async(str(LOG_PATH.absolute())) diff --git a/hasherino/factory.py b/hasherino/factory.py index 0505293..9dd5b30 100644 --- a/hasherino/factory.py +++ b/hasherino/factory.py @@ -1,3 +1,5 @@ +from datetime import datetime + from hasherino.hasherino_dataclasses import Emote, HasherinoUser, Message from hasherino.parse_irc import ParsedMessage @@ -22,6 +24,7 @@ def message_factory( elements=elements, message_type="chat_message", me=False, + timestamp=datetime.now(), ) elif isinstance(message, ParsedMessage): emote_map = emote_map.copy() @@ -34,6 +37,7 @@ def message_factory( elements=elements, message_type="chat_message", me=message.is_me(), + timestamp=message.get_timestamp(), ) else: raise TypeError("The message parameter can only be an str or ParsedMessage.") diff --git a/hasherino/hasherino_dataclasses.py b/hasherino/hasherino_dataclasses.py index 289a003..322446e 100644 --- a/hasherino/hasherino_dataclasses.py +++ b/hasherino/hasherino_dataclasses.py @@ -1,6 +1,6 @@ from dataclasses import dataclass +from datetime import datetime from enum import Enum -from json import dumps @dataclass @@ -35,3 +35,4 @@ class Message: elements: list[str | Emote] message_type: str me: bool + timestamp: datetime | None = None diff --git a/hasherino/parse_irc.py b/hasherino/parse_irc.py index 5adf69d..e17246c 100644 --- a/hasherino/parse_irc.py +++ b/hasherino/parse_irc.py @@ -1,5 +1,6 @@ import logging from collections import defaultdict +from datetime import datetime from enum import Enum, auto from hasherino.hasherino_dataclasses import Badge, Emote @@ -98,6 +99,13 @@ def get_command(self) -> Command: return result + def get_timestamp(self) -> datetime | None: + if not self.tags or not self.tags.get("tmi-sent-ts"): + return None + + ts = int(self.tags["tmi-sent-ts"]) + return datetime.fromtimestamp(ts / 1000) + def is_me(self) -> bool: """ Messages sent with /me, coloring the whole line with the user's chat color @@ -346,6 +354,9 @@ def _parse_tags(self, raw_tags: str): case "emote-sets": dict_parsed_tags["emote-sets"] = tag_value + case "tmi-sent-ts": + dict_parsed_tags["tmi-sent-ts"] = tag_value + case _: pass