Skip to content

Commit 697c8e2

Browse files
ndbroadbentclaude
andcommitted
Improve permissions UX and add manual chat.db file selection
Permission screen improvements: - Redesigned with clear explanation of what we access (~/Library/Messages/chat.db) - Shows what we DON'T access (photos, documents, attachments) - Explains why FDA is required ("Apple protects your Messages folder") - Added step-by-step instructions for enabling FDA - Changed icon from warning to lock for less scary appearance Manual file selection fallback: - Added "select a copied chat.db file manually" option for users who: - Refuse to grant FDA but can copy their database - Have Time Machine backups of their Messages folder - Validates selected file is a valid iMessage database - Uses Tauri dialog plugin for native file picker Backend changes: - list_chats() now accepts optional custom_db_path parameter - export_chats() now accepts optional custom_db_path parameter - Added validate_chat_db() function to verify database integrity - CLI updated to use new function signatures Analytics: - Added funnel_selected_custom_db event for tracking fallback usage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent e6a33af commit 697c8e2

File tree

13 files changed

+399
-32
lines changed

13 files changed

+399
-32
lines changed

bun.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"name": "chat-to-map-desktop",
66
"dependencies": {
77
"@tauri-apps/api": "^2.9.1",
8+
"@tauri-apps/plugin-dialog": "^2.4.2",
89
"@tauri-apps/plugin-shell": "^2.3.3",
910
},
1011
"devDependencies": {
@@ -185,6 +186,8 @@
185186

186187
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw=="],
187188

189+
"@tauri-apps/plugin-dialog": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="],
190+
188191
"@tauri-apps/plugin-shell": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],
189192

190193
"@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
},
2626
"dependencies": {
2727
"@tauri-apps/api": "^2.9.1",
28+
"@tauri-apps/plugin-dialog": "^2.4.2",
2829
"@tauri-apps/plugin-shell": "^2.3.3"
2930
}
3031
}

src-tauri/Cargo.lock

Lines changed: 90 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ prettyplease = "0.2"
2020
[dependencies]
2121
tauri = { version = "2", features = [], optional = true }
2222
tauri-plugin-shell = { version = "2", optional = true }
23+
tauri-plugin-dialog = { version = "2", optional = true }
2324
serde = { version = "1", features = ["derive"] }
2425
serde_json = "1"
2526
tokio = { version = "1", features = ["full"] }
@@ -59,7 +60,7 @@ path = "src/cli.rs"
5960

6061
[features]
6162
default = ["desktop"]
62-
desktop = ["tauri", "tauri-plugin-shell", "tauri/custom-protocol"]
63+
desktop = ["tauri", "tauri-plugin-shell", "tauri-plugin-dialog", "tauri/custom-protocol"]
6364
# Use local dev server instead of production
6465
dev-server = []
6566

src-tauri/capabilities/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"core:event:default",
99
"core:event:allow-listen",
1010
"core:event:allow-emit",
11-
"shell:allow-open"
11+
"shell:allow-open",
12+
"dialog:allow-open"
1213
]
1314
}

src-tauri/src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ fn main() {
7575
}
7676

7777
fn cmd_list_chats(verbose: bool, limit: Option<usize>, filter: Option<String>, json: bool) {
78-
match chat_to_map_desktop::list_chats() {
78+
match chat_to_map_desktop::list_chats(None) {
7979
Ok(mut chats) => {
8080
// Apply filter if provided
8181
if let Some(ref filter_str) = filter {

src-tauri/src/export.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ const TIMESTAMP_FACTOR: i64 = 1_000_000_000;
114114
pub fn export_chats(
115115
chat_ids: &[i32],
116116
progress_callback: Option<ProgressCallback>,
117+
custom_db_path: Option<&std::path::Path>,
117118
) -> Result<ExportResult, String> {
118119
let emit_progress = |progress: ExportProgress| {
119120
if let Some(ref cb) = progress_callback {
@@ -128,7 +129,9 @@ pub fn export_chats(
128129
});
129130

130131
// Connect to database
131-
let db_path = default_db_path();
132+
let db_path = custom_db_path
133+
.map(|p| p.to_path_buf())
134+
.unwrap_or_else(default_db_path);
132135
let db = get_connection(&db_path).map_err(|e| format!("Failed to connect to database: {e}"))?;
133136

134137
// Build contacts index for name resolution

src-tauri/src/lib.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,14 @@ pub fn resolve_chat_display_name(
117117
}
118118

119119
/// List available iMessage chats
120-
pub fn list_chats() -> Result<Vec<ChatInfo>, String> {
120+
/// If custom_db_path is provided, uses that instead of the default ~/Library/Messages/chat.db
121+
pub fn list_chats(custom_db_path: Option<&std::path::Path>) -> Result<Vec<ChatInfo>, String> {
121122
eprintln!("[list_chats] Starting...");
122123

123124
// Get database path
124-
let db_path = default_db_path();
125+
let db_path = custom_db_path
126+
.map(|p| p.to_path_buf())
127+
.unwrap_or_else(default_db_path);
125128
eprintln!("[list_chats] DB path: {:?}", db_path);
126129

127130
// Connect to database
@@ -201,3 +204,43 @@ pub fn list_chats() -> Result<Vec<ChatInfo>, String> {
201204
eprintln!("[list_chats] Done! Returning {} chats", result.len());
202205
Ok(result)
203206
}
207+
208+
/// Validate that a file is a valid iMessage chat.db database
209+
/// Returns true if it can be opened and contains the expected tables
210+
pub fn validate_chat_db(path: &std::path::Path) -> bool {
211+
eprintln!("[validate_chat_db] Validating: {:?}", path);
212+
213+
// Check file exists
214+
if !path.exists() {
215+
eprintln!("[validate_chat_db] File does not exist");
216+
return false;
217+
}
218+
219+
// Try to open as SQLite database
220+
let db = match get_connection(path) {
221+
Ok(db) => db,
222+
Err(e) => {
223+
eprintln!("[validate_chat_db] Failed to open: {e}");
224+
return false;
225+
}
226+
};
227+
228+
// Check for expected iMessage tables (chat, message, handle)
229+
let result: Result<i64, _> = db.query_row(
230+
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('chat', 'message', 'handle')",
231+
[],
232+
|row| row.get(0),
233+
);
234+
235+
match result {
236+
Ok(count) => {
237+
let valid = count >= 3;
238+
eprintln!("[validate_chat_db] Found {count} expected tables, valid={valid}");
239+
valid
240+
}
241+
Err(e) => {
242+
eprintln!("[validate_chat_db] Query failed: {e}");
243+
false
244+
}
245+
}
246+
}

src-tauri/src/main.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use chat_to_map_desktop::{
99
list_chats as lib_list_chats,
1010
screenshot::{capture_window, ScreenshotConfig},
1111
upload::{complete_upload, get_presigned_url, get_results_url, upload_file},
12-
ChatInfo,
12+
validate_chat_db as lib_validate_chat_db, ChatInfo,
1313
};
1414
use clap::Parser;
1515
use imessage_database::{tables::table::get_connection, util::dirs::default_db_path};
@@ -55,20 +55,32 @@ pub struct ExportResult {
5555

5656
/// List available iMessage chats
5757
#[tauri::command]
58-
fn list_chats() -> Result<Vec<ChatInfo>, String> {
59-
eprintln!("[tauri::list_chats] Command invoked");
60-
let result = lib_list_chats();
58+
fn list_chats(custom_db_path: Option<String>) -> Result<Vec<ChatInfo>, String> {
59+
eprintln!(
60+
"[tauri::list_chats] Command invoked, custom_db_path: {:?}",
61+
custom_db_path
62+
);
63+
let path = custom_db_path.as_ref().map(PathBuf::from);
64+
let result = lib_list_chats(path.as_deref());
6165
eprintln!(
6266
"[tauri::list_chats] Result: {:?}",
6367
result.as_ref().map(|v| v.len())
6468
);
6569
result
6670
}
6771

72+
/// Validate that a file is a valid iMessage chat.db database
73+
#[tauri::command]
74+
fn validate_chat_db(path: String) -> bool {
75+
eprintln!("[tauri::validate_chat_db] Validating: {}", path);
76+
lib_validate_chat_db(&PathBuf::from(path))
77+
}
78+
6879
/// Export selected chats and upload to server
6980
#[tauri::command]
7081
async fn export_and_upload(
7182
chat_ids: Vec<i32>,
83+
custom_db_path: Option<String>,
7284
window: tauri::Window,
7385
) -> Result<ExportResult, String> {
7486
// Helper to emit progress
@@ -100,11 +112,13 @@ async fn export_and_upload(
100112
);
101113
});
102114

103-
let export_result =
104-
tokio::task::spawn_blocking(move || export_chats(&chat_ids, Some(progress_callback)))
105-
.await
106-
.map_err(|e| format!("Export task failed: {e}"))?
107-
.map_err(|e| format!("Export failed: {e}"))?;
115+
let db_path = custom_db_path.map(PathBuf::from);
116+
let export_result = tokio::task::spawn_blocking(move || {
117+
export_chats(&chat_ids, Some(progress_callback), db_path.as_deref())
118+
})
119+
.await
120+
.map_err(|e| format!("Export task failed: {e}"))?
121+
.map_err(|e| format!("Export failed: {e}"))?;
108122

109123
// Stage 2: Get pre-signed URL (50-55%)
110124
emit("Uploading", 50, "Preparing upload...");
@@ -329,8 +343,10 @@ fn main() {
329343
}
330344
}
331345
})
346+
.plugin(tauri_plugin_dialog::init())
332347
.invoke_handler(tauri::generate_handler![
333348
list_chats,
349+
validate_chat_db,
334350
export_and_upload,
335351
check_full_disk_access,
336352
open_full_disk_access_settings,

src/analytics.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ export const FunnelEvents = {
102102
/** User retried permission check */
103103
retriedPermission: () => trackEvent('funnel_retried_permission'),
104104

105+
/** User selected a custom chat.db file (fallback path) */
106+
selectedCustomDb: () => trackEvent('funnel_selected_custom_db'),
107+
105108
/** Chats loaded successfully */
106109
chatsLoaded: (count: number) => trackEvent('funnel_chats_loaded', { chat_count: count }),
107110

0 commit comments

Comments
 (0)