diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b812b9..e07e9a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,30 @@ ### Added -1. Add lazy loading of texts and make them scrollable. + ### Changed +### Fixed + + +--- + +## [v0.4.0] + +### Added + +1. Add lazy loading of texts and make them scrollable. +2. Add participants property and a modifiable username attribute to Conversation. +3. Add option to choose username from participants during chat import. + +### Changed + +1. Set minimum size and initial geometry for main window. +2. Align messages according to username. + ### Fixed 1. Fix missing UI cues during exceptions. @@ -44,7 +62,7 @@ - Main area containing messages with metadata and correct alignment. 2. Add QAbstractListModel models for chat list and message list. 3. Add message service module to read messages from DB. -4. Add chat list and chat messages views with forementioned models. +4. Add chat list and chat messages views with aforementioned models. 5. Add message list delegate to display messages with correct alignment. ### Changed diff --git a/src/memorytext/app.py b/src/memorytext/app.py index 06b5b3d..d6bffa8 100644 --- a/src/memorytext/app.py +++ b/src/memorytext/app.py @@ -15,7 +15,8 @@ def main(): app = QApplication(sys.argv) window = MainWindow() window.show() # IMPORTANT!!!!! Windows are hidden by default. - + window.setMinimumSize(500, 500) + window.setGeometry(0, 0, 800, 600) # Start the event loop. app.exec() diff --git a/src/memorytext/io/import_wa.py b/src/memorytext/io/import_wa.py index 0ed6c40..09057de 100644 --- a/src/memorytext/io/import_wa.py +++ b/src/memorytext/io/import_wa.py @@ -9,8 +9,10 @@ from pathlib import Path -def import_whatsapp(path: str | Path, title: str, tz: str = "Etc/UTC"): - conversation = Conversation.from_whatsapp(path, title, tz) +def import_whatsapp( + path: str | Path, title: str, tz: str = "Etc/UTC", username: str | None = None +): + conversation = Conversation.from_whatsapp(path, title, tz, username) engine = get_db() with Session(engine) as session: try: diff --git a/src/memorytext/models/chat_list.py b/src/memorytext/models/chat_list.py index 4754d13..20340b1 100644 --- a/src/memorytext/models/chat_list.py +++ b/src/memorytext/models/chat_list.py @@ -3,6 +3,8 @@ class ChatList(QAbstractListModel): + ParticipantsRole = Qt.ItemDataRole.UserRole + 4 + def __init__(self, chat_list=[], parent=None): super().__init__(parent) self._items = chat_list @@ -13,11 +15,13 @@ def rowCount(self, parent=QModelIndex()): # pyright: ignore[reportIncompatibleM def data(self, index, role=Qt.ItemDataRole.DisplayRole): # pyright: ignore[reportIncompatibleMethodOverride] if not index.isValid(): return None - conversation_id, title = self._items[index.row()] + conversation_id, title, participants = self._items[index.row()] if role == Qt.ItemDataRole.DisplayRole: return title if role == Qt.ItemDataRole.UserRole: return conversation_id + if role == self.ParticipantsRole: + return participants return None def get_id(self, index): diff --git a/src/memorytext/models/core.py b/src/memorytext/models/core.py index 571f93d..f726277 100644 --- a/src/memorytext/models/core.py +++ b/src/memorytext/models/core.py @@ -44,9 +44,21 @@ class Conversation(Base): messages: Mapped[List["Message"]] = relationship( back_populates="conversation", cascade="all, delete-orphan" ) + username: Mapped[Optional[str]] = mapped_column(default=None) + + @property + def participants(self): + participant_names = {m.sender for m in self.messages} + return participant_names @classmethod - def from_whatsapp(cls, path: str | Path, title: str, tz: str = "Etc/UTC"): + def from_whatsapp( + cls, + path: str | Path, + title: str, + tz: str = "Etc/UTC", + username: str | None = None, + ): dt_fmt = WHATSAPP_DT_FMT messages = parsers.parse_chat(path) messages = normalize.normalize_timestamps(messages, dt_fmt, tz) @@ -59,7 +71,7 @@ def from_whatsapp(cls, path: str | Path, title: str, tz: str = "Etc/UTC"): ) for m in messages ] - return cls(title=title, tz=tz, messages=messages2) + return cls(title=title, tz=tz, messages=messages2, username=username) class Tag(Base): diff --git a/src/memorytext/models/message_list.py b/src/memorytext/models/message_list.py index 88c0225..a9e952d 100644 --- a/src/memorytext/models/message_list.py +++ b/src/memorytext/models/message_list.py @@ -7,6 +7,8 @@ from collections.abc import Callable from memorytext.models.core import Message +from memorytext.services import chat_service + class MessageList(QAbstractListModel): IsSameSenderRole = Qt.ItemDataRole.UserRole + 1 @@ -24,6 +26,7 @@ def __init__( self._conversation_id = conversation_id self._message_service = message_service self._items, self._total = self._message_service(conversation_id, limit, 0) + self._username = chat_service.get_username(conversation_id) def rowCount(self, parent=QModelIndex()): # pyright: ignore[reportIncompatibleMethodOverride] return len(self._items) @@ -43,10 +46,10 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): # pyright: ignore[repo # Alignment if role == Qt.ItemDataRole.TextAlignmentRole: - if m.sender == "Meera": - return Qt.AlignmentFlag.AlignLeft - else: + if m.sender == chat_service.get_username(self._conversation_id): return Qt.AlignmentFlag.AlignRight + else: + return Qt.AlignmentFlag.AlignLeft # Sender Name if role == self.SenderRole: diff --git a/src/memorytext/services/chat_service.py b/src/memorytext/services/chat_service.py index 46695f6..9b3916a 100644 --- a/src/memorytext/services/chat_service.py +++ b/src/memorytext/services/chat_service.py @@ -1,6 +1,6 @@ from __future__ import annotations -from sqlalchemy.orm import Session -from sqlalchemy import select +from sqlalchemy.orm import Session, selectinload +from sqlalchemy import select, update from memorytext.models.core import Conversation from memorytext.db import get_db @@ -9,7 +9,46 @@ def get_chats(count: int = 10): engine = get_db() with Session(engine) as session: - stmt = select(Conversation) + stmt = select(Conversation).options(selectinload(Conversation.messages)) conversations = session.scalars(stmt).fetchmany(count) - chat_list = [(c.id, c.title) for c in conversations] + chat_list = [(c.id, c.title, c.participants) for c in conversations] return chat_list + + +def get_participants(title: str): + engine = get_db() + with Session(engine) as session: + stmt = ( + select(Conversation) + .where(Conversation.title == title) + .options(selectinload(Conversation.messages)) + ) + conversation = session.scalar(stmt) + if conversation: + participants = conversation.participants + return participants + return set() + + +def set_username(title: str, username: str): + engine = get_db() + with Session(engine) as session: + stmt = ( + update(Conversation) + .where(Conversation.title == title) + .values(username=username) + ) + try: + session.execute(stmt) + session.commit() + except Exception: + session.rollback() + raise + + +def get_username(conversation_id: int | None): + engine = get_db() + with Session(engine) as session: + stmt = select(Conversation.username).where(Conversation.id == conversation_id) + username = session.scalar(stmt) + return username diff --git a/src/memorytext/windows/import_window.py b/src/memorytext/windows/import_window.py index 747a9bf..18622d5 100644 --- a/src/memorytext/windows/import_window.py +++ b/src/memorytext/windows/import_window.py @@ -1,15 +1,25 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + from PySide6.QtWidgets import ( + QComboBox, QFileDialog, - QMainWindow, QFormLayout, + QMainWindow, + QHBoxLayout, QVBoxLayout, QLineEdit, QPushButton, QWidget, QLabel, ) +from PySide6.QtCore import Slot from sqlalchemy.exc import IntegrityError from memorytext.io.import_wa import import_whatsapp +from memorytext.services import chat_service + +if TYPE_CHECKING: + from collections.abc import Callable class ImportWindow(QMainWindow): @@ -36,6 +46,7 @@ def __init__(self, parent=None): layout.addRow(self.file_pick) layout.addRow("Title:", self.title_box) layout.addRow(self.import_button, self.cancel_button) + container = QWidget() container.setLayout(layout) @@ -60,6 +71,10 @@ def import_chat(self): if self.file_path and title: try: import_whatsapp(self.file_path, title) + self.select_user = SelectUserWindow( + service=chat_service.get_participants, title=title + ) + self.select_user.show() self.parent().sidebar.update_data() # pyright: ignore [reportOptionalMemberAccess, reportAttributeAccessIssue] except IntegrityError: self.error_window = ErrorWindow( @@ -74,6 +89,63 @@ def import_chat(self): self.close() +class SelectUserWindow(QMainWindow): + def __init__(self, service: Callable[[str], set[str]], title: str): + super().__init__() + self.service = service + self.chat_title = title + self.initUI() + + def initUI(self): + self.participants = self.service(self.chat_title) + self.setWindowTitle("Select your username") + self.setGeometry(100, 100, 300, 200) + + button_layout = QHBoxLayout() + layout = QVBoxLayout() + + self.comboBox = QComboBox() + self.comboBox.addItems(self.participants) # pyright: ignore[reportArgumentType] + layout.addWidget(self.comboBox) + + self.default_select = str(next((name for name in self.participants), None)) + self.comboBox.setCurrentText(self.default_select) + + self.label = QLabel(f"Selected username: {self.default_select}") + layout.addWidget(self.label) + + self.ok_button = QPushButton("Ok") + button_layout.addWidget(self.ok_button) + + self.cancel_button = QPushButton("Cancel") + button_layout.addWidget(self.cancel_button) + + layout.addLayout(button_layout) + + self.cancel_button.clicked.connect(self.close) + self.ok_button.clicked.connect(self.set_username) + self.comboBox.currentTextChanged.connect(self.on_selection_changed) + + container = QWidget() + container.setLayout(layout) + + self.setCentralWidget(container) + + @Slot(str) + def on_selection_changed(self, text): + self.label.setText(f"Selected username: {text}") + + @Slot() + def set_username(self): + try: + username = self.comboBox.currentText() + chat_service.set_username(self.chat_title, username) + except Exception as e: + self.error_window = ErrorWindow(error=f"Error: {e}") + self.error_window.show() + self.close() + + class ErrorWindow(QMainWindow): def __init__(self, error: str): super().__init__() diff --git a/src/memorytext/windows/main_window.py b/src/memorytext/windows/main_window.py index dfcac2f..e71a49f 100644 --- a/src/memorytext/windows/main_window.py +++ b/src/memorytext/windows/main_window.py @@ -2,6 +2,7 @@ QMainWindow, QHBoxLayout, QVBoxLayout, + # QLayout, QWidget, QPushButton, ) @@ -17,6 +18,7 @@ def __init__(self): super().__init__() main_layout = QHBoxLayout() + central_widget = QWidget() central_widget.setLayout(main_layout) self.setCentralWidget(central_widget)