Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion js/app/packages/app/component/Soup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
ShortcutLabel,
} from './Soup/components/FilterButton';
import { SortDropdown } from './Soup/components/SortDropdown';
import { isMobile } from '@core/mobile/isMobile';

false && fileFolderDrop;

Expand Down Expand Up @@ -778,7 +779,7 @@ export function Soup() {
/>
</Show>
</div>
<Show when={ENABLE_UNIFIED_LIST_AI_INPUT}>
<Show when={ENABLE_UNIFIED_LIST_AI_INPUT && !isMobile()}>
<SoupChatInput />
</Show>
</div>
Expand Down
13 changes: 0 additions & 13 deletions js/app/tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion js/app/tauri/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,4 @@ tauri-plugin-virtual-keyboard = { git = "https://github.com/voxelbee/tauri-plugi
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }

[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
tauri-plugin-app-events = "0.2"
tauri-plugin-haptics = "2"
3 changes: 1 addition & 2 deletions js/app/tauri/src-tauri/capabilities/mobile.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"haptics:allow-notification-feedback",
"haptics:allow-selection-feedback",
"haptics:allow-vibrate",
"log:default",
"app-events:default"
"log:default"
]
}
26 changes: 26 additions & 0 deletions js/app/tauri/src-tauri/gen/apple/Sources/app/main.mm
Original file line number Diff line number Diff line change
@@ -1,4 +1,30 @@
#include "bindings/bindings.h"
#import <UIKit/UIKit.h>

extern "C" void on_app_resumed(void);

@interface AppLifecycleObserver : NSObject
@end

@implementation AppLifecycleObserver

+ (void)load {
__block BOOL isFirstActivation = YES;

[[NSNotificationCenter defaultCenter]
addObserverForName:UIApplicationDidBecomeActiveNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
if (isFirstActivation) {
isFirstActivation = NO;
return;
}
on_app_resumed();
}];
}

@end

int main(int argc, char * argv[]) {
ffi::start_app();
Expand Down
98 changes: 54 additions & 44 deletions js/app/tauri/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use reqwest::header::COOKIE;
use rootcause::{Report, report};
use serde::Serialize;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
#[cfg(target_os = "ios")]
use std::sync::OnceLock;
use tauri::http::{HeaderMap, HeaderValue};
use tauri::{AppHandle, Emitter};
use tauri::{Manager, Runtime};
Expand All @@ -29,6 +31,56 @@ fn heartbeat_response(state: tauri::State<'_, HeartbeatState>) {
#[cfg(debug_assertions)] // do not remove this
mod debug;

#[cfg(target_os = "ios")]
static GLOBAL_APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();

/// Send a heartbeat ping to the JS layer and check for a response after 1 second.
/// If no response is received, reload the webview (the content process is likely dead).
#[cfg(target_os = "ios")]
fn send_heartbeat(handle: &AppHandle) {
tracing::info!("app resumed, sending heartbeat ping");

let state = handle.state::<HeartbeatState>();
let current_gen = state.generation.fetch_add(1, Ordering::SeqCst) + 1;
state.alive.store(false, Ordering::SeqCst);

let _ = handle.emit("heartbeat_ping", ());

let handle = handle.clone();
tauri::async_runtime::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let state = handle.state::<HeartbeatState>();

if state.generation.load(Ordering::SeqCst) != current_gen {
tracing::debug!("heartbeat: stale generation {current_gen}, skipping");
return;
}

if !state.alive.load(Ordering::SeqCst) {
tracing::warn!(
"heartbeat: no response from JS — content process likely dead, reloading webview"
);
if let Some(webview) = handle.webview_windows().values().next() {
let _ = webview.reload();
}
} else {
tracing::info!("heartbeat: JS responded, content process alive");
}
});
}

/// Called from native Objective-C when the iOS app resumes from background.
/// See `main.mm` for the notification observer.
#[cfg(target_os = "ios")]
#[unsafe(no_mangle)]
extern "C" fn on_app_resumed() {
let Some(handle) = GLOBAL_APP_HANDLE.get() else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storing the app handle in a oncelock is kinda weird. We should probably follow the recommended paradigm for calling from swift -> rust, because I think most plugins do not do this.

If this works though I guess you could merge it and I could look at this as a follow up

tracing::warn!("on_app_resumed: app handle not yet initialized");
return;
};
send_heartbeat(handle);
}

/// domains which the tauri webview can render.
/// This should be as restrictive as possible.
/// If the webview attempts to naviate to other domains,
Expand Down Expand Up @@ -145,51 +197,9 @@ pub fn run() {

app.chain(attach_deep_link_handler);

#[cfg(mobile)]
#[cfg(target_os = "ios")]
{
use tauri::ipc::Channel;
use tauri_plugin_app_events::AppEventsExt;

let app_handle = app.handle().clone();
app_handle.plugin(tauri_plugin_app_events::init())?;

let handle = app_handle.clone();
app_handle
.app_events()
.set_resume_handler(Channel::new(move |_| {
tracing::info!("app resumed, sending heartbeat ping");

let state = handle.state::<HeartbeatState>();
let current_gen = state.generation.fetch_add(1, Ordering::SeqCst) + 1;
state.alive.store(false, Ordering::SeqCst);

let _ = handle.emit("heartbeat_ping", ());

let handle = handle.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let state = handle.state::<HeartbeatState>();

// A newer resume has superseded this one; bail out
if state.generation.load(Ordering::SeqCst) != current_gen {
tracing::debug!("heartbeat: stale generation {current_gen}, skipping");
return;
}

if !state.alive.load(Ordering::SeqCst) {
tracing::warn!(
"heartbeat: no response from JS — content process likely dead, reloading webview"
);
if let Some(webview) = handle.webview_windows().values().next() {
let _ = webview.reload();
}
} else {
tracing::info!("heartbeat: JS responded, content process alive");
}
});

Ok(())
}))?;
let _ = GLOBAL_APP_HANDLE.set(app.handle().clone());
}

Ok(())
Expand Down
Loading