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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Thumbs.db
# Roadmap / Ideas
**/Roadmap.md

# vscode
**/.vscode/


# Python cache
*.pyc
__pycache__/
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## Added

1. Add day separator before the first message of a day.
2. Add a jump / navigate to date floating window triggered with:
- a shortcut: Ctrl+Shift+F.
- a toobar button


---
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ pip install git+https://gitlab.com/sharmasiddhant/memory.text.git

### Navigation & Structure

- [ ] Day markers
- [ ] Jump to a specific date
- [x] Day markers
- [x] Jump to a specific date
- [ ] Timeline-style navigation

### Filter and Search
Expand Down
4 changes: 2 additions & 2 deletions src/memorytext/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ class Message(Base):
conversation: Mapped["Conversation"] = relationship(back_populates="messages")

def __str__(self) -> str:
dt_fmt = "%Y-%d-%m %H:%M"
dt_fmt = "%Y-%m-%d %H:%M"
return f"{self.timestamp.strftime(dt_fmt)} - {self.sender}: {self.text}"

def __repr__(self) -> str:
dt_fmt = "%Y-%d-%m %H:%M"
dt_fmt = "%Y-%m-%d %H:%M"
return f"{self.timestamp.strftime(dt_fmt)} - {self.sender}: {self.text}"
25 changes: 24 additions & 1 deletion src/memorytext/models/message_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

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.services import chat_service, message_service
from memorytext.models.roles import Roles

if TYPE_CHECKING:
from collections.abc import Callable
import datetime
from memorytext.models.core import Message


Expand Down Expand Up @@ -107,3 +108,25 @@ def fetchMore(
self.beginInsertRows(QModelIndex(), offset, offset + count - 1)
self._items.extend(new_messages)
self.endInsertRows()

def jump_to_date(self, date: datetime.date):
limit = 10
target_offset = message_service.get_offset_from_date(
self._conversation_id, date
)
current_count = self.rowCount()

if target_offset is not None and target_offset > current_count:
to_fetch = target_offset - current_count + limit

new_messages, count = self._message_service(
self._conversation_id, to_fetch, current_count
)
if count:
self.beginInsertRows(
QModelIndex(), current_count, current_count + count - 1
)
self._items.extend(new_messages)
self.endInsertRows()
if target_offset is not None:
return self.index(target_offset)
9 changes: 9 additions & 0 deletions src/memorytext/services/message_service.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import memorytext.storage.message_repo as mrepo
from memorytext.storage.db import get_db_session

if TYPE_CHECKING:
import datetime


def get_messages(
conversation_id: int | None, limit: int = 10, offset: int = 0
) -> tuple[list, int]:
with get_db_session() as session:
return mrepo.get_messages(session, conversation_id, limit, offset)


def get_offset_from_date(conversation_id: int | None, date: datetime.date):
with get_db_session() as session:
return mrepo.get_offset_from_date(session, conversation_id, date)
27 changes: 27 additions & 0 deletions src/memorytext/storage/message_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

if TYPE_CHECKING:
from sqlalchemy.orm import Session
from sqlalchemy import Row
from datetime import date, datetime


def get_messages(
Expand All @@ -25,3 +27,28 @@ def get_messages(
message_list = list(session.scalars(stmt_2).fetchall())
session.expunge_all()
return message_list, total


def get_offset_from_date(session: Session, conversation_id: int | None, date: date):
timestamps = get_first_and_last_ts(session, conversation_id)
date_bdry = date
if timestamps:
first_ts, last_ts = timestamps
if first_ts and date < first_ts.date():
return 0
elif last_ts and date > last_ts.date():
date_bdry = last_ts.date()

stmt = select(func.count(Message.id)).where(
Message.conversation_id == conversation_id, Message.timestamp < date_bdry
)
return session.scalar(stmt)


def get_first_and_last_ts(
session: Session, conversation_id: int | None
) -> Row[tuple[datetime, datetime]] | None:
stmt = select(func.min(Message.timestamp), func.max(Message.timestamp)).where(
Message.conversation_id == conversation_id
)
return session.execute(stmt).fetchone()
7 changes: 7 additions & 0 deletions src/memorytext/views/chat_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

if TYPE_CHECKING:
from collections.abc import Callable
import datetime


class ChatView(QListView):
Expand Down Expand Up @@ -42,3 +43,9 @@ def set_messages(
def resizeEvent(self, event):
super().resizeEvent(event)
self.doItemsLayout()

def jump_to_date(self, date: datetime.date):
target_index = self.message_model.jump_to_date(date)
if target_index:
self.scrollTo(target_index, self.ScrollHint.PositionAtTop)
self.setCurrentIndex(target_index)
47 changes: 45 additions & 2 deletions src/memorytext/windows/helper_windows.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations
from typing import TYPE_CHECKING

import datetime
from PySide6.QtWidgets import (
QDateEdit,
QComboBox,
QFormLayout,
QMainWindow,
Expand All @@ -12,7 +13,7 @@
QWidget,
QLabel,
)
from PySide6.QtCore import Slot
from PySide6.QtCore import Slot, Qt, QDate, Signal
from memorytext.services import chat_service

if TYPE_CHECKING:
Expand Down Expand Up @@ -162,3 +163,45 @@ def __init__(self, text: str, ok_action: Callable):
container.setLayout(layout)

self.setCentralWidget(container)


class Floating_Input(QWidget):
selected_date = Signal(datetime.date)

def __init__(
self,
label: str,
set_date: datetime.date = datetime.date.today(),
chat_view_open: bool = False,
):
super().__init__()

self.chat_view_open = chat_view_open

self.setWindowFlags(
Qt.WindowType.FramelessWindowHint
| Qt.WindowType.WindowStaysOnTopHint
| Qt.WindowType.Tool
)
layout = QFormLayout(self)
self.input = QDateEdit(self)
self.input.setCalendarPopup(True)
self.input.setDate(QDate(set_date.year, set_date.month, set_date.day))
self.input.setDisplayFormat("yyyy-MM-dd")
layout.addRow(label, self.input)

self.input.editingFinished.connect(self.emit_input)

def toggle_window(self):
if self.isVisible() or not self.chat_view_open:
self.hide()
else:
self.show()
self.activateWindow()
self.input.setFocus()

def emit_input(self):
if self.input.hasFocus():
qdate = self.input.date()
self.selected_date.emit(qdate.toPython())
self.hide()
50 changes: 49 additions & 1 deletion src/memorytext/windows/main_window.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from PySide6.QtWidgets import (
QMainWindow,
QHBoxLayout,
Expand All @@ -6,13 +9,18 @@
QWidget,
)
from PySide6.QtCore import Qt, QModelIndex
from PySide6.QtGui import QAction, QIcon
from PySide6.QtGui import QAction, QIcon, QKeySequence, QShortcut
from memorytext.views.chat_view import ChatView
from memorytext.views.chat_list_view import ChatListView
from memorytext.windows.import_window import ImportWindow
from memorytext.windows.helper_windows import Floating_Input
from memorytext.services import message_service

if TYPE_CHECKING:
import datetime

IMPORT_ICON = QIcon.fromTheme("list-add")
JUMP_TO_DATE_ICON = QIcon.fromTheme("go-next")


class MainWindow(QMainWindow):
Expand All @@ -28,8 +36,16 @@ def __init__(self):
# Toolbar
toolbar = QToolBar("Main Toolbar")
self.addToolBar(toolbar)

self.import_action = QAction(IMPORT_ICON, "Import Chat")
toolbar.addAction(self.import_action)

toolbar.addSeparator()

self.jump_to_date_action = QAction(JUMP_TO_DATE_ICON, "Jump to Date")
self.jump_to_date_action.setDisabled(True)

toolbar.addAction(self.jump_to_date_action)
toolbar.setMovable(False)
toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)

Expand All @@ -50,9 +66,29 @@ def __init__(self):
self.import_window = None
self.import_action.triggered.connect(self.import_new_chat)

# Jump to Date
self.jump_to_date = Floating_Input(label="Jump to date:")
self.jump_to_date.selected_date.connect(self.on_jump_to_date)

# Shortcut - Jump to Date: Ctrl+Shift+F
self.jump_to_date_shortcut = QShortcut(QKeySequence("Ctrl+Shift+F"), self)
self.jump_to_date_shortcut.activated.connect(self.jump_to_date.toggle_window)

# Jump to Date Action
self.sidebar.selectionModel().selectionChanged.connect(self.update_toolbar)
self.jump_to_date_action.triggered.connect(
self.jump_to_date_shortcut.activated.emit
)
self.jump_to_date_close = QShortcut(
QKeySequence(Qt.Key.Key_Escape), self.jump_to_date
)
self.jump_to_date_close.activated.connect(self.jump_to_date.close)

def load_messages(self, index: QModelIndex):
convo_id = self.sidebar.model().get_id(index) # pyright: ignore[reportAttributeAccessIssue]
self.chat_area.set_messages(convo_id, message_service.get_messages)
self.chat_area.scrollToTop()
self.jump_to_date.chat_view_open = True

def import_new_chat(self):
if self.import_window is None:
Expand All @@ -63,3 +99,15 @@ def handle_chat_deletion(self, deleted_conversation_id: int):
self.chat_area.set_messages(
deleted_conversation_id, message_service.get_messages
)

def on_jump_to_date(self, date: datetime.date):
target_index = self.chat_area.message_model.jump_to_date(date)
if target_index and target_index.isValid():
self.chat_area.scrollTo(
target_index, self.chat_area.ScrollHint.PositionAtTop
)
self.chat_area.setCurrentIndex(target_index)

def update_toolbar(self):
has_selection = self.sidebar.selectionModel().hasSelection()
self.jump_to_date_action.setEnabled(has_selection)
Loading