Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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


---


Expand Down
2 changes: 1 addition & 1 deletion src/memorytext/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
59 changes: 47 additions & 12 deletions src/memorytext/delegates/message_list.py
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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
)
Expand All @@ -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)

Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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)
2 changes: 1 addition & 1 deletion src/memorytext/io/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/memorytext/models/message_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/memorytext/services/message_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down
Loading