Skip to content

Commit ebf4e90

Browse files
ndbroadbentclaude
andcommitted
Fix Tauri 2 permissions + multiple UI/UX improvements
- Add capabilities/default.json with required Tauri 2 permissions - Fix async command handlers (remove async from sync functions) - Add dark mode support via prefers-color-scheme media query - Sort chats by most recent message (not message count) - Remove unused "Save Locally" button and handler - Add default-run to fix cargo binary ambiguity - Add debug logging to list_chats and check_full_disk_access - Fix Taskfile dev:local to include desktop feature 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 9e08105 commit ebf4e90

File tree

8 files changed

+139
-72
lines changed

8 files changed

+139
-72
lines changed

Taskfile.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ tasks:
1515
dev:local:
1616
desc: Start Tauri dev mode (uses localhost:5173 API)
1717
cmds:
18-
- cargo tauri dev --features dev-server
18+
- cargo tauri dev --features desktop,dev-server
1919

2020
build:
2121
desc: Build for production (points to chattomap.com)
2222
cmds:
2323
- cargo tauri build
2424

25-
build:dev:
26-
desc: Build release with dev server (for testing)
25+
build:local:
26+
desc: Build release with localhost API (for testing)
2727
cmds:
28-
- cargo tauri build --features dev-server
28+
- cargo tauri build --features desktop,dev-server
2929

3030
# === Quality Checks (run before marking any task complete) ===
3131
ci:

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ license = "GPL-3.0"
77
repository = "https://github.com/DocSpring/chat_to_map_desktop"
88
edition = "2021"
99
rust-version = "1.70"
10+
default-run = "chat-to-map-desktop"
1011

1112
[build-dependencies]
1213
tauri-build = { version = "2", features = [] }
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "../gen/schemas/desktop-schema.json",
3+
"identifier": "default",
4+
"description": "Default capabilities for ChatToMap Desktop",
5+
"windows": ["main"],
6+
"permissions": [
7+
"core:default",
8+
"core:event:default",
9+
"core:event:allow-listen",
10+
"core:event:allow-emit",
11+
"shell:allow-open"
12+
]
13+
}

src-tauri/src/lib.rs

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,44 @@ pub struct ChatInfo {
3939
pub message_count: usize,
4040
}
4141

42-
/// Get message counts per chat using custom SQL
43-
fn get_message_counts(
42+
/// Chat statistics (message count and last message timestamp)
43+
struct ChatStats {
44+
message_count: usize,
45+
last_message_date: i64,
46+
}
47+
48+
/// Get message counts and last message date per chat using custom SQL
49+
fn get_chat_stats(
4450
db: &rusqlite::Connection,
45-
) -> Result<HashMap<i32, usize>, imessage_database::error::table::TableError> {
46-
let mut counts = HashMap::new();
51+
) -> Result<HashMap<i32, ChatStats>, imessage_database::error::table::TableError> {
52+
let mut stats = HashMap::new();
4753

48-
let mut stmt =
49-
db.prepare("SELECT chat_id, COUNT(*) as count FROM chat_message_join GROUP BY chat_id")?;
54+
let mut stmt = db.prepare(
55+
"SELECT cmj.chat_id, COUNT(*) as count, MAX(m.date) as last_date
56+
FROM chat_message_join cmj
57+
JOIN message m ON cmj.message_id = m.ROWID
58+
GROUP BY cmj.chat_id",
59+
)?;
5060

5161
let rows = stmt.query_map([], |row| {
52-
Ok((row.get::<_, i32>(0)?, row.get::<_, usize>(1)?))
62+
Ok((
63+
row.get::<_, i32>(0)?,
64+
row.get::<_, usize>(1)?,
65+
row.get::<_, i64>(2).unwrap_or(0),
66+
))
5367
})?;
5468

55-
for (chat_id, count) in rows.flatten() {
56-
counts.insert(chat_id, count);
69+
for (chat_id, count, last_date) in rows.flatten() {
70+
stats.insert(
71+
chat_id,
72+
ChatStats {
73+
message_count: count,
74+
last_message_date: last_date,
75+
},
76+
);
5777
}
5878

59-
Ok(counts)
79+
Ok(stats)
6080
}
6181

6282
/// Resolve a display name for a chat, using contacts if available
@@ -96,61 +116,86 @@ pub fn resolve_chat_display_name(
96116

97117
/// List available iMessage chats
98118
pub fn list_chats() -> Result<Vec<ChatInfo>, String> {
119+
eprintln!("[list_chats] Starting...");
120+
99121
// Get database path
100122
let db_path = default_db_path();
123+
eprintln!("[list_chats] DB path: {:?}", db_path);
101124

102125
// Connect to database
103126
let db = get_connection(&db_path).map_err(|e| format!("Failed to connect to database: {e}"))?;
127+
eprintln!("[list_chats] Connected to database");
104128

105129
// Build contacts index for name resolution
130+
eprintln!("[list_chats] Building contacts index...");
106131
let contacts_index = ContactsIndex::build(None).unwrap_or_default();
132+
eprintln!("[list_chats] Contacts index built");
107133

108134
// Cache all chats
135+
eprintln!("[list_chats] Loading chats...");
109136
let chats = Chat::cache(&db).map_err(|e| format!("Failed to load chats: {e}"))?;
137+
eprintln!("[list_chats] Loaded {} chats", chats.len());
110138

111139
// Cache handles (contacts)
140+
eprintln!("[list_chats] Loading handles...");
112141
let handles = Handle::cache(&db).map_err(|e| format!("Failed to load handles: {e}"))?;
113142
let deduped_handles = Handle::dedupe(&handles);
143+
eprintln!("[list_chats] Loaded {} handles", handles.len());
114144

115145
// Build participants map with resolved names
116146
let participants_map = contacts_index.build_participants_map(&handles, &deduped_handles);
117147

118148
// Cache chat participants (chat_id -> set of handle_ids)
149+
eprintln!("[list_chats] Loading chat participants...");
119150
let chat_participants =
120151
ChatToHandle::cache(&db).map_err(|e| format!("Failed to load participants: {e}"))?;
121-
122-
// Get message counts
123-
let message_counts =
124-
get_message_counts(&db).map_err(|e| format!("Failed to get message counts: {e}"))?;
125-
126-
// Build result
127-
let mut result: Vec<ChatInfo> = chats
152+
eprintln!(
153+
"[list_chats] Loaded participants for {} chats",
154+
chat_participants.len()
155+
);
156+
157+
// Get chat stats (message counts and last message dates)
158+
eprintln!("[list_chats] Getting chat stats...");
159+
let chat_stats = get_chat_stats(&db).map_err(|e| format!("Failed to get chat stats: {e}"))?;
160+
eprintln!("[list_chats] Got chat stats");
161+
162+
// Build result with last_message_date for sorting
163+
let mut result: Vec<(ChatInfo, i64)> = chats
128164
.into_iter()
129165
.map(|(id, chat)| {
130166
let participants = chat_participants.get(&id);
131167
let participant_count = participants.map(|p| p.len()).unwrap_or(0);
132-
let message_count = message_counts.get(&id).copied().unwrap_or(0);
168+
let stats = chat_stats.get(&id);
169+
let message_count = stats.map(|s| s.message_count).unwrap_or(0);
170+
let last_message_date = stats.map(|s| s.last_message_date).unwrap_or(0);
133171

134172
let display_name =
135173
resolve_chat_display_name(&chat, participants, &participants_map, &deduped_handles);
136174

137-
ChatInfo {
138-
id,
139-
display_name,
140-
chat_identifier: chat.chat_identifier.clone(),
141-
service: chat
142-
.service_name
143-
.as_deref()
144-
.unwrap_or("Unknown")
145-
.to_string(),
146-
participant_count,
147-
message_count,
148-
}
175+
(
176+
ChatInfo {
177+
id,
178+
display_name,
179+
chat_identifier: chat.chat_identifier.clone(),
180+
service: chat
181+
.service_name
182+
.as_deref()
183+
.unwrap_or("Unknown")
184+
.to_string(),
185+
participant_count,
186+
message_count,
187+
},
188+
last_message_date,
189+
)
149190
})
150191
.collect();
151192

152-
// Sort by message count descending (most active chats first)
153-
result.sort_by(|a, b| b.message_count.cmp(&a.message_count));
193+
// Sort by last message date descending (most recent first)
194+
result.sort_by(|a, b| b.1.cmp(&a.1));
195+
196+
// Extract just the ChatInfo
197+
let result: Vec<ChatInfo> = result.into_iter().map(|(info, _)| info).collect();
154198

199+
eprintln!("[list_chats] Done! Returning {} chats", result.len());
155200
Ok(result)
156201
}

src-tauri/src/main.rs

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@ pub struct ExportResult {
2222

2323
/// List available iMessage chats
2424
#[tauri::command]
25-
async fn list_chats() -> Result<Vec<ChatInfo>, String> {
26-
lib_list_chats()
25+
fn list_chats() -> Result<Vec<ChatInfo>, String> {
26+
eprintln!("[tauri::list_chats] Command invoked");
27+
let result = lib_list_chats();
28+
eprintln!(
29+
"[tauri::list_chats] Result: {:?}",
30+
result.as_ref().map(|v| v.len())
31+
);
32+
result
2733
}
2834

2935
/// Export selected chats and upload to server
@@ -129,19 +135,28 @@ async fn export_and_upload(
129135

130136
/// Check if Full Disk Access is granted (macOS)
131137
#[tauri::command]
132-
async fn check_full_disk_access() -> Result<bool, String> {
138+
fn check_full_disk_access() -> Result<bool, String> {
139+
eprintln!("[check_full_disk_access] Checking...");
133140
#[cfg(target_os = "macos")]
134141
{
135142
// Check if we can actually read the database
136143
let db_path = default_db_path();
144+
eprintln!("[check_full_disk_access] DB path: {:?}", db_path);
137145
if !db_path.exists() {
146+
eprintln!("[check_full_disk_access] DB does not exist");
138147
return Ok(false);
139148
}
140149

141150
// Try to open the database - this will fail without FDA
142151
match get_connection(&db_path) {
143-
Ok(_) => Ok(true),
144-
Err(_) => Ok(false),
152+
Ok(_) => {
153+
eprintln!("[check_full_disk_access] FDA granted (can open DB)");
154+
Ok(true)
155+
}
156+
Err(e) => {
157+
eprintln!("[check_full_disk_access] FDA denied: {:?}", e);
158+
Ok(false)
159+
}
145160
}
146161
}
147162

@@ -153,7 +168,7 @@ async fn check_full_disk_access() -> Result<bool, String> {
153168

154169
/// Open System Preferences to Full Disk Access (macOS)
155170
#[tauri::command]
156-
async fn open_full_disk_access_settings() -> Result<(), String> {
171+
fn open_full_disk_access_settings() -> Result<(), String> {
157172
#[cfg(target_os = "macos")]
158173
{
159174
std::process::Command::new("open")
@@ -164,21 +179,6 @@ async fn open_full_disk_access_settings() -> Result<(), String> {
164179
Ok(())
165180
}
166181

167-
/// Save export locally (fallback when upload fails)
168-
#[tauri::command]
169-
async fn save_export_locally(chat_ids: Vec<i32>, save_path: String) -> Result<String, String> {
170-
let export_result = tokio::task::spawn_blocking(move || export_chats(&chat_ids, None))
171-
.await
172-
.map_err(|e| format!("Export task failed: {e}"))?
173-
.map_err(|e| format!("Export failed: {e}"))?;
174-
175-
// Copy zip to user-specified location
176-
std::fs::copy(&export_result.zip_path, &save_path)
177-
.map_err(|e| format!("Failed to save file: {e}"))?;
178-
179-
Ok(save_path)
180-
}
181-
182182
fn main() {
183183
tauri::Builder::default()
184184
.plugin(tauri_plugin_shell::init())
@@ -187,7 +187,6 @@ fn main() {
187187
export_and_upload,
188188
check_full_disk_access,
189189
open_full_disk_access_settings,
190-
save_export_locally,
191190
])
192191
.run(tauri::generate_context!())
193192
.expect("error while running tauri application");

src/index.html

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,7 @@ <h2>Export Complete!</h2>
8888
<div class="icon">⚠️</div>
8989
<h2>Something went wrong</h2>
9090
<p id="error-message"></p>
91-
<div class="error-actions">
92-
<button id="retry-btn" class="btn btn-primary">Try Again</button>
93-
<button id="save-locally-btn" class="btn btn-secondary">
94-
Save Locally
95-
</button>
96-
</div>
91+
<button id="retry-btn" class="btn btn-primary">Try Again</button>
9792
</div>
9893
</main>
9994

src/main.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ const elements = {
6868
openResultsBtn: getElement<HTMLButtonElement>('open-results-btn'),
6969

7070
errorMessage: getElement<HTMLElement>('error-message'),
71-
retryBtn: getElement<HTMLButtonElement>('retry-btn'),
72-
saveLocallyBtn: getElement<HTMLButtonElement>('save-locally-btn')
71+
retryBtn: getElement<HTMLButtonElement>('retry-btn')
7372
}
7473

7574
// Screen management
@@ -196,12 +195,14 @@ function setupEventListeners(): void {
196195

197196
// Error screen
198197
elements.retryBtn.addEventListener('click', handleExport)
199-
elements.saveLocallyBtn.addEventListener('click', handleSaveLocally)
200198
}
201199

202200
async function checkPermissionAndLoadChats(): Promise<void> {
201+
console.log('[checkPermissionAndLoadChats] Starting...')
203202
try {
203+
console.log('[checkPermissionAndLoadChats] Invoking check_full_disk_access...')
204204
const hasAccess = await invoke<boolean>('check_full_disk_access')
205+
console.log('[checkPermissionAndLoadChats] hasAccess:', hasAccess)
205206

206207
if (!hasAccess) {
207208
showScreen(elements.permissionScreen)
@@ -254,11 +255,6 @@ async function handleExport(): Promise<void> {
254255
}
255256
}
256257

257-
function handleSaveLocally(): void {
258-
// TODO: Implement save to local file
259-
alert('Save locally feature coming soon')
260-
}
261-
262258
function showError(message: string): void {
263259
elements.errorMessage.textContent = message
264260
showScreen(elements.errorScreen)

src/styles.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,28 @@
1010
--color-text: #111827;
1111
--color-text-secondary: #6b7280;
1212
--color-border: #e5e7eb;
13+
--color-selected: #eff6ff;
1314
--radius: 8px;
1415
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
1516
}
1617

18+
@media (prefers-color-scheme: dark) {
19+
:root {
20+
--color-primary: #3b82f6;
21+
--color-primary-hover: #2563eb;
22+
--color-secondary: #9ca3af;
23+
--color-secondary-hover: #6b7280;
24+
--color-success: #34d399;
25+
--color-error: #f87171;
26+
--color-bg: #1f2937;
27+
--color-bg-secondary: #111827;
28+
--color-text: #f9fafb;
29+
--color-text-secondary: #9ca3af;
30+
--color-border: #374151;
31+
--color-selected: #1e3a5f;
32+
}
33+
}
34+
1735
* {
1836
box-sizing: border-box;
1937
margin: 0;

0 commit comments

Comments
 (0)