Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3c8dc45
feat: add persistent conversation feature with cross-platform file re…
rodrigogs Mar 31, 2026
cd4fbfc
fix: resolve all 15 type errors in persistent conversations feature
rodrigogs Mar 31, 2026
9820f06
fix(electron): harden file:readFromPath IPC handler
rodrigogs Mar 31, 2026
d295bb1
fix(i18n): replace hardcoded English strings with Paraglide messages
rodrigogs Mar 31, 2026
33d1254
fix(persistence): clean up IndexedDB entries when removing a remember…
rodrigogs Mar 31, 2026
579c1e5
fix(persistence): add error logging to silent catch blocks
rodrigogs Mar 31, 2026
81c0fa6
fix(ReselectFileModal): eliminate drag-drop flicker with counter pattern
rodrigogs Mar 31, 2026
2b54c7d
chore: remove AI handoff document
rodrigogs Mar 31, 2026
6a65b7e
fix: clean up dead code, timer leaks, and unused imports
rodrigogs Mar 31, 2026
ce85901
fix: resolve critical bugs and extract duplicated parse/index logic
rodrigogs Mar 31, 2026
746b5e8
fix: address final review items
rodrigogs Mar 31, 2026
fde6c24
fix: translate all persistence keys and remove dead isRestoring state
rodrigogs Apr 1, 2026
d2d7397
refactor: deduplicate getBookmarksForChatAsExport
rodrigogs Apr 1, 2026
31bb85d
fix(electron): use file descriptor to prevent TOCTOU race in file read
rodrigogs Apr 1, 2026
e262e17
refactor(Toast): replace inline SVGs with Icon component
rodrigogs Apr 1, 2026
5ce83a3
fix(a11y): add aria-label to drop zone and i18n Modal close label
rodrigogs Apr 1, 2026
ff46430
refactor: address all 13 review items
rodrigogs Apr 1, 2026
4a58b4c
fix: address all 9 Copilot review comments
rodrigogs Apr 1, 2026
42777c7
fix: address 3 new Copilot review comments
rodrigogs Apr 1, 2026
434aff5
fix: capture FileSystemFileHandle during drag-drop for seamless persi…
rodrigogs Apr 1, 2026
a0d79c4
fix: capture file handles synchronously during drop and fix closure bug
rodrigogs Apr 1, 2026
d8305bd
fix: pass Electron file path from dialog to persistence layer
rodrigogs Apr 1, 2026
174f306
debug: add persistence flow logging to trace Electron path issue
rodrigogs Apr 1, 2026
fe5e1f0
fix: address 4 Copilot review comments
rodrigogs Apr 1, 2026
4533bf7
fix: address 3 Copilot review comments
rodrigogs Apr 1, 2026
4fba7e9
fix: ReselectFileModal uses Electron dialog to capture file path
rodrigogs Apr 1, 2026
d626990
fix: move Remember Conversation above Transcription Language in conte…
rodrigogs Apr 1, 2026
04d7456
fix: ReselectFileModal captures FileSystemFileHandle on Chrome and up…
rodrigogs Apr 1, 2026
a15e4f2
fix: removing chat from list no longer deletes persisted data
rodrigogs Apr 1, 2026
c9741b3
chore: remove debug logging from persistence flow
rodrigogs Apr 1, 2026
a744b92
fix: sidebar import uses Electron dialog and captures file path
rodrigogs Apr 1, 2026
5affe1e
refactor: address all review items - dead code, DRY, a11y, error feed…
rodrigogs Apr 1, 2026
a2a910a
refactor: centralize modals, fix cross-chat ID collisions, address re…
rodrigogs Apr 1, 2026
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
57 changes: 56 additions & 1 deletion electron/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ ipcMain.handle('dialog:openFile', async () => {
return {
path: filePath,
name: fileName,
buffer: fileContent.buffer,
// Slice the buffer to avoid sending the entire Node.js Buffer pool
buffer: fileContent.buffer.slice(
fileContent.byteOffset,
fileContent.byteOffset + fileContent.byteLength,
),
};
});

Expand Down Expand Up @@ -227,6 +231,57 @@ ipcMain.handle('fs:fileExists', async (_event, filePath) => {
return fs.existsSync(filePath);
});

// Read file from absolute path (for persistence)
ipcMain.handle('file:readFromPath', async (_event, filePath) => {
try {
if (typeof filePath !== 'string') {
return { success: false, error: 'Invalid file path' };
}
const normalized = path.resolve(filePath);
// Require absolute path and reject path traversal segments
if (!path.isAbsolute(filePath)) {
return { success: false, error: 'Invalid file path' };
}
if (filePath.split(/[/\\]+/).includes('..')) {
return { success: false, error: 'Invalid file path' };
}
if (path.extname(normalized).toLowerCase() !== '.zip') {
return { success: false, error: 'Only .zip files are allowed' };
}

// Pre-check with lstat to reject symlinks (works cross-platform including Windows)
const lst = await fs.promises.lstat(normalized);
if (!lst.isFile() || lst.isSymbolicLink()) {
return { success: false, error: 'Path is not a regular file' };
}

// Use O_NOFOLLOW when available to reject symlinks atomically (not supported on Windows)
const openFlags =
typeof fs.constants.O_NOFOLLOW === 'number'
? fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW
: fs.constants.O_RDONLY;
const fd = await fs.promises.open(normalized, openFlags);
try {
const content = await fd.readFile();
return {
success: true,
buffer: content.buffer.slice(
content.byteOffset,
content.byteOffset + content.byteLength,
),
name: path.basename(normalized),
};
} finally {
await fd.close();
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
Comment thread
rodrigogs marked this conversation as resolved.
});

ipcMain.handle('shell:openExternal', async (_event, url) => {
// Validate URL before opening
if (!url.startsWith('https://github.com/rodrigogs/whats-reader')) {
Expand Down
4 changes: 4 additions & 0 deletions electron/preload.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
readDir: (dirPath) => ipcRenderer.invoke('fs:readDir', dirPath),
fileExists: (filePath) => ipcRenderer.invoke('fs:fileExists', filePath),

// Persistence file operations
readFileFromPath: (filePath) =>
ipcRenderer.invoke('file:readFromPath', filePath),

// External links
openExternal: (url) => ipcRenderer.invoke('shell:openExternal', url),

Expand Down
26 changes: 24 additions & 2 deletions messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
"update_never_tooltip": "Nie Update-Benachrichtigungen anzeigen",
"version_view_releases": "Versionen auf GitHub anzeigen",
"close": "Schließen",
"close_modal": "Modal schließen",
"update_ready": "Update bereit",
"update_downloading": "Update wird heruntergeladen",
"update_close": "Aktualisierungsbenachrichtigung schließen",
Expand Down Expand Up @@ -241,5 +242,26 @@
"media_gallery_type_other": "Andere",
"media_gallery_clear_filter": "Filter löschen",
"media_gallery_participant_search_placeholder": "Suche nach Name oder Nummer...",
"media_gallery_participant_no_match": "Kein Teilnehmer entspricht \"{query}\""
}
"persistence_remember_conversation": "Unterhaltung Merken",
"persistence_conversation_saved": "Die Unterhaltung wird gespeichert",
"persistence_conversation_removed": "Unterhaltung aus gespeicherten Sitzungen entfernt",
"persistence_restore_title": "Gespeicherte Unterhaltungen Wiederherstellen",
"persistence_restore_description": "Sie haben gespeicherte Unterhaltungen aus früheren Sitzungen:",
"persistence_restore_button": "Ausgewählte Wiederherstellen ({count})",
"persistence_start_fresh": "Neu Beginnen",
"persistence_dont_show_again": "Nicht mehr anzeigen",
"persistence_last_opened": "Zuletzt geöffnet: {date}",
"persistence_message_count": "{count} Nachrichten",
"persistence_reselect_title": "Bitte Datei Erneut Auswählen",
"persistence_reselect_description": "Um \"{chatTitle}\" wiederherzustellen, wählen Sie bitte die Originaldatei aus:",
"persistence_skip": "Überspringen",
"persistence_select_all": "Alle auswählen",
"persistence_deselect_all": "Alle abwählen",
"persistence_drop_zip": "WhatsApp-ZIP-Datei hier ablegen",
"persistence_or_browse": "oder klicken Sie unten zum Durchsuchen",
"persistence_browse_files": "Dateien durchsuchen",
"persistence_save_failed": "Fehler beim Speichern der Unterhaltung",
"persistence_restore_failed": "Fehler beim Wiederherstellen der Unterhaltung",
"persistence_remove_failed": "Fehler beim Entfernen der Unterhaltung",
"close_notification": "Benachrichtigung schließen"
}
25 changes: 24 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@
"calendar_weekday_fri": "Fr",
"calendar_weekday_sat": "Sa",
"close": "Close",
"close_modal": "Close modal",
"media_gallery_options": "Options",
"media_gallery_filter_by_participant": "Filter by participant",
"media_gallery_filter_by_type": "Filter by type",
Expand All @@ -241,5 +242,27 @@
"media_gallery_type_other": "Other",
"media_gallery_clear_filter": "Clear filter",
"media_gallery_participant_search_placeholder": "Search by name or number...",
"media_gallery_participant_no_match": "No participant matches \"{query}\""
"media_gallery_participant_no_match": "No participant matches \"{query}\"",
"persistence_remember_conversation": "Remember Conversation",
"persistence_conversation_saved": "Conversation will be remembered",
"persistence_conversation_removed": "Conversation removed from saved sessions",
"persistence_restore_title": "Restore Saved Conversations",
"persistence_restore_description": "You have saved conversations from previous sessions:",
"persistence_restore_button": "Restore Selected ({count})",
"persistence_start_fresh": "Start Fresh",
"persistence_dont_show_again": "Don't show this again",
"persistence_last_opened": "Last opened: {date}",
"persistence_message_count": "{count} messages",
"persistence_reselect_title": "Please Re-select File",
"persistence_reselect_description": "To restore \"{chatTitle}\", please select the original file:",
"persistence_skip": "Skip",
"persistence_select_all": "Select All",
"persistence_deselect_all": "Deselect All",
"persistence_drop_zip": "Drop WhatsApp ZIP file here",
"persistence_or_browse": "or click below to browse",
"persistence_browse_files": "Browse Files",
"persistence_save_failed": "Failed to save conversation",
"persistence_restore_failed": "Failed to restore conversation",
"persistence_remove_failed": "Failed to remove conversation",
"close_notification": "Close notification"
}
26 changes: 24 additions & 2 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
"update_never_tooltip": "Nunca mostrar notificaciones de actualización",
"version_view_releases": "Ver versiones en GitHub",
"close": "Cerrar",
"close_modal": "Cerrar modal",
"update_ready": "Actualización lista",
"update_downloading": "Descargando actualización",
"update_close": "Cerrar notificación de actualización",
Expand Down Expand Up @@ -241,5 +242,26 @@
"media_gallery_type_other": "Otro",
"media_gallery_clear_filter": "Limpiar filtro",
"media_gallery_participant_search_placeholder": "Buscar por nombre o número...",
"media_gallery_participant_no_match": "Ningún participante coincide con \"{query}\""
}
"persistence_remember_conversation": "Recordar Conversación",
"persistence_conversation_saved": "La conversación será recordada",
"persistence_conversation_removed": "Conversación eliminada de las sesiones guardadas",
"persistence_restore_title": "Restaurar Conversaciones Guardadas",
"persistence_restore_description": "Tienes conversaciones guardadas de sesiones anteriores:",
"persistence_restore_button": "Restaurar Seleccionadas ({count})",
"persistence_start_fresh": "Empezar de Nuevo",
"persistence_dont_show_again": "No mostrar de nuevo",
"persistence_last_opened": "Última apertura: {date}",
"persistence_message_count": "{count} mensajes",
"persistence_reselect_title": "Por Favor, Selecciona el Archivo de Nuevo",
"persistence_reselect_description": "Para restaurar \"{chatTitle}\", selecciona el archivo original:",
"persistence_skip": "Omitir",
"persistence_select_all": "Seleccionar Todo",
"persistence_deselect_all": "Deseleccionar Todo",
"persistence_drop_zip": "Suelta el archivo ZIP de WhatsApp aquí",
"persistence_or_browse": "o haz clic abajo para buscar",
"persistence_browse_files": "Buscar Archivos",
"persistence_save_failed": "Error al guardar la conversación",
"persistence_restore_failed": "Error al restaurar la conversación",
"persistence_remove_failed": "Error al eliminar la conversación",
"close_notification": "Cerrar notificación"
}
26 changes: 24 additions & 2 deletions messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
"update_never_tooltip": "Ne jamais afficher les notifications de mise à jour",
"version_view_releases": "Voir les versions sur GitHub",
"close": "Fermer",
"close_modal": "Fermer la boîte de dialogue",
"update_ready": "Prêt pour la mise à jour",
"update_downloading": "Téléchargement de la mise à jour",
"update_close": "Fermer la notification de mise à jour",
Expand Down Expand Up @@ -241,5 +242,26 @@
"media_gallery_type_other": "Autre",
"media_gallery_clear_filter": "Effacer le filtre",
"media_gallery_participant_search_placeholder": "Recherche par nom ou numéro...",
"media_gallery_participant_no_match": "Aucun participant ne correspond à \"{query}\""
}
"persistence_remember_conversation": "Mémoriser la Conversation",
"persistence_conversation_saved": "La conversation sera mémorisée",
"persistence_conversation_removed": "Conversation supprimée des sessions enregistrées",
"persistence_restore_title": "Restaurer les Conversations Sauvegardées",
"persistence_restore_description": "Vous avez des conversations sauvegardées de sessions précédentes :",
"persistence_restore_button": "Restaurer la Sélection ({count})",
"persistence_start_fresh": "Repartir à Zéro",
"persistence_dont_show_again": "Ne plus afficher",
"persistence_last_opened": "Dernière ouverture : {date}",
"persistence_message_count": "{count} messages",
"persistence_reselect_title": "Veuillez Resélectionner le Fichier",
"persistence_reselect_description": "Pour restaurer \"{chatTitle}\", veuillez sélectionner le fichier original :",
"persistence_skip": "Ignorer",
"persistence_select_all": "Tout sélectionner",
"persistence_deselect_all": "Tout désélectionner",
"persistence_drop_zip": "Déposez le fichier ZIP WhatsApp ici",
"persistence_or_browse": "ou cliquez ci-dessous pour parcourir",
"persistence_browse_files": "Parcourir les fichiers",
"persistence_save_failed": "Échec de l'enregistrement de la conversation",
"persistence_restore_failed": "Échec de la restauration de la conversation",
"persistence_remove_failed": "Échec de la suppression de la conversation",
"close_notification": "Fermer la notification"
}
26 changes: 24 additions & 2 deletions messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
"update_never_tooltip": "Non mostrare mai notifiche di aggiornamento",
"version_view_releases": "Visualizza le versioni su GitHub",
"close": "Chiudi",
"close_modal": "Chiudi finestra modale",
"update_ready": "Aggiornamento pronto",
"update_downloading": "Download dell'aggiornamento",
"update_close": "Chiudi notifica di aggiornamento",
Expand Down Expand Up @@ -241,5 +242,26 @@
"media_gallery_type_other": "Altro",
"media_gallery_clear_filter": "Cancella filtro",
"media_gallery_participant_search_placeholder": "Cerca per nome o numero...",
"media_gallery_participant_no_match": "Nessun partecipante corrisponde a \"{query}\""
}
"persistence_remember_conversation": "Ricorda Conversazione",
"persistence_conversation_saved": "La conversazione verrà ricordata",
"persistence_conversation_removed": "Conversazione rimossa dalle sessioni salvate",
"persistence_restore_title": "Ripristina Conversazioni Salvate",
"persistence_restore_description": "Hai conversazioni salvate da sessioni precedenti:",
"persistence_restore_button": "Ripristina Selezionate ({count})",
"persistence_start_fresh": "Ricomincia da Zero",
"persistence_dont_show_again": "Non mostrare più",
"persistence_last_opened": "Ultima apertura: {date}",
"persistence_message_count": "{count} messaggi",
"persistence_reselect_title": "Seleziona Nuovamente il File",
"persistence_reselect_description": "Per ripristinare \"{chatTitle}\", seleziona il file originale:",
"persistence_skip": "Salta",
"persistence_select_all": "Seleziona tutto",
"persistence_deselect_all": "Deseleziona tutto",
"persistence_drop_zip": "Trascina qui il file ZIP di WhatsApp",
"persistence_or_browse": "o clicca sotto per sfogliare",
"persistence_browse_files": "Sfoglia file",
"persistence_save_failed": "Impossibile salvare la conversazione",
"persistence_restore_failed": "Impossibile ripristinare la conversazione",
"persistence_remove_failed": "Impossibile rimuovere la conversazione",
"close_notification": "Chiudi notifica"
}
26 changes: 24 additions & 2 deletions messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
"update_never_tooltip": "アップデート通知を表示しない",
"version_view_releases": "GitHubでリリースを見る",
"close": "閉じる",
"close_modal": "モーダルを閉じる",
"update_ready": "アップデート準備完了",
"update_downloading": "アップデートをダウンロード中",
"update_close": "更新通知を閉じる",
Expand Down Expand Up @@ -241,5 +242,26 @@
"media_gallery_type_other": "他の",
"media_gallery_clear_filter": "フィルターをクリア",
"media_gallery_participant_search_placeholder": "名前または番号で検索...",
"media_gallery_participant_no_match": "「{query}」に一致する参加者はいません"
}
"persistence_remember_conversation": "会話を記憶",
"persistence_conversation_saved": "会話が記憶されます",
"persistence_conversation_removed": "保存済みセッションから会話が削除されました",
"persistence_restore_title": "保存済みの会話を復元",
"persistence_restore_description": "以前のセッションから保存された会話があります:",
"persistence_restore_button": "選択した会話を復元 ({count})",
"persistence_start_fresh": "新しく始める",
"persistence_dont_show_again": "今後表示しない",
"persistence_last_opened": "最終オープン: {date}",
"persistence_message_count": "{count} 件のメッセージ",
"persistence_reselect_title": "ファイルを再選択してください",
"persistence_reselect_description": "\"{chatTitle}\" を復元するには、元のファイルを選択してください:",
"persistence_skip": "スキップ",
"persistence_select_all": "すべて選択",
"persistence_deselect_all": "すべて解除",
"persistence_drop_zip": "WhatsAppのZIPファイルをここにドロップ",
"persistence_or_browse": "または下をクリックして参照",
"persistence_browse_files": "ファイルを参照",
"persistence_save_failed": "会話の保存に失敗しました",
"persistence_restore_failed": "会話の復元に失敗しました",
"persistence_remove_failed": "会話の削除に失敗しました",
"close_notification": "通知を閉じる"
}
26 changes: 24 additions & 2 deletions messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
"update_never_tooltip": "Nooit updatemeldingen tonen",
"version_view_releases": "Bekijk releases op GitHub",
"close": "Sluiten",
"close_modal": "Venster sluiten",
"update_ready": "Update gereed",
"update_downloading": "Update downloaden",
"update_close": "Sluit de updatemelding",
Expand Down Expand Up @@ -241,5 +242,26 @@
"media_gallery_type_other": "Ander",
"media_gallery_clear_filter": "Filter wissen",
"media_gallery_participant_search_placeholder": "Zoeken op naam of nummer...",
"media_gallery_participant_no_match": "Geen deelnemer komt overeen met \"{query}\""
}
"persistence_remember_conversation": "Gesprek Onthouden",
"persistence_conversation_saved": "Het gesprek wordt onthouden",
"persistence_conversation_removed": "Gesprek verwijderd uit opgeslagen sessies",
"persistence_restore_title": "Opgeslagen Gesprekken Herstellen",
"persistence_restore_description": "U heeft opgeslagen gesprekken van eerdere sessies:",
"persistence_restore_button": "Geselecteerde Herstellen ({count})",
"persistence_start_fresh": "Opnieuw Beginnen",
"persistence_dont_show_again": "Niet meer tonen",
"persistence_last_opened": "Laatst geopend: {date}",
"persistence_message_count": "{count} berichten",
"persistence_reselect_title": "Selecteer het Bestand Opnieuw",
"persistence_reselect_description": "Om \"{chatTitle}\" te herstellen, selecteer het originele bestand:",
"persistence_skip": "Overslaan",
"persistence_select_all": "Alles selecteren",
"persistence_deselect_all": "Alles deselecteren",
"persistence_drop_zip": "WhatsApp ZIP-bestand hier neerzetten",
"persistence_or_browse": "of klik hieronder om te bladeren",
"persistence_browse_files": "Bestanden bladeren",
"persistence_save_failed": "Gesprek opslaan mislukt",
"persistence_restore_failed": "Gesprek herstellen mislukt",
"persistence_remove_failed": "Gesprek verwijderen mislukt",
"close_notification": "Melding sluiten"
}
26 changes: 24 additions & 2 deletions messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
"update_never_tooltip": "Nunca mostrar notificações de atualização",
"version_view_releases": "Ver versões no GitHub",
"close": "Fechar",
"close_modal": "Fechar modal",
"update_ready": "Atualização pronta",
"update_downloading": "Baixando atualização",
"update_close": "Fechar notificação de atualização",
Expand Down Expand Up @@ -241,5 +242,26 @@
"media_gallery_type_other": "Outro",
"media_gallery_clear_filter": "Limpar filtro",
"media_gallery_participant_search_placeholder": "Pesquisar por nome ou número...",
"media_gallery_participant_no_match": "Nenhum participante corresponde a \"{query}\""
}
"persistence_remember_conversation": "Lembrar Conversa",
"persistence_conversation_saved": "A conversa será lembrada",
"persistence_conversation_removed": "Conversa removida das sessões salvas",
"persistence_restore_title": "Restaurar Conversas Salvas",
"persistence_restore_description": "Você tem conversas salvas de sessões anteriores:",
"persistence_restore_button": "Restaurar Selecionadas ({count})",
"persistence_start_fresh": "Começar do Zero",
"persistence_dont_show_again": "Não mostrar novamente",
"persistence_last_opened": "Última abertura: {date}",
"persistence_message_count": "{count} mensagens",
"persistence_reselect_title": "Por Favor, Selecione o Arquivo Novamente",
"persistence_reselect_description": "Para restaurar \"{chatTitle}\", selecione o arquivo original:",
"persistence_skip": "Pular",
"persistence_select_all": "Selecionar Tudo",
"persistence_deselect_all": "Desmarcar Tudo",
"persistence_drop_zip": "Solte o arquivo ZIP do WhatsApp aqui",
"persistence_or_browse": "ou clique abaixo para procurar",
"persistence_browse_files": "Procurar Arquivos",
"persistence_save_failed": "Falha ao salvar conversa",
"persistence_restore_failed": "Falha ao restaurar conversa",
"persistence_remove_failed": "Falha ao remover conversa",
"close_notification": "Fechar notificação"
}
Loading