This commit is contained in:
Tim Bendt
2025-05-13 08:16:23 -06:00
parent 7123ff1f43
commit 5c9ad69309
4 changed files with 257 additions and 177 deletions

View File

@@ -23,13 +23,25 @@ async def archive_current(app) -> None:
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
) )
stdout, stderr = await process.communicate() stdout, stderr = await process.communicate()
# app.reload_needed = True
app.show_status(f"{stdout.decode()}", "info") app.show_status(f"{stdout.decode()}", "info")
logging.info(stdout.decode()) logging.info(stdout.decode())
if process.returncode == 0: if process.returncode == 0:
# Remove the item from the ListView
app.query_one(ListView).pop(index) app.query_one(ListView).pop(index)
app.query_one(ListView).index = index
# app.action_next() # Automatically show the next message # Find the next message to display using the MessageStore
next_id, next_idx = app.message_store.find_next_valid_id(index)
# Show the next available message
if next_id is not None and next_idx is not None:
# Set ListView index first to ensure UI is synchronized
app.query_one(ListView).index = next_idx
# Now update the current_message_id to trigger content update
app.current_message_id = next_id
else:
# No messages left, just update ListView
app.query_one(ListView).index = 0
app.reload_needed = True
else: else:
app.show_status(f"Error archiving message: {stderr.decode()}", "error") app.show_status(f"Error archiving message: {stderr.decode()}", "error")
except Exception as e: except Exception as e:

View File

@@ -14,12 +14,24 @@ async def delete_current(app) -> None:
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
) )
stdout, stderr = await process.communicate() stdout, stderr = await process.communicate()
# app.reload_needed = True
app.show_status(f"{stdout.decode()}", "info") app.show_status(f"{stdout.decode()}", "info")
if process.returncode == 0: if process.returncode == 0:
# Remove the item from the ListView
await app.query_one(ListView).pop(index) await app.query_one(ListView).pop(index)
app.query_one(ListView).index = index
# app.action_next() # Automatically show the next message # Find the next message to display using the MessageStore
next_id, next_idx = app.message_store.find_next_valid_id(index)
# Show the next available message
if next_id is not None and next_idx is not None:
# Set ListView index first to ensure UI is synchronized
app.query_one(ListView).index = next_idx
# Now update the current_message_id to trigger content update
app.current_message_id = next_id
else:
# No messages left, just update ListView
app.query_one(ListView).index = 0
app.reload_needed = True
else: else:
app.show_status( app.show_status(
f"Failed to delete message {app.current_message_id}. {stderr.decode()}", f"Failed to delete message {app.current_message_id}. {stderr.decode()}",

View File

@@ -4,7 +4,7 @@ import os
from datetime import datetime from datetime import datetime
import asyncio import asyncio
import logging import logging
from typing import Iterable from typing import Iterable, Optional, List, Dict, Any, Generator, Tuple
# Add the parent directory to the system path to resolve relative imports # Add the parent directory to the system path to resolve relative imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -37,6 +37,161 @@ logging.basicConfig(
) )
class MessageStore:
"""Centralized store for email message data with efficient lookups and updates."""
def __init__(self):
self.envelopes: List[Dict[str, Any]] = [] # Full envelope data including headers
self.by_id: Dict[int, Dict[str, Any]] = {} # Map message IDs to envelope data
self.id_to_index: Dict[int, int] = {} # Map message IDs to list indices
self.total_messages = 0
self.sort_ascending = True
def clear(self) -> None:
"""Clear all data structures."""
self.envelopes = []
self.by_id = {}
self.id_to_index = {}
self.total_messages = 0
def load(self, raw_envelopes: List[Dict[str, Any]], sort_ascending: bool = True) -> None:
"""Load envelopes from raw data and set up the data structures."""
self.clear()
self.sort_ascending = sort_ascending
# Sort the envelopes by date
sorted_envelopes = sorted(
raw_envelopes,
key=lambda x: x["date"],
reverse=not sort_ascending,
)
# Group them by date for display
self.envelopes = group_envelopes_by_date(sorted_envelopes)
# Build lookup dictionaries
for idx, envelope in enumerate(self.envelopes):
if "id" in envelope and envelope.get("type") != "header":
msg_id = int(envelope["id"])
self.by_id[msg_id] = envelope
self.id_to_index[msg_id] = idx
# Count actual messages (excluding headers)
self.total_messages = len(self.by_id)
def get_by_id(self, msg_id: int) -> Optional[Dict[str, Any]]:
"""Get an envelope by its ID."""
return self.by_id.get(msg_id)
def get_index_by_id(self, msg_id: int) -> Optional[int]:
"""Get the list index for a message ID."""
return self.id_to_index.get(msg_id)
def get_metadata(self, msg_id: int) -> Dict[str, Any]:
"""Get essential metadata for a message."""
envelope = self.get_by_id(msg_id)
if not envelope:
return {}
return {
"subject": envelope.get("subject", ""),
"from": envelope.get("from", {}),
"to": envelope.get("to", {}),
"date": envelope.get("date", ""),
"cc": envelope.get("cc", {}),
"index": self.get_index_by_id(msg_id),
}
def remove(self, msg_id: int) -> None:
"""Remove a message from all data structures."""
# Get the index first before we remove from dictionaries
idx = self.id_to_index.get(msg_id)
# Remove from dictionaries
self.by_id.pop(msg_id, None)
self.id_to_index.pop(msg_id, None)
# Remove from list if we found an index
if idx is not None:
self.envelopes[idx] = None # Mark as None rather than removing to maintain indices
# Update total count
self.total_messages = len(self.by_id)
def find_next_valid_id(self, current_idx: int) -> Tuple[Optional[int], Optional[int]]:
"""Find the next valid message ID and its index after the current index."""
# Look forward first
try:
# Optimized with better short-circuit logic
# Only check type if env exists and has an ID
idx, envelope = next(
(i, env) for i, env in enumerate(self.envelopes[current_idx + 1:], current_idx + 1)
if env and "id" in env and env.get("type") != "header"
)
return int(envelope["id"]), idx
except StopIteration:
# If not found in forward direction, look from beginning
try:
idx, envelope = next(
(i, env) for i, env in enumerate(self.envelopes[:current_idx])
if env and "id" in env and env.get("type") != "header"
)
return int(envelope["id"]), idx
except StopIteration:
return None, None
def find_prev_valid_id(self, current_idx: int) -> Tuple[Optional[int], Optional[int]]:
"""Find the previous valid message ID and its index before the current index."""
# Look backward first
try:
# Create a range of indices in reverse order
backward_range = range(current_idx - 1, -1, -1) # No need to convert to list
# Using optimized short-circuit evaluation
idx, envelope = next(
(i, self.envelopes[i]) for i in backward_range
if self.envelopes[i] and "id" in self.envelopes[i] and self.envelopes[i].get("type") != "header"
)
return int(envelope["id"]), idx
except StopIteration:
# If not found, look from end downward to current
try:
backward_range = range(len(self.envelopes) - 1, current_idx, -1) # No need to convert to list
idx, envelope = next(
(i, self.envelopes[i]) for i in backward_range
if self.envelopes[i] and "id" in self.envelopes[i] and self.envelopes[i].get("type") != "header"
)
return int(envelope["id"]), idx
except StopIteration:
return None, None
def get_oldest_id(self) -> Optional[int]:
"""Get the ID of the oldest message."""
if not self.envelopes:
return None
for envelope in self.envelopes:
if envelope and "id" in envelope and envelope.get("type") != "header":
return int(envelope["id"])
return None
def get_newest_id(self) -> Optional[int]:
"""Get the ID of the newest message."""
if not self.envelopes:
return None
for envelope in reversed(self.envelopes):
if envelope and "id" in envelope and envelope.get("type") != "header":
return int(envelope["id"])
return None
def get_valid_envelopes(self) -> Generator[Dict[str, Any], None, None]:
"""Get all valid (non-header) envelopes."""
return (envelope for envelope in self.envelopes
if envelope and "id" in envelope and envelope.get("type") != "header")
class StatusTitle(Static): class StatusTitle(Static):
total_messages: Reactive[int] = reactive(0) total_messages: Reactive[int] = reactive(0)
current_message_index: Reactive[int] = reactive(0) current_message_index: Reactive[int] = reactive(0)
@@ -57,18 +212,13 @@ class EmailViewerApp(App):
folder = reactive("INBOX") folder = reactive("INBOX")
header_expanded = reactive(False) header_expanded = reactive(False)
reload_needed = reactive(True) reload_needed = reactive(True)
all_envelopes = reactive([]) # Keep as list for compatibility with ListView message_store = MessageStore()
envelope_map = {} # Add a dictionary to map IDs to envelope data
envelope_index_map = {} # Map indices in the list to envelope IDs
oldest_id: Reactive[int] = reactive(0) oldest_id: Reactive[int] = reactive(0)
newest_id: Reactive[int] = reactive(0) newest_id: Reactive[int] = reactive(0)
msg_worker: Worker | None = None msg_worker: Worker | None = None
message_metadata: dict[int, dict] = {}
message_body_cache: dict[int, str] = {}
total_messages: Reactive[int] = reactive(0) total_messages: Reactive[int] = reactive(0)
status_title = reactive("Message View") status_title = reactive("Message View")
sort_order_ascending: Reactive[bool] = reactive(True) sort_order_ascending: Reactive[bool] = reactive(True)
valid_envelopes = reactive([])
def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]: def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
yield from super().get_system_commands(screen) yield from super().get_system_commands(screen)
@@ -149,9 +299,6 @@ class EmailViewerApp(App):
self.query_one("#folders_list").border_title = "\[3] Folders" self.query_one("#folders_list").border_title = "\[3] Folders"
# self.query_one(ListView).data_bind(index=EmailViewerApp.current_message_index)
# self.watch(self.query_one(StatusTitle), "current_message_id", update_progress)
# Fetch the ID of the most recent message using the Himalaya CLI
self.fetch_accounts() self.fetch_accounts()
self.fetch_folders() self.fetch_folders()
worker = self.fetch_envelopes() worker = self.fetch_envelopes()
@@ -162,9 +309,6 @@ class EmailViewerApp(App):
def compute_status_title(self) -> None: def compute_status_title(self) -> None:
return f"✉️ Message ID: {self.current_message_id} " return f"✉️ Message ID: {self.current_message_id} "
def compute_valid_envelopes(self) -> None:
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(ContentContainer).border_title = new_status_title self.query_one(ContentContainer).border_title = new_status_title
@@ -185,26 +329,6 @@ class EmailViewerApp(App):
).border_subtitle = f"[b]{new_index}[/b]/{self.total_messages}" ).border_subtitle = f"[b]{new_index}[/b]/{self.total_messages}"
self.query_one("#envelopes_list").index = new_index self.query_one("#envelopes_list").index = new_index
def compute_newest_id(self) -> None:
if not self.all_envelopes:
return 0
items = sorted(
self.valid_envelopes,
key=lambda x: x["date"],
reverse=not self.sort_order_ascending,
)
return items[-1]["id"] if items else 0
def compute_oldest_id(self) -> None:
if not self.valid_envelopes:
return 0
items = sorted(
self.valid_envelopes,
key=lambda x: x["date"],
reverse=not self.sort_order_ascending,
)
return items[0]["id"] if items else 0
def watch_reload_needed( def watch_reload_needed(
self, old_reload_needed: bool, new_reload_needed: bool self, old_reload_needed: bool, new_reload_needed: bool
) -> None: ) -> None:
@@ -223,20 +347,17 @@ class EmailViewerApp(App):
return return
self.msg_worker.cancel() if self.msg_worker else None self.msg_worker.cancel() if self.msg_worker else None
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())}")
content_container = self.query_one(ContentContainer) 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: metadata = self.message_store.get_metadata(new_message_id)
metadata = self.message_metadata[new_message_id] if metadata:
message_date = re.sub(r"[\+\-]\d\d:\d\d", "", metadata["date"]) message_date = re.sub(r"[\+\-]\d\d:\d\d", "", metadata["date"])
message_date = datetime.strptime(message_date, "%Y-%m-%d %H:%M").strftime( message_date = datetime.strptime(message_date, "%Y-%m-%d %H:%M").strftime(
"%a %b %d %H:%M" "%a %b %d %H:%M"
) )
# Only update the current_message_index if it's different from the index in the ListView
# This prevents the sidebar selection from getting out of sync with the displayed content
if self.current_message_index != metadata["index"]: if self.current_message_index != metadata["index"]:
self.current_message_index = metadata["index"] self.current_message_index = metadata["index"]
@@ -248,7 +369,6 @@ class EmailViewerApp(App):
cc=metadata["cc"].get("addr", "") if "cc" in metadata else "", cc=metadata["cc"].get("addr", "") if "cc" in metadata else "",
) )
# Make sure the ListView index matches the current message index
list_view = self.query_one("#envelopes_list") list_view = self.query_one("#envelopes_list")
if list_view.index != metadata["index"]: if list_view.index != metadata["index"]:
list_view.index = metadata["index"] list_view.index = metadata["index"]
@@ -257,18 +377,13 @@ class EmailViewerApp(App):
def on_list_view_selected(self, event: ListView.Selected) -> None: def on_list_view_selected(self, event: ListView.Selected) -> None:
"""Called when an item in the list view is selected.""" """Called when an item in the list view is selected."""
current_item = self.all_envelopes[event.list_view.index] current_item = self.message_store.envelopes[event.list_view.index]
# Skip if it's a header or None
if current_item is None or current_item.get("type") == "header": if current_item is None or current_item.get("type") == "header":
return return
# Get the message ID and update current index in a consistent way
message_id = int(current_item["id"]) message_id = int(current_item["id"])
self.current_message_id = message_id self.current_message_id = message_id
# Update the index directly based on the ListView selection
# This ensures the sidebar selection and displayed content stay in sync
self.current_message_index = event.list_view.index self.current_message_index = event.list_view.index
@work(exclusive=False) @work(exclusive=False)
@@ -289,37 +404,11 @@ class EmailViewerApp(App):
envelopes = json.loads(stdout.decode()) envelopes = json.loads(stdout.decode())
if envelopes: if envelopes:
self.reload_needed = False self.reload_needed = False
self.total_messages = len(envelopes) self.message_store.load(envelopes, self.sort_order_ascending)
self.total_messages = self.message_store.total_messages
msglist.clear() msglist.clear()
envelopes = sorted( for item in self.message_store.envelopes:
envelopes,
key=lambda x: x["date"],
reverse=not self.sort_order_ascending,
)
grouped_envelopes = group_envelopes_by_date(envelopes)
self.all_envelopes = grouped_envelopes
# Update our dictionary mappings
self.envelope_map = {int(envelope["id"]): envelope for envelope in grouped_envelopes if "id" in envelope}
self.envelope_index_map = {index: int(envelope["id"]) for index, envelope in enumerate(grouped_envelopes) if "id" in envelope}
# Store metadata with correct indices
self.message_metadata = {
int(envelope["id"]): {
"subject": envelope.get("subject", ""),
"from": envelope.get("from", {}),
"to": envelope.get("to", {}),
"date": envelope.get("date", ""),
"cc": envelope.get("cc", {}),
"index": index, # Store the position index
}
for index, envelope in enumerate(self.all_envelopes)
if "id" in envelope
}
# Add items to the ListView
for item in grouped_envelopes:
if item.get("type") == "header": if item.get("type") == "header":
msglist.append( msglist.append(
ListItem( ListItem(
@@ -431,7 +520,6 @@ class EmailViewerApp(App):
worker = self.fetch_envelopes() worker = self.fetch_envelopes()
await worker.wait() await worker.wait()
# Call action_newest or action_oldest based on the new sort order
if self.sort_order_ascending: if self.sort_order_ascending:
self.action_oldest() self.action_oldest()
else: else:
@@ -445,109 +533,36 @@ class EmailViewerApp(App):
def action_next(self) -> None: def action_next(self) -> None:
if not self.current_message_index >= 0: if not self.current_message_index >= 0:
return return
modifier = 1
idx = self.current_message_index next_id, next_idx = self.message_store.find_next_valid_id(self.current_message_index)
try: if next_id is not None and next_idx is not None:
if ( self.current_message_id = next_id
self.all_envelopes[idx + modifier] is None self.current_message_index = next_idx
or self.all_envelopes[idx + modifier].get("type") == "header"
):
idx = idx + modifier
except IndexError:
# If we reach the end of the list, wrap around to the beginning
idx = 0
self.show_message(self.all_envelopes[idx + modifier].get("id"), idx + modifier)
self.fetch_envelopes() if self.reload_needed else None self.fetch_envelopes() if self.reload_needed else None
def action_previous(self) -> None: def action_previous(self) -> None:
if not self.current_message_index >= 0: if not self.current_message_index >= 0:
return return
modifier = -1
idx = self.current_message_index prev_id, prev_idx = self.message_store.find_prev_valid_id(self.current_message_index)
try: if prev_id is not None and prev_idx is not None:
if ( self.current_message_id = prev_id
self.all_envelopes[idx + modifier] is None self.current_message_index = prev_idx
or self.all_envelopes[idx + modifier].get("type") == "header"
):
idx = idx + modifier
except IndexError:
# If we reach the beginning of the list, wrap around to the end
idx = len(self.all_envelopes) - 1
self.show_message(self.all_envelopes[idx + modifier].get("id"), idx + modifier)
self.fetch_envelopes() if self.reload_needed else None self.fetch_envelopes() if self.reload_needed else None
async def action_delete(self) -> None: async def action_delete(self) -> None:
# Remove from all data structures message_id_to_delete = self.current_message_id
self.all_envelopes = [item for item in self.all_envelopes if item and item.get("id") != self.current_message_id] self.message_store.remove(message_id_to_delete)
self.envelope_map.pop(self.current_message_id, None) self.total_messages = self.message_store.total_messages
self.envelope_index_map = {index: id for index, id in self.envelope_index_map.items() if id != self.current_message_id}
self.message_metadata = {
k: v for k, v in self.message_metadata.items() if k != self.current_message_id
}
self.message_body_cache = {
k: v for k, v in self.message_body_cache.items() if k != self.current_message_id
}
self.total_messages = len(self.message_metadata)
# Perform delete operation
delete_current(self) delete_current(self)
# Get next message to display
try:
newmsg = self.all_envelopes[self.current_message_index]
# Skip headers
if newmsg.get("type") == "header":
if self.current_message_index + 1 < len(self.all_envelopes):
newmsg = self.all_envelopes[self.current_message_index + 1]
else:
# If we're at the end, go to the previous message
newmsg = self.all_envelopes[self.current_message_index - 1]
self.current_message_index -= 1
# Show the next message
if "id" in newmsg:
self.show_message(newmsg["id"])
except (IndexError, KeyError):
# If no more messages, just reload envelopes
self.reload_needed = True
self.fetch_envelopes()
async def action_archive(self) -> None: async def action_archive(self) -> None:
# Remove from all data structures message_id_to_archive = self.current_message_id
self.all_envelopes = [item for item in self.all_envelopes if item and item.get("id") != self.current_message_id] self.message_store.remove(message_id_to_archive)
self.envelope_map.pop(self.current_message_id, None) self.total_messages = self.message_store.total_messages
self.envelope_index_map = {index: id for index, id in self.envelope_index_map.items() if id != self.current_message_id} archive_current(self)
self.message_metadata = {
k: v for k, v in self.message_metadata.items() if k != self.current_message_id
}
self.message_body_cache = {
k: v for k, v in self.message_body_cache.items() if k != self.current_message_id
}
self.total_messages = len(self.message_metadata)
# Perform archive operation
worker = archive_current(self)
await worker.wait()
# Get next message to display
try:
newmsg = self.all_envelopes[self.current_message_index]
# Skip headers
if newmsg.get("type") == "header":
if self.current_message_index + 1 < len(self.all_envelopes):
newmsg = self.all_envelopes[self.current_message_index + 1]
else:
# If we're at the end, go to the previous message
newmsg = self.all_envelopes[self.current_message_index - 1]
self.current_message_index -= 1
# Show the next message
if "id" in newmsg:
self.show_message(newmsg["id"])
except (IndexError, KeyError):
# If no more messages, just reload envelopes
self.reload_needed = True
self.fetch_envelopes()
def action_open(self) -> None: def action_open(self) -> None:
action_open(self) action_open(self)
@@ -577,11 +592,11 @@ class EmailViewerApp(App):
def action_oldest(self) -> None: def action_oldest(self) -> None:
self.fetch_envelopes() if self.reload_needed else None self.fetch_envelopes() if self.reload_needed else None
self.show_message(self.oldest_id) self.show_message(self.message_store.get_oldest_id())
def action_newest(self) -> None: def action_newest(self) -> None:
self.fetch_envelopes() if self.reload_needed else None self.fetch_envelopes() if self.reload_needed else None
self.show_message(self.newest_id) self.show_message(self.message_store.get_newest_id())
def action_focus_1(self) -> None: def action_focus_1(self) -> None:
self.query_one("#envelopes_list").focus() self.query_one("#envelopes_list").focus()

View File

@@ -75,8 +75,10 @@ class ContentContainer(ScrollableContainer):
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:
# Store the ID of the message we're currently loading
loading_id = message_id
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",
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
@@ -85,15 +87,46 @@ class ContentContainer(ScrollableContainer):
stdout, stderr = await process.communicate() stdout, stderr = await process.communicate()
logging.info(f"stdout: {stdout.decode()[0:50]}...") logging.info(f"stdout: {stdout.decode()[0:50]}...")
# Check if we're still loading the same message or if navigation has moved on
if loading_id != self.current_id:
logging.info(f"Message ID changed during loading. Abandoning load of {loading_id}")
return ""
if process.returncode == 0: if process.returncode == 0:
# Process the email content # Process the email content
fixed_text = stdout.decode().replace("https://urldefense.com/v3/", "") content = stdout.decode()
# Remove header lines from the beginning of the message
# Headers typically end with a blank line before the message body
lines = content.split('\n')
body_start = 0
# Find the first blank line which typically separates headers from body
for i, line in enumerate(lines):
if line.strip() == '' and i > 0:
# Check if we're past the headers section
# Headers are typically in "Key: Value" format
has_headers = any(': ' in l for l in lines[:i])
if has_headers:
body_start = i + 1
break
# Join the body lines back together
content = '\n'.join(lines[body_start:])
# Apply existing cleanup logic
fixed_text = content.replace("https://urldefense.com/v3/", "")
fixed_text = re.sub(r"atlOrigin.+?\w", "", fixed_text) fixed_text = re.sub(r"atlOrigin.+?\w", "", fixed_text)
logging.info(f"rendering fixedText: {fixed_text[0:50]}") logging.info(f"rendering fixedText: {fixed_text[0:50]}")
self.current_text = fixed_text self.current_text = fixed_text
self.message_cache[message_id] = fixed_text self.message_cache[message_id] = fixed_text
# Check again if we're still on the same message before updating UI
if loading_id != self.current_id:
logging.info(f"Message ID changed after loading. Abandoning update for {loading_id}")
return fixed_text
# Update the plaintext content # Update the plaintext content
plaintext = self.query_one("#plaintext_content", Label) plaintext = self.query_one("#plaintext_content", Label)
plaintext.update(fixed_text) plaintext.update(fixed_text)
@@ -107,12 +140,20 @@ class ContentContainer(ScrollableContainer):
self.query_one("#markdown_content").add_class("hidden") self.query_one("#markdown_content").add_class("hidden")
self.loading = False self.loading = False
return fixed_text
else: else:
logging.error(f"Error fetching message: {stderr.decode()}") logging.error(f"Error fetching message: {stderr.decode()}")
self.loading = False
return f"Error fetching message content: {stderr.decode()}" return f"Error fetching message content: {stderr.decode()}"
except Exception as e: except Exception as e:
logging.error(f"Error fetching message content: {e}") logging.error(f"Error fetching message content: {e}")
self.loading = False
return f"Error fetching message content: {e}" return f"Error fetching message content: {e}"
finally:
# Ensure loading state is always reset if this worker completes
# This prevents the loading indicator from getting stuck
if loading_id == self.current_id:
self.loading = False
async def render_markdown(self) -> None: async def render_markdown(self) -> None:
"""Render the markdown content asynchronously.""" """Render the markdown content asynchronously."""