Add toggle read/unread action with 'u' keybinding in mail app
This commit is contained in:
@@ -452,7 +452,7 @@ Implement `/` keybinding for search across all apps with similar UX:
|
|||||||
4. Calendar: Sidebar mini-calendar
|
4. Calendar: Sidebar mini-calendar
|
||||||
5. Calendar: Calendar invites sidebar
|
5. Calendar: Calendar invites sidebar
|
||||||
6. ~~Mail: Add refresh keybinding~~ (DONE - `r` key)
|
6. ~~Mail: Add refresh keybinding~~ (DONE - `r` key)
|
||||||
7. Mail: Add mark read/unread action
|
7. ~~Mail: Add mark read/unread action~~ (DONE - `u` key)
|
||||||
8. Mail: Folder message counts
|
8. Mail: Folder message counts
|
||||||
9. ~~Mail: URL compression in markdown view~~ (DONE)
|
9. ~~Mail: URL compression in markdown view~~ (DONE)
|
||||||
10. Mail: Enhance subject styling
|
10. Mail: Enhance subject styling
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ class EmailViewerApp(App):
|
|||||||
Binding("space", "toggle_selection", "Toggle selection"),
|
Binding("space", "toggle_selection", "Toggle selection"),
|
||||||
Binding("escape", "clear_selection", "Clear selection"),
|
Binding("escape", "clear_selection", "Clear selection"),
|
||||||
Binding("/", "search", "Search"),
|
Binding("/", "search", "Search"),
|
||||||
|
Binding("u", "toggle_read", "Toggle read/unread"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -905,6 +906,86 @@ class EmailViewerApp(App):
|
|||||||
self.refresh_list_view_items() # Refresh all items to uncheck checkboxes
|
self.refresh_list_view_items() # Refresh all items to uncheck checkboxes
|
||||||
self._update_list_view_subtitle()
|
self._update_list_view_subtitle()
|
||||||
|
|
||||||
|
async def action_toggle_read(self) -> None:
|
||||||
|
"""Toggle read/unread status for the current or selected messages."""
|
||||||
|
folder = self.folder if self.folder else None
|
||||||
|
account = self.current_account if self.current_account else None
|
||||||
|
|
||||||
|
if self.selected_messages:
|
||||||
|
# Toggle multiple selected messages
|
||||||
|
for message_id in self.selected_messages:
|
||||||
|
await self._toggle_message_read_status(message_id, folder, account)
|
||||||
|
self.show_status(
|
||||||
|
f"Toggled read status for {len(self.selected_messages)} messages"
|
||||||
|
)
|
||||||
|
self.selected_messages.clear()
|
||||||
|
else:
|
||||||
|
# Toggle current message
|
||||||
|
if self.current_message_id:
|
||||||
|
await self._toggle_message_read_status(
|
||||||
|
self.current_message_id, folder, account
|
||||||
|
)
|
||||||
|
|
||||||
|
# Refresh the list to show updated read status
|
||||||
|
await self.fetch_envelopes().wait()
|
||||||
|
|
||||||
|
async def _toggle_message_read_status(
|
||||||
|
self, message_id: int, folder: str | None, account: str | None
|
||||||
|
) -> None:
|
||||||
|
"""Toggle read status for a single message."""
|
||||||
|
# Find the message in the store to check current status
|
||||||
|
metadata = self.message_store.get_metadata(message_id)
|
||||||
|
if not metadata:
|
||||||
|
return
|
||||||
|
|
||||||
|
index = metadata.get("index", -1)
|
||||||
|
if index < 0 or index >= len(self.message_store.envelopes):
|
||||||
|
return
|
||||||
|
|
||||||
|
envelope_data = self.message_store.envelopes[index]
|
||||||
|
if not envelope_data or envelope_data.get("type") == "header":
|
||||||
|
return
|
||||||
|
|
||||||
|
flags = envelope_data.get("flags", [])
|
||||||
|
is_read = "Seen" in flags
|
||||||
|
|
||||||
|
if is_read:
|
||||||
|
# Mark as unread
|
||||||
|
result, success = await himalaya_client.mark_as_unread(
|
||||||
|
message_id, folder=folder, account=account
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
if "Seen" in envelope_data.get("flags", []):
|
||||||
|
envelope_data["flags"].remove("Seen")
|
||||||
|
self.show_status(f"Marked message {message_id} as unread")
|
||||||
|
self._update_envelope_read_state(index, is_read=False)
|
||||||
|
else:
|
||||||
|
# Mark as read
|
||||||
|
result, success = await himalaya_client.mark_as_read(
|
||||||
|
message_id, folder=folder, account=account
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
if "flags" not in envelope_data:
|
||||||
|
envelope_data["flags"] = []
|
||||||
|
if "Seen" not in envelope_data["flags"]:
|
||||||
|
envelope_data["flags"].append("Seen")
|
||||||
|
self.show_status(f"Marked message {message_id} as read")
|
||||||
|
self._update_envelope_read_state(index, is_read=True)
|
||||||
|
|
||||||
|
def _update_envelope_read_state(self, index: int, is_read: bool) -> None:
|
||||||
|
"""Update the visual state of an envelope in the list."""
|
||||||
|
try:
|
||||||
|
list_view = self.query_one("#envelopes_list", ListView)
|
||||||
|
list_item = list_view.children[index]
|
||||||
|
envelope_widget = list_item.query_one(EnvelopeListItem)
|
||||||
|
envelope_widget.is_read = is_read
|
||||||
|
if is_read:
|
||||||
|
envelope_widget.remove_class("unread")
|
||||||
|
else:
|
||||||
|
envelope_widget.add_class("unread")
|
||||||
|
except Exception:
|
||||||
|
pass # Widget may not exist
|
||||||
|
|
||||||
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.message_store.get_oldest_id())
|
self.show_message(self.message_store.get_oldest_id())
|
||||||
|
|||||||
@@ -312,6 +312,49 @@ async def mark_as_read(
|
|||||||
return str(e), False
|
return str(e), False
|
||||||
|
|
||||||
|
|
||||||
|
async def mark_as_unread(
|
||||||
|
message_id: int,
|
||||||
|
folder: Optional[str] = None,
|
||||||
|
account: Optional[str] = None,
|
||||||
|
) -> Tuple[Optional[str], bool]:
|
||||||
|
"""
|
||||||
|
Mark a message as unread by removing the 'seen' flag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_id: The ID of the message to mark as unread
|
||||||
|
folder: The folder containing the message
|
||||||
|
account: The account to use
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing:
|
||||||
|
- Result message or error
|
||||||
|
- Success status (True if operation was successful)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cmd = f"himalaya flag remove seen {message_id}"
|
||||||
|
if folder:
|
||||||
|
cmd += f" -f '{folder}'"
|
||||||
|
if account:
|
||||||
|
cmd += f" -a '{account}'"
|
||||||
|
|
||||||
|
process = await asyncio.create_subprocess_shell(
|
||||||
|
cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
return stdout.decode().strip() or "Marked as unread", True
|
||||||
|
else:
|
||||||
|
error_msg = stderr.decode().strip()
|
||||||
|
logging.error(f"Error marking message as unread: {error_msg}")
|
||||||
|
return error_msg or "Unknown error", False
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Exception during marking message as unread: {e}")
|
||||||
|
return str(e), False
|
||||||
|
|
||||||
|
|
||||||
async def search_envelopes(
|
async def search_envelopes(
|
||||||
query: str,
|
query: str,
|
||||||
folder: Optional[str] = None,
|
folder: Optional[str] = None,
|
||||||
|
|||||||
Reference in New Issue
Block a user