fixes to email display
This commit is contained in:
1
Congruence
Submodule
1
Congruence
Submodule
Submodule Congruence added at 2860e1884d
@@ -3,6 +3,7 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import msal
|
import msal
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -34,6 +35,20 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "maildir_gtd"))
|
|||||||
from maildir_gtd.screens.DocumentViewer import DocumentViewerScreen
|
from maildir_gtd.screens.DocumentViewer import DocumentViewerScreen
|
||||||
|
|
||||||
|
|
||||||
|
class FolderHistoryEntry:
|
||||||
|
"""Represents an entry in the folder navigation history."""
|
||||||
|
|
||||||
|
def __init__(self, folder_id: str, folder_name: str, parent_id: str = None):
|
||||||
|
self.folder_id = folder_id
|
||||||
|
self.folder_name = folder_name
|
||||||
|
self.parent_id = parent_id
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, FolderHistoryEntry):
|
||||||
|
return False
|
||||||
|
return self.folder_id == other.folder_id
|
||||||
|
|
||||||
|
|
||||||
class OneDriveTUI(App):
|
class OneDriveTUI(App):
|
||||||
"""A Textual app for OneDrive integration with MSAL authentication."""
|
"""A Textual app for OneDrive integration with MSAL authentication."""
|
||||||
|
|
||||||
@@ -44,6 +59,8 @@ class OneDriveTUI(App):
|
|||||||
selected_drive_id = reactive("")
|
selected_drive_id = reactive("")
|
||||||
drive_name = reactive("")
|
drive_name = reactive("")
|
||||||
current_view = reactive("Following") # Track current view: "Following" or "Root"
|
current_view = reactive("Following") # Track current view: "Following" or "Root"
|
||||||
|
current_folder_id = reactive("root") # Track current folder ID
|
||||||
|
current_folder_name = reactive("Root") # Track current folder name
|
||||||
|
|
||||||
# App bindings
|
# App bindings
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
@@ -54,6 +71,8 @@ class OneDriveTUI(App):
|
|||||||
Binding("enter", "open_url", "Open URL"),
|
Binding("enter", "open_url", "Open URL"),
|
||||||
Binding("v", "view_document", "View Document"),
|
Binding("v", "view_document", "View Document"),
|
||||||
Binding("tab", "next_view", "Switch View"),
|
Binding("tab", "next_view", "Switch View"),
|
||||||
|
Binding("backspace", "navigate_back", "Back"),
|
||||||
|
Binding("b", "navigate_back", "Back"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -61,8 +80,8 @@ class OneDriveTUI(App):
|
|||||||
self.access_token = None
|
self.access_token = None
|
||||||
self.drives = []
|
self.drives = []
|
||||||
self.followed_items = []
|
self.followed_items = []
|
||||||
|
|
||||||
self.current_items = {} # Store currently displayed items
|
self.current_items = {} # Store currently displayed items
|
||||||
|
self.folder_history = [] # History stack for folder navigation
|
||||||
self.msal_app = None
|
self.msal_app = None
|
||||||
self.cache = msal.SerializableTokenCache()
|
self.cache = msal.SerializableTokenCache()
|
||||||
# Read Azure app credentials from environment variables
|
# Read Azure app credentials from environment variables
|
||||||
@@ -287,18 +306,22 @@ class OneDriveTUI(App):
|
|||||||
self.action_view_document()
|
self.action_view_document()
|
||||||
|
|
||||||
@work
|
@work
|
||||||
async def load_root_items(self, folder_id: str = "", drive_id: str = ""):
|
async def load_root_items(self, folder_id: str = "", drive_id: str = "", track_history: bool = True):
|
||||||
"""Load root items from the selected drive."""
|
"""Load root items from the selected drive."""
|
||||||
if not self.access_token or not self.selected_drive_id:
|
if not self.access_token or not self.selected_drive_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.query_one("#status_label").update("Loading root items...")
|
self.query_one("#status_label").update("Loading drive folder items...")
|
||||||
headers = {"Authorization": f"Bearer {self.access_token}"}
|
headers = {"Authorization": f"Bearer {self.access_token}"}
|
||||||
url = f"https://graph.microsoft.com/v1.0/me/drive/root/children"
|
url = f"https://graph.microsoft.com/v1.0/me/drive/root/children"
|
||||||
|
|
||||||
if folder_id and drive_id:
|
if folder_id and drive_id:
|
||||||
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{folder_id}/children"
|
url = f"https://graph.microsoft.com/v1.0/drives/{drive_id}/items/{folder_id}/children"
|
||||||
|
if track_history:
|
||||||
|
self.folder_history.append(FolderHistoryEntry(folder_id, self.current_folder_name, drive_id))
|
||||||
self.selected_drive_id = drive_id
|
self.selected_drive_id = drive_id
|
||||||
|
self.current_folder_id = folder_id
|
||||||
|
self.current_folder_name = self.current_items[folder_id].get("name", "Unknown")
|
||||||
try:
|
try:
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
@@ -311,7 +334,7 @@ class OneDriveTUI(App):
|
|||||||
|
|
||||||
|
|
||||||
# Update the table with the root items
|
# Update the table with the root items
|
||||||
self.update_items_table(items_data.get("value", []), is_root_view=True)
|
self.update_items_table(items_data.get("value", []))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.notify(f"Error loading root items: {str(e)}", severity="error")
|
self.notify(f"Error loading root items: {str(e)}", severity="error")
|
||||||
|
|
||||||
@@ -456,6 +479,16 @@ class OneDriveTUI(App):
|
|||||||
"""Quit the application."""
|
"""Quit the application."""
|
||||||
self.exit()
|
self.exit()
|
||||||
|
|
||||||
|
async def action_navigate_back(self) -> None:
|
||||||
|
"""Navigate back to the previous folder."""
|
||||||
|
if self.folder_history:
|
||||||
|
previous_entry = self.folder_history.pop()
|
||||||
|
self.current_folder_id = previous_entry.folder_id
|
||||||
|
self.current_folder_name = previous_entry.folder_name
|
||||||
|
self.load_root_items(folder_id=previous_entry.folder_id, drive_id=previous_entry.parent_id, track_history=False)
|
||||||
|
else:
|
||||||
|
self.notify("No previous folder to navigate back to")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = OneDriveTUI()
|
app = OneDriveTUI()
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class EmailViewerApp(App):
|
|||||||
return (envelope for envelope in self.all_envelopes if envelope.get("id"))
|
return (envelope for envelope in self.all_envelopes if envelope.get("id"))
|
||||||
|
|
||||||
def watch_status_title(self, old_status_title: str, new_status_title: str) -> None:
|
def watch_status_title(self, old_status_title: str, new_status_title: str) -> None:
|
||||||
self.query_one("#main_content").border_title = new_status_title
|
self.query_one(ContentContainer).border_title = new_status_title
|
||||||
|
|
||||||
def watch_sort_order_ascending(self, old_value: bool, new_value: bool) -> None:
|
def watch_sort_order_ascending(self, old_value: bool, new_value: bool) -> None:
|
||||||
"""Update the border title of the envelopes list when the sort order changes."""
|
"""Update the border title of the envelopes list when the sort order changes."""
|
||||||
@@ -223,7 +223,7 @@ class EmailViewerApp(App):
|
|||||||
logging.info(f"new_message_id: {new_message_id}, type: {type(new_message_id)}")
|
logging.info(f"new_message_id: {new_message_id}, type: {type(new_message_id)}")
|
||||||
logging.info(f"message_metadata keys: {list(self.message_metadata.keys())}")
|
logging.info(f"message_metadata keys: {list(self.message_metadata.keys())}")
|
||||||
|
|
||||||
content_container = self.query_one("#main_content")
|
content_container = self.query_one(ContentContainer)
|
||||||
content_container.display_content(new_message_id)
|
content_container.display_content(new_message_id)
|
||||||
|
|
||||||
if new_message_id in self.message_metadata:
|
if new_message_id in self.message_metadata:
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ class ContentContainer(ScrollableContainer):
|
|||||||
self.markup_worker = None
|
self.markup_worker = None
|
||||||
self.current_text = ""
|
self.current_text = ""
|
||||||
self.current_id = None
|
self.current_id = None
|
||||||
|
self.message_cache = dict()
|
||||||
# LRU cache with a max size of 100 messages
|
# LRU cache with a max size of 100 messages
|
||||||
self.get_message_body = lru_cache(maxsize=100)(self._get_message_body)
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""Compose the container with a label for plaintext and markdown for rich content."""
|
"""Compose the container with a label for plaintext and markdown for rich content."""
|
||||||
@@ -29,7 +30,6 @@ class ContentContainer(ScrollableContainer):
|
|||||||
yield Markdown(id="markdown_content", classes="hidden")
|
yield Markdown(id="markdown_content", classes="hidden")
|
||||||
|
|
||||||
def update_header(self, subject: str = "", date: str = "", from_: str = "", to: str = "", cc: str = "", bcc: str = "") -> None:
|
def update_header(self, subject: str = "", date: str = "", from_: str = "", to: str = "", cc: str = "", bcc: str = "") -> None:
|
||||||
"""Update the header with the given email details."""
|
|
||||||
header = self.query_one(EnvelopeHeader)
|
header = self.query_one(EnvelopeHeader)
|
||||||
header.subject = subject
|
header.subject = subject
|
||||||
header.date = date
|
header.date = date
|
||||||
@@ -44,34 +44,37 @@ class ContentContainer(ScrollableContainer):
|
|||||||
header.styles.height = "1" if self.header_expanded else "auto"
|
header.styles.height = "1" if self.header_expanded else "auto"
|
||||||
self.header_expanded = not self.header_expanded
|
self.header_expanded = not self.header_expanded
|
||||||
|
|
||||||
async def display_content(self, message_id: int) -> None:
|
def display_content(self, message_id: int) -> None:
|
||||||
"""Display content for the given message ID."""
|
"""Display content for the given message ID."""
|
||||||
self.current_id = message_id
|
self.current_id = message_id
|
||||||
|
|
||||||
# Show loading state
|
# Show loading state
|
||||||
self.loading = True
|
self.loading = True
|
||||||
|
# Check if the message is already cached
|
||||||
# Get message body (from cache or fetch new)
|
if message_id in self.message_cache:
|
||||||
message_text = await self.get_message_body(message_id)
|
self.current_text = self.message_cache[message_id]
|
||||||
self.current_text = message_text
|
|
||||||
|
|
||||||
# Update the plaintext content
|
|
||||||
plaintext = self.query_one("#plaintext_content", Label)
|
plaintext = self.query_one("#plaintext_content", Label)
|
||||||
await plaintext.update(message_text)
|
plaintext.update(self.current_text)
|
||||||
|
|
||||||
if not self.plaintext_mode:
|
if not self.plaintext_mode:
|
||||||
# We're in markdown mode, so render the markdown
|
# We're in markdown mode, so render the markdown
|
||||||
await self.render_markdown()
|
self.render_markdown()
|
||||||
else:
|
else:
|
||||||
# Hide markdown, show plaintext
|
# Hide markdown, show plaintext
|
||||||
plaintext.remove_class("hidden")
|
plaintext.remove_class("hidden")
|
||||||
self.query_one("#markdown_content").add_class("hidden")
|
self.query_one("#markdown_content").add_class("hidden")
|
||||||
|
|
||||||
self.loading = False
|
self.loading = False
|
||||||
|
return self.current_text
|
||||||
|
else:
|
||||||
|
# Get message body (from cache or fetch new)
|
||||||
|
self.get_message_body(message_id)
|
||||||
|
|
||||||
|
|
||||||
@work(exclusive=True)
|
@work(exclusive=True)
|
||||||
async def _get_message_body(self, message_id: int) -> str:
|
async def get_message_body(self, message_id: int) -> str:
|
||||||
"""Fetch the message body from Himalaya CLI."""
|
"""Fetch the message body from Himalaya CLI."""
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process = await asyncio.create_subprocess_shell(
|
process = await asyncio.create_subprocess_shell(
|
||||||
f"himalaya message read {str(message_id)} -p",
|
f"himalaya message read {str(message_id)} -p",
|
||||||
@@ -83,10 +86,26 @@ class ContentContainer(ScrollableContainer):
|
|||||||
|
|
||||||
if process.returncode == 0:
|
if process.returncode == 0:
|
||||||
# Process the email content
|
# Process the email content
|
||||||
fixedText = stdout.decode().replace("https://urldefense.com/v3/", "")
|
fixed_text = stdout.decode().replace("https://urldefense.com/v3/", "")
|
||||||
fixedText = re.sub(r"atlOrigin.+?\w", "", fixedText)
|
fixed_text = re.sub(r"atlOrigin.+?\w", "", fixed_text)
|
||||||
logging.info(f"rendering fixedText: {fixedText[0:50]}")
|
logging.info(f"rendering fixedText: {fixed_text[0:50]}")
|
||||||
return fixedText
|
|
||||||
|
self.current_text = fixed_text
|
||||||
|
self.message_cache[message_id] = fixed_text
|
||||||
|
|
||||||
|
# Update the plaintext content
|
||||||
|
plaintext = self.query_one("#plaintext_content", Label)
|
||||||
|
plaintext.update(fixed_text)
|
||||||
|
|
||||||
|
if not self.plaintext_mode:
|
||||||
|
# We're in markdown mode, so render the markdown
|
||||||
|
self.render_markdown()
|
||||||
|
else:
|
||||||
|
# Hide markdown, show plaintext
|
||||||
|
plaintext.remove_class("hidden")
|
||||||
|
self.query_one("#markdown_content").add_class("hidden")
|
||||||
|
|
||||||
|
self.loading = False
|
||||||
else:
|
else:
|
||||||
logging.error(f"Error fetching message: {stderr.decode()}")
|
logging.error(f"Error fetching message: {stderr.decode()}")
|
||||||
return f"Error fetching message content: {stderr.decode()}"
|
return f"Error fetching message content: {stderr.decode()}"
|
||||||
@@ -122,6 +141,4 @@ class ContentContainer(ScrollableContainer):
|
|||||||
|
|
||||||
return self.plaintext_mode
|
return self.plaintext_mode
|
||||||
|
|
||||||
def clear_cache(self) -> None:
|
|
||||||
"""Clear the message body cache."""
|
|
||||||
self.get_message_body.cache_clear()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user