diff --git a/CHANGELOG.md b/CHANGELOG.md index 721e18e..9590dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,12 @@ ### Added 1. Add message bubbles. +2. Show stickers in the chat. ### Changed 1. Use SQLAlchemy offset and limit to fetch only required messages. +2. Break timestamp sort tie with message id. ### Build diff --git a/README.md b/README.md index 57164d3..c3aa087 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,30 @@ A simple app to revisit your text messages. ## Installation +Requires Python >=3.12 + ```shell pip install git+https://gitlab.com/sharmasiddhant/memory.text.git ``` +## Features + +- Import WhatsApp chat exports. +- Native Lightweight Qt UI. +- Chats sidebars +- Message bubbles +- Stickers + +## Roadmap + +- [ ] Option to modify/delete currently imported chats, for instance, modify username or timezone. +- [ ] Sort and filter by timestamps +- [ ] Filter by participants +- [ ] Search for text in messages +- [ ] Add tags to messages + + --- diff --git a/src/memorytext/app.py b/src/memorytext/app.py index d6bffa8..3a9dc65 100644 --- a/src/memorytext/app.py +++ b/src/memorytext/app.py @@ -15,7 +15,7 @@ def main(): app = QApplication(sys.argv) window = MainWindow() window.show() # IMPORTANT!!!!! Windows are hidden by default. - window.setMinimumSize(500, 500) + window.setMinimumSize(800, 500) window.setGeometry(0, 0, 800, 600) # Start the event loop. app.exec() diff --git a/src/memorytext/delegates/message_list.py b/src/memorytext/delegates/message_list.py index 3fce9b9..12fa357 100644 --- a/src/memorytext/delegates/message_list.py +++ b/src/memorytext/delegates/message_list.py @@ -1,8 +1,8 @@ from PySide6.QtWidgets import QStyledItemDelegate from PySide6.QtCore import Qt, QRect, QSize -from PySide6.QtGui import QFont, QFontMetrics, QColor +from PySide6.QtGui import QFont, QFontMetrics, QColor, QPixmap from memorytext.models.message_list import MessageList - +from memorytext.db import BASE_DIR USER_COLOR = "#ABE7FF" OTHER_COLOR = "#e0e0e0" @@ -19,6 +19,7 @@ def __init__(self, parent=None): self.padding = 10 self.sender_height = 20 self.spacing = 5 + self.sticker_size = QSize(128, 128) def paint(self, painter, option, index): painter.save() @@ -30,8 +31,15 @@ def paint(self, painter, option, index): ts_text = timestamp.strftime(TIMESTAMP_FORMAT) is_same_sender = index.data(MessageList.IsSameSenderRole) alignment = index.data(Qt.ItemDataRole.TextAlignmentRole) + attachment = index.data(MessageList.AttachmentRole) + path = None + + if attachment and attachment.startswith("STK"): + path = str(BASE_DIR / "media" / attachment) + extra_text = attachment + " (file attached)" + text = text.replace(extra_text, "") - is_user = alignment & Qt.AlignmentFlag.AlignLeft + is_user = alignment & Qt.AlignmentFlag.AlignRight content_rect = option.rect.adjusted( self.padding, self.padding, -self.padding, -self.padding @@ -57,22 +65,25 @@ def paint(self, painter, option, index): ) # Bubble Dimensions - bubble_height = text_rect.height() + self.padding * 2 + bubble_height = text_rect.height() + self.padding * 2 if text else self.spacing bubble_width = text_rect.width() if not is_same_sender: bubble_height += self.sender_height bubble_width = max(bubble_width, sender_metrics.horizontalAdvance(sender)) + if path: + bubble_height += self.sticker_size.height() + 30 + bubble_width = max(bubble_width, self.sticker_size.width()) bubble_height += ts_metrics.height() bubble_width = max(bubble_width, ts_metrics.horizontalAdvance(ts_text)) bubble_width += 2 * self.padding # Bubble Position - if alignment & Qt.AlignmentFlag.AlignLeft: - bubble_left = content_rect.left() - self.padding - bubble_color = OTHER_COLOR - else: - bubble_left = content_rect.right() - bubble_width - self.padding - bubble_color = USER_COLOR + bubble_left = ( + content_rect.right() - bubble_width - self.padding + if is_user + else content_rect.left() - self.padding + ) + bubble_color = USER_COLOR if is_user else OTHER_COLOR bubble_rect = QRect( bubble_left, option.rect.top() + self.spacing, bubble_width, bubble_height ) @@ -81,7 +92,7 @@ def paint(self, painter, option, index): # Paint Bubble painter.setBrush(QColor(bubble_color)) - pen_color = TEXT_COLOR if is_user else USER_TEXT_COLOR + pen_color = USER_TEXT_COLOR if is_user else TEXT_COLOR painter.setPen(QColor(pen_color)) painter.drawRoundedRect(bubble_rect, 10, 10) @@ -103,6 +114,27 @@ def paint(self, painter, option, index): ) current_top += self.sender_height + # Sticker + pixmap = None + if path: + pixmap = QPixmap(path) + if pixmap and not pixmap.isNull(): + sticker_rect = QRect( + current_left, + current_top, + min(self.sticker_size.width(), inner_width), + self.sticker_size.height(), + ) + painter.drawPixmap( + sticker_rect, + pixmap.scaled( + self.sticker_size, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ), + ) + current_top += self.sticker_size.height() + # Message Text painter.setFont(option.font) painter.drawText( @@ -117,7 +149,7 @@ def paint(self, painter, option, index): # Timestamp painter.setFont(ts_font) - ts_color = TIMESTAMP_COLOR if is_user else USER_TIMESTAMP_COLOR + ts_color = USER_TIMESTAMP_COLOR if is_user else TIMESTAMP_COLOR painter.setPen(QColor(ts_color)) painter.drawText( current_left, @@ -132,6 +164,7 @@ def paint(self, painter, option, index): def sizeHint(self, option, index): text = str(index.data(Qt.ItemDataRole.DisplayRole)) is_same_sender = index.data(MessageList.IsSameSenderRole) + attachment = index.data(MessageList.AttachmentRole) max_width = option.rect.width() * 0.8 inner_width = max_width - 2 * self.padding @@ -148,5 +181,7 @@ def sizeHint(self, option, index): height = text_rect.height() + self.padding * 2 + ts_metrics.height() if not is_same_sender: height += self.sender_height + if attachment and attachment.startswith("STK"): + height += self.sticker_size.height() return QSize(option.rect.width(), height + self.spacing) diff --git a/src/memorytext/io/parsers.py b/src/memorytext/io/parsers.py index bac8147..4f998bd 100644 --- a/src/memorytext/io/parsers.py +++ b/src/memorytext/io/parsers.py @@ -69,7 +69,7 @@ def parse_chat(file: str | Path, pattern=PATTERN) -> list[dict]: Each item contains one message. """ - messages = [] + messages: list[dict] = [] keys = KEYS values = None lno = None diff --git a/src/memorytext/models/message_list.py b/src/memorytext/models/message_list.py index a9e952d..5c82a1f 100644 --- a/src/memorytext/models/message_list.py +++ b/src/memorytext/models/message_list.py @@ -14,6 +14,7 @@ class MessageList(QAbstractListModel): IsSameSenderRole = Qt.ItemDataRole.UserRole + 1 SenderRole = Qt.ItemDataRole.UserRole + 2 TimestampRole = Qt.ItemDataRole.UserRole + 3 + AttachmentRole = Qt.ItemDataRole.UserRole + 5 def __init__( self, @@ -59,6 +60,10 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): # pyright: ignore[repo if role == self.TimestampRole: return m.timestamp + # Attachment + if role == self.AttachmentRole: + return m.attachment + return None def flags(self, index): diff --git a/src/memorytext/services/message_service.py b/src/memorytext/services/message_service.py index 4fe0ed0..598bbae 100644 --- a/src/memorytext/services/message_service.py +++ b/src/memorytext/services/message_service.py @@ -19,7 +19,7 @@ def get_messages( stmt_2 = ( select(Message) .where(Message.conversation_id == conversation_id) - .order_by(Message.timestamp.asc()) + .order_by(Message.timestamp.asc(), Message.id.asc()) .limit(limit) .offset(offset) )