diff --git a/.gitignore b/.gitignore index b3af7a9..56c57f6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ Thumbs.db *.bak *.tmp +# Roadmap / Ideas +**/Roadmap.md + # Python cache *.pyc __pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b14de23..9c34701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [Unreleased] + +## Added + +1. Add day separator before the first message of a day. + + +--- + ## [v0.7.0] ### Added diff --git a/README.md b/README.md index c3aa087..113ccb5 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,34 @@ # memory.text [![Python](https://img.shields.io/badge/python-3.12%2B-4B8BBE?style=for-the-badge&logo=python&logoColor=%23FFE873)](https://www.python.org/) +[![Qt](https://img.shields.io/badge/Qt-%2CDE85.svg?style=for-the-badge&logo=Qt&logoColor=white)](https://wiki.qt.io/Qt_for_Python) [![LICENSE: MIT](https://img.shields.io/badge/LICENSE-MIT-green?style=for-the-badge)](LICENSE) -A simple app to revisit your text messages. +A desktop application that reimagines your chat history into a book-like reading experience, with chapters, navigation, and structure instead of endless scrolling. --- +## Vision + +Chat apps are great for communication but terrible for revisiting conversations, with no navigation and require an awful lot of scrolling. +This app explores a different approach, treating chats as something you can navigate and read, not just scroll through. + +Think: +- Jumping to specific days or moments +- Navigating like chapters in a book +- Bookmarks +- Rediscovering conversations as memories + +## Preview + +> Current UI — early development (focused on chat rendering and core structure) + +

+ +

+ + + ## Installation Requires Python >=3.12 @@ -20,18 +42,30 @@ pip install git+https://gitlab.com/sharmasiddhant/memory.text.git ## Features - Import WhatsApp chat exports. -- Native Lightweight Qt UI. +- 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 +### Navigation & Structure + +- [ ] Day markers +- [ ] Jump to a specific date +- [ ] Timeline-style navigation + +### Filter and Search + +- [ ] Search messages - [ ] Filter by participants -- [ ] Search for text in messages -- [ ] Add tags to messages +- [ ] Sort/filter by timestamps + +### Organization + +- [ ] Edit imported chat metadata (title, username, timezone) +- [ ] Tags for messages +- [ ] Bookmark important moments --- @@ -42,7 +76,7 @@ pip install git+https://gitlab.com/sharmasiddhant/memory.text.git If you'd like to explore, improve, fix something, report bugs, or suggest any feature ideas, you are welcome to contribute. -To get started, you can have a look at the [issue tracker](https://github.com/shsiddhant/memory.texts/issues). If you want to report a bug or make a feature request, please open a [new issue](https://github.com/shsiddhant/memory.texts/issues/new/choose) using an appropriate template. +To get started, you can have a look at the [issue tracker](https://github.com/shsiddhant/memory.text/issues). If you want to report a bug or make a feature request, please open a [new issue](https://github.com/shsiddhant/memory.text/issues/new/choose) using an appropriate template. See [CONTRIBUTING](CONTRIBUTING.md) for a detailed overview of the contributing guidelines. diff --git a/assets/screenshots/chat_view.png b/assets/screenshots/chat_view.png new file mode 100644 index 0000000..63c3f5e Binary files /dev/null and b/assets/screenshots/chat_view.png differ diff --git a/src/memorytext/delegates/message_list.py b/src/memorytext/delegates/message_list.py index 217a21c..8cccedd 100644 --- a/src/memorytext/delegates/message_list.py +++ b/src/memorytext/delegates/message_list.py @@ -1,6 +1,6 @@ from PySide6.QtWidgets import QStyledItemDelegate from PySide6.QtCore import Qt, QRect, QSize -from PySide6.QtGui import QFont, QFontMetrics, QColor, QPixmap +from PySide6.QtGui import QFont, QFontMetrics, QColor, QPixmap, QPainter from memorytext.models.roles import Roles from memorytext.config import USER_DIR @@ -16,13 +16,15 @@ class MessageDelegate(QStyledItemDelegate): def __init__(self, parent=None): super().__init__(parent) + self.day_text_height = 20 + self.day_text_spacing = 20 self.padding = 15 self.radius = 10 self.sender_height = 20 self.spacing = 5 self.sticker_size = QSize(128, 128) - def paint(self, painter, option, index): + def paint(self, painter: QPainter, option, index): painter.save() # Data @@ -33,6 +35,8 @@ def paint(self, painter, option, index): is_same_sender = index.data(Roles.IsSameSenderRole) alignment = index.data(Qt.ItemDataRole.TextAlignmentRole) attachment = index.data(Roles.AttachmentRole) + is_day_separator = index.data(Roles.IsDaySeparatorRole) + day_text = index.data(Roles.DayTextRole) path = None if attachment and attachment.startswith("STK"): @@ -61,6 +65,15 @@ def paint(self, painter, option, index): sender_metrics = QFontMetrics(sender_font) ts_metrics = QFontMetrics(ts_font) + current_top = content_rect.top() + + if is_day_separator: + day_text_font = base_font + day_text_font.setBold(True) + painter.setFont(day_text_font) + painter.drawText(content_rect, day_text, Qt.AlignmentFlag.AlignHCenter) + current_top += self.day_text_height + 2 * self.day_text_spacing + text_rect = metrics.boundingRect( 0, 0, inner_width, 10000, Qt.TextFlag.TextWordWrap, text ) @@ -91,7 +104,7 @@ def paint(self, painter, option, index): ) bubble_color = USER_COLOR if is_user else OTHER_COLOR bubble_rect = QRect( - bubble_left, option.rect.top() + self.spacing, bubble_width, bubble_height + bubble_left, current_top + self.spacing, bubble_width, bubble_height ) painter.setRenderHint(painter.RenderHint.Antialiasing) @@ -171,6 +184,7 @@ def sizeHint(self, option, index): text = str(index.data(Qt.ItemDataRole.DisplayRole)) is_same_sender = index.data(Roles.IsSameSenderRole) attachment = index.data(Roles.AttachmentRole) + is_date_separator = index.data(Roles.IsDaySeparatorRole) max_width = option.rect.width() * 0.5 inner_width = max_width - 2 * self.padding @@ -185,6 +199,8 @@ def sizeHint(self, option, index): 0, 0, inner_width, 10000, Qt.TextFlag.TextWordWrap, text ) height = text_rect.height() + self.padding * 2 + ts_metrics.height() + if is_date_separator: + height += self.day_text_height + 2 * self.day_text_spacing if not is_same_sender: height += self.sender_height if attachment and attachment.startswith("STK"): diff --git a/src/memorytext/models/message_list.py b/src/memorytext/models/message_list.py index 3d41ab7..9f62621 100644 --- a/src/memorytext/models/message_list.py +++ b/src/memorytext/models/message_list.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING from PySide6.QtCore import QAbstractListModel, QPersistentModelIndex, Qt, QModelIndex +from memorytext.util import get_day_string_from_timestamp from memorytext.services import chat_service from memorytext.models.roles import Roles @@ -59,6 +60,16 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole): # pyright: ignore[repo if role == Roles.AttachmentRole: return m.attachment + # Day Separator + if role == Roles.IsDaySeparatorRole: + return ( + row == 0 or m.timestamp.date() != self._items[row - 1].timestamp.date() + ) + + # Day Text + if role == Roles.DayTextRole: + return get_day_string_from_timestamp(m.timestamp) + return None def flags(self, index): diff --git a/src/memorytext/models/roles.py b/src/memorytext/models/roles.py index 91b2a7f..598f6d9 100644 --- a/src/memorytext/models/roles.py +++ b/src/memorytext/models/roles.py @@ -8,6 +8,8 @@ class Roles(IntEnum): SenderRole = Qt.ItemDataRole.UserRole + 2 TimestampRole = Qt.ItemDataRole.UserRole + 3 AttachmentRole = Qt.ItemDataRole.UserRole + 5 + IsDaySeparatorRole = Qt.ItemDataRole.UserRole + 6 + DayTextRole = Qt.ItemDataRole.UserRole + 7 # Chat List Model ParticipantsRole = Qt.ItemDataRole.UserRole + 4 diff --git a/src/memorytext/util.py b/src/memorytext/util.py new file mode 100644 index 0000000..cbca4d5 --- /dev/null +++ b/src/memorytext/util.py @@ -0,0 +1,10 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from datetime import datetime + + +def get_day_string_from_timestamp(timestamp: datetime): + fmt = "%A, %b %d, %Y" + return timestamp.strftime(fmt)