fix(ios): 🐛 fix ios keyboard pop-up problem

This commit is contained in:
Dawn
2025-10-25 02:03:02 +08:00
parent c1ce99e8d1
commit ab933e37ff
16 changed files with 760 additions and 114 deletions
+127 -79
View File
@@ -201,11 +201,11 @@ dependencies = [
"clipboard-win",
"image 0.25.6",
"log",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.1",
"objc2-foundation 0.3.2",
"parking_lot",
"percent-encoding",
"windows-sys 0.59.0",
@@ -698,11 +698,11 @@ dependencies = [
[[package]]
name = "block2"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2 0.6.2",
"objc2 0.6.3",
]
[[package]]
@@ -1600,7 +1600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.9.2",
"objc2 0.6.2",
"objc2 0.6.3",
]
[[package]]
@@ -2486,7 +2486,7 @@ checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
dependencies = [
"crossbeam-channel",
"keyboard-types",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"once_cell",
"serde",
@@ -2804,6 +2804,7 @@ dependencies = [
"anyhow",
"async-walkdir",
"base64 0.22.1",
"block2 0.6.2",
"cc",
"chrono",
"config",
@@ -2821,8 +2822,11 @@ dependencies = [
"mime_guess",
"moka",
"ndk-context",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"objc2-ui-kit",
"pulldown-cmark",
"reqwest 0.11.27",
"rodio",
@@ -3662,8 +3666,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "119c8490084af61b44c9eda9d626475847a186737c0378c85e32d77c33a01cd4"
dependencies = [
"cc",
"objc2 0.6.2",
"objc2-foundation 0.3.1",
"objc2 0.6.3",
"objc2-foundation 0.3.2",
"time",
]
@@ -3859,10 +3863,10 @@ dependencies = [
"dpi",
"gtk",
"keyboard-types",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
"objc2-foundation 0.3.2",
"once_cell",
"png",
"serde",
@@ -4154,9 +4158,9 @@ dependencies = [
[[package]]
name = "objc2"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc"
checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
dependencies = [
"objc2-encode",
"objc2-exception-helper",
@@ -4185,27 +4189,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
dependencies = [
"bitflags 2.9.2",
"block2 0.6.1",
"block2 0.6.2",
"libc",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-cloud-kit",
"objc2-core-data 0.3.1",
"objc2-core-data 0.3.2",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-image 0.3.1",
"objc2-foundation 0.3.1",
"objc2-quartz-core 0.3.1",
"objc2-core-image 0.3.2",
"objc2-foundation 0.3.2",
"objc2-quartz-core 0.3.2",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d"
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
dependencies = [
"bitflags 2.9.2",
"objc2 0.6.2",
"objc2-foundation 0.3.1",
"objc2 0.6.3",
"objc2-foundation 0.3.2",
]
[[package]]
@@ -4222,35 +4226,37 @@ dependencies = [
[[package]]
name = "objc2-core-data"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d"
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
dependencies = [
"bitflags 2.9.2",
"objc2 0.6.2",
"objc2-foundation 0.3.1",
"objc2 0.6.3",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.9.2",
"block2 0.6.2",
"dispatch2",
"objc2 0.6.2",
"libc",
"objc2 0.6.3",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.9.2",
"dispatch2",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-io-surface",
]
@@ -4269,12 +4275,34 @@ dependencies = [
[[package]]
name = "objc2-core-image"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e"
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
dependencies = [
"objc2 0.6.2",
"objc2-foundation 0.3.1",
"objc2 0.6.3",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-core-location"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
dependencies = [
"objc2 0.6.3",
"objc2-foundation 0.3.2",
]
[[package]]
name = "objc2-core-text"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
dependencies = [
"bitflags 2.9.2",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-core-graphics",
]
[[package]]
@@ -4307,25 +4335,25 @@ dependencies = [
[[package]]
name = "objc2-foundation"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.9.2",
"block2 0.6.1",
"block2 0.6.2",
"libc",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
"bitflags 2.9.2",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-core-foundation",
]
@@ -4335,7 +4363,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a"
dependencies = [
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-core-foundation",
]
@@ -4358,9 +4386,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d"
dependencies = [
"bitflags 2.9.2",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-foundation 0.3.1",
"objc2-foundation 0.3.2",
]
[[package]]
@@ -4378,13 +4406,14 @@ dependencies = [
[[package]]
name = "objc2-quartz-core"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5"
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
dependencies = [
"bitflags 2.9.2",
"objc2 0.6.2",
"objc2-foundation 0.3.1",
"objc2 0.6.3",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
]
[[package]]
@@ -4394,20 +4423,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44"
dependencies = [
"bitflags 2.9.2",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-core-foundation",
]
[[package]]
name = "objc2-ui-kit"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
dependencies = [
"bitflags 2.9.2",
"objc2 0.6.2",
"block2 0.6.2",
"objc2 0.6.3",
"objc2-cloud-kit",
"objc2-core-data 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
"objc2-core-graphics",
"objc2-core-image 0.3.2",
"objc2-core-location",
"objc2-core-text",
"objc2-foundation 0.3.2",
"objc2-quartz-core 0.3.2",
"objc2-user-notifications",
]
[[package]]
name = "objc2-user-notifications"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
dependencies = [
"objc2 0.6.3",
"objc2-foundation 0.3.2",
]
[[package]]
@@ -4417,11 +4465,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad"
dependencies = [
"bitflags 2.9.2",
"block2 0.6.1",
"objc2 0.6.2",
"block2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
"objc2-foundation 0.3.2",
"objc2-javascript-core",
"objc2-security",
]
@@ -4544,8 +4592,8 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2 0.6.2",
"objc2-foundation 0.3.1",
"objc2 0.6.3",
"objc2-foundation 0.3.2",
"objc2-osa-kit",
"serde",
"serde_json",
@@ -7032,7 +7080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
dependencies = [
"bitflags 2.9.2",
"block2 0.6.1",
"block2 0.6.2",
"core-foundation 0.10.1",
"core-graphics 0.24.0",
"crossbeam-channel",
@@ -7049,9 +7097,9 @@ dependencies = [
"ndk 0.9.0",
"ndk-context",
"ndk-sys 0.6.0+11769913",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-foundation 0.3.1",
"objc2-foundation 0.3.2",
"once_cell",
"parking_lot",
"raw-window-handle",
@@ -7123,9 +7171,9 @@ dependencies = [
"log",
"mime",
"muda",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-foundation 0.3.1",
"objc2-foundation 0.3.2",
"objc2-ui-kit",
"objc2-web-kit",
"percent-encoding",
@@ -7374,8 +7422,8 @@ dependencies = [
"byte-unit",
"fern",
"log",
"objc2 0.6.2",
"objc2-foundation 0.3.1",
"objc2 0.6.3",
"objc2-foundation 0.3.2",
"serde",
"serde_json",
"serde_repr",
@@ -7600,7 +7648,7 @@ dependencies = [
"gtk",
"http 1.3.1",
"jni",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-ui-kit",
"objc2-web-kit",
"raw-window-handle",
@@ -7624,9 +7672,9 @@ dependencies = [
"http 1.3.1",
"jni",
"log",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-foundation 0.3.1",
"objc2-foundation 0.3.2",
"once_cell",
"percent-encoding",
"raw-window-handle",
@@ -8157,11 +8205,11 @@ dependencies = [
"dirs 6.0.0",
"libappindicator",
"muda",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.1",
"objc2-foundation 0.3.2",
"once_cell",
"png",
"serde",
@@ -8903,10 +8951,10 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
dependencies = [
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
"objc2-foundation 0.3.2",
"raw-window-handle",
"windows-sys 0.59.0",
"windows-version",
@@ -9638,7 +9686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2"
dependencies = [
"base64 0.22.1",
"block2 0.6.1",
"block2 0.6.2",
"cookie",
"crossbeam-channel",
"dirs 6.0.0",
@@ -9653,10 +9701,10 @@ dependencies = [
"kuchikiki",
"libc",
"ndk 0.9.0",
"objc2 0.6.2",
"objc2 0.6.3",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
"objc2-foundation 0.3.2",
"objc2-ui-kit",
"objc2-web-kit",
"once_cell",
+7
View File
@@ -109,6 +109,13 @@ tauri-plugin-safe-area-insets = "0.1.0"
tauri-plugin-hula = { path="../tauri-plugin-hula"}
tauri-plugin-barcode-scanner = "2.4.0"
[target."cfg(target_os = \"ios\")".dependencies]
objc2 = "0.6.3"
objc2-core-foundation = "0.3.2"
objc2-foundation = "0.3.2"
objc2-ui-kit = "0.3.2"
block2 = "0.6.2"
[target."cfg(target_os = \"android\")".dependencies]
jni = "0.21"
ndk-context = "0.1"
File diff suppressed because one or more lines are too long
@@ -131,6 +131,7 @@ pub async fn parse_markdown(app: AppHandle, file_path: String) -> Result<String,
/// 读取 README markdown 文件
#[tauri::command]
#[allow(unused_variables)]
pub async fn get_readme_html(app: AppHandle, language: String) -> Result<String, String> {
// 构建 README 文件路径
// 在开发模式下,从项目根目录读取 README.md
+19 -2
View File
@@ -33,6 +33,8 @@ pub mod timeout_config;
pub mod utils;
mod vo;
pub mod websocket;
#[cfg(target_os = "ios")]
mod webview_helper;
use crate::command::request_command::{im_request_command, login_command};
use crate::command::room_member_command::{
@@ -71,7 +73,9 @@ use crate::command::file_manager_command::{
use crate::command::message_command::{page_msg, save_msg, send_msg, update_message_recall_status};
use crate::command::message_mark_command::save_message_mark;
use tauri::{AppHandle, Listener, Manager};
use tauri::{AppHandle, Manager};
#[cfg(desktop)]
use tauri::Listener;
use tokio::sync::Mutex;
pub fn run() {
@@ -301,7 +305,16 @@ fn setup_mobile() {
if let Err(e) = tauri::Builder::default()
.init_plugin()
.setup(move |app| {
common_setup(app.handle().clone())?;
let app_handle = app.handle().clone();
#[cfg(target_os = "ios")]
{
if let Some(webview_window) = app_handle.get_webview_window("mobile-home") {
webview_helper::initialize_keyboard_adjustment(&webview_window);
} else {
tracing::warn!("Mobile home webview window not found during setup");
}
}
common_setup(app_handle)?;
tracing::info!("Mobile application setup completed successfully");
Ok(())
})
@@ -359,6 +372,8 @@ fn get_invoke_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Se
use crate::desktops::common_cmd::set_badge_count;
#[cfg(mobile)]
use crate::mobiles::splash::hide_splash_screen;
#[cfg(target_os = "ios")]
use crate::mobiles::keyboard::set_webview_keyboard_adjustment;
use crate::websocket::commands::{
ws_disconnect, ws_force_reconnect, ws_get_app_background_state, ws_get_health,
ws_get_state, ws_init_connection, ws_is_connected, ws_send_message,
@@ -440,5 +455,7 @@ fn get_invoke_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Se
set_complete,
#[cfg(mobile)]
hide_splash_screen,
#[cfg(target_os = "ios")]
set_webview_keyboard_adjustment,
]
}
+5
View File
@@ -0,0 +1,5 @@
#[cfg(target_os = "ios")]
#[tauri::command]
pub fn set_webview_keyboard_adjustment(enabled: bool) {
crate::webview_helper::set_keyboard_adjustment(enabled);
}
+1
View File
@@ -1,2 +1,3 @@
pub mod init;
pub mod splash;
pub mod keyboard;
+542
View File
@@ -0,0 +1,542 @@
use std::{
cell::RefCell,
ptr::NonNull,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
use objc2::{
define_class, msg_send,
rc::Retained,
runtime::{AnyObject, ProtocolObject},
DefinedClass, MainThreadMarker, MainThreadOnly,
};
use objc2_core_foundation::{CGPoint, CGRect};
use objc2_foundation::{
NSDictionary, NSNumber, NSNotification, NSNotificationCenter, NSNotificationName, NSObject,
NSObjectProtocol, NSString, NSTimeInterval, NSValue,
};
use objc2_ui_kit::{
UIKeyboardDidShowNotification, UIKeyboardWillHideNotification, UIKeyboardWillShowNotification,
UIScrollView, UIScrollViewContentInsetAdjustmentBehavior, UIScrollViewDelegate, UIWebView,
UIColor, UIView, UIViewAnimationOptions,
};
use tauri::WebviewWindow;
thread_local! {
static KEYBOARD_DELEGATE: RefCell<Option<KeyboardDelegateHolder>> = RefCell::new(None);
}
static SHOULD_ADJUST: AtomicBool = AtomicBool::new(false);
#[inline]
fn should_adjust() -> bool {
SHOULD_ADJUST.load(Ordering::SeqCst)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum KeyboardHandlingMode {
None,
Adjust,
Lock,
}
enum KeyboardDelegateHolder {
Adjust(Retained<KeyboardScrollPreventDelegate>),
Lock(Retained<KeyboardLockDelegate>),
}
pub fn initialize_keyboard_adjustment(webview_window: &WebviewWindow) {
let _ = webview_window.with_webview(|webview| unsafe {
#[allow(deprecated)]
let webview: &UIWebView = &*webview.inner().cast();
let notification_center = NSNotificationCenter::defaultCenter();
let main_thread = MainThreadMarker::from(webview);
let background_color =
UIColor::colorWithRed_green_blue_alpha(243.0 / 255.0, 251.0 / 255.0, 248.0 / 255.0, 1.0);
webview.setOpaque(false);
webview.setBackgroundColor(Some(&background_color));
let scroll_view_arc = Arc::new(webview.scrollView());
scroll_view_arc.setBackgroundColor(Some(&background_color));
let old_delegate_arc = Arc::new(Mutex::new(None));
let handling_mode_arc = Arc::new(Mutex::new(KeyboardHandlingMode::None));
scroll_view_arc
.setContentInsetAdjustmentBehavior(UIScrollViewContentInsetAdjustmentBehavior::Never);
// 记录原始状态,避免累积调整
let keyboard_height_arc = Arc::new(Mutex::new(0f64));
let original_frame_arc = Arc::new(Mutex::new(webview.frame()));
let original_inset_arc = Arc::new(Mutex::new(scroll_view_arc.contentInset()));
let webview_arc = Arc::new(
Retained::retain(webview as *const _ as *mut UIWebView)
.expect("Failed to retain UIWebView reference"),
);
let scroll_view_arc_observer = scroll_view_arc.clone();
let old_delegate_arc_will_show = old_delegate_arc.clone();
let keyboard_height_arc_observer = keyboard_height_arc.clone();
let original_frame_arc_observer = original_frame_arc.clone();
let original_inset_arc_observer = original_inset_arc.clone();
let handling_mode_arc_observer = handling_mode_arc.clone();
let webview_arc_observer = webview_arc.clone();
create_observer(
&notification_center,
&UIKeyboardWillShowNotification,
move |notification| {
let desired_mode = if should_adjust() {
KeyboardHandlingMode::Adjust
} else {
KeyboardHandlingMode::Lock
};
{
let mut mode = handling_mode_arc_observer.lock().unwrap();
*mode = desired_mode;
}
let mut old_delegate = old_delegate_arc_will_show.lock().unwrap();
*old_delegate = scroll_view_arc_observer.delegate();
match desired_mode {
KeyboardHandlingMode::Adjust => {
unsafe {
let scroll_view_ptr = (&**scroll_view_arc_observer)
as *const UIScrollView
as *mut UIScrollView;
let _: () = msg_send![
scroll_view_ptr,
setShowsVerticalScrollIndicator: true
];
let _: () = msg_send![
scroll_view_ptr,
setShowsHorizontalScrollIndicator: true
];
let _: () = msg_send![scroll_view_ptr, setBounces: true];
}
let new_delegate = KeyboardScrollPreventDelegate::new(
main_thread,
scroll_view_arc_observer.clone(),
scroll_view_arc_observer.contentOffset(),
);
KEYBOARD_DELEGATE.with(|cell| {
*cell.borrow_mut() = Some(KeyboardDelegateHolder::Adjust(new_delegate));
});
KEYBOARD_DELEGATE.with(|cell| {
if let Some(KeyboardDelegateHolder::Adjust(delegate)) =
cell.borrow().as_ref()
{
let delegate_obj = ProtocolObject::from_ref(&**delegate);
scroll_view_arc_observer.setDelegate(Some(delegate_obj));
}
});
}
KeyboardHandlingMode::Lock => {
let new_delegate = KeyboardLockDelegate::new(
main_thread,
scroll_view_arc_observer.clone(),
webview_arc_observer.clone(),
scroll_view_arc_observer.contentOffset(),
);
KEYBOARD_DELEGATE.with(|cell| {
*cell.borrow_mut() = Some(KeyboardDelegateHolder::Lock(new_delegate));
});
KEYBOARD_DELEGATE.with(|cell| {
if let Some(KeyboardDelegateHolder::Lock(delegate)) =
cell.borrow().as_ref()
{
let delegate_obj = ProtocolObject::from_ref(&**delegate);
scroll_view_arc_observer.setDelegate(Some(delegate_obj));
}
});
unsafe {
let scroll_view_ptr = (&**scroll_view_arc_observer)
as *const UIScrollView
as *mut UIScrollView;
let _: () = msg_send![
scroll_view_ptr,
setShowsVerticalScrollIndicator: false
];
let _: () = msg_send![
scroll_view_ptr,
setShowsHorizontalScrollIndicator: false
];
let _: () = msg_send![scroll_view_ptr, setBounces: false];
}
// 清理键盘高度,避免误触发还原逻辑
let mut keyboard_height = keyboard_height_arc_observer.lock().unwrap();
*keyboard_height = 0.0;
// 在锁定模式下不需要执行下方的高度调整逻辑
return;
}
KeyboardHandlingMode::None => unreachable!(),
};
let user_info: *mut NSDictionary<NSString, AnyObject> =
msg_send![notification, userInfo];
if user_info.is_null() {
return;
}
let frame_key = NSString::from_str("UIKeyboardFrameEndUserInfoKey");
let value: *mut NSValue = msg_send![user_info, objectForKey: &*frame_key];
if value.is_null() {
return;
}
let keyboard_rect: CGRect = msg_send![value, CGRectValue];
let new_keyboard_height = keyboard_rect.size.height;
let original_frame = *original_frame_arc_observer.lock().unwrap();
let mut adjusted_frame = original_frame;
adjusted_frame.size.height -= new_keyboard_height;
let original_inset = *original_inset_arc_observer.lock().unwrap();
let mut adjusted_inset = original_inset;
adjusted_inset.bottom += new_keyboard_height;
{
let mut keyboard_height = keyboard_height_arc_observer.lock().unwrap();
*keyboard_height = new_keyboard_height as f64;
}
let duration_key = NSString::from_str("UIKeyboardAnimationDurationUserInfoKey");
let duration_value: *mut NSNumber =
msg_send![user_info, objectForKey: &*duration_key];
let duration: NSTimeInterval = {
duration_value
.as_ref()
.map(|number| number.doubleValue() as NSTimeInterval)
.unwrap_or(0.25)
};
let curve_key = NSString::from_str("UIKeyboardAnimationCurveUserInfoKey");
let curve_value: *mut NSNumber =
msg_send![user_info, objectForKey: &*curve_key];
let curve_bits = {
curve_value
.as_ref()
.map(|number| (number.integerValue() as u64) << 16)
.unwrap_or(UIViewAnimationOptions::CurveEaseInOut.bits() as u64)
};
let mut options = UIViewAnimationOptions::BeginFromCurrentState
| UIViewAnimationOptions::AllowUserInteraction;
options |= UIViewAnimationOptions::from_bits_retain(curve_bits as _);
let scroll_view_for_animation = scroll_view_arc_observer.clone();
let animation_block = block2::StackBlock::new(move || {
// Match the keyboard transition to avoid visible jumps.
scroll_view_for_animation.setContentInset(adjusted_inset);
scroll_view_for_animation.setScrollIndicatorInsets(adjusted_inset);
webview.setFrame(adjusted_frame);
})
.copy();
unsafe {
UIView::animateWithDuration_delay_options_animations_completion(
duration,
0.0,
options,
&animation_block,
None,
main_thread,
);
}
},
);
let scroll_view_arc_observer = scroll_view_arc.clone();
let old_delegate_arc_will_hide = old_delegate_arc.clone();
let keyboard_height_arc_observer = keyboard_height_arc.clone();
let original_frame_arc_observer = original_frame_arc.clone();
let original_inset_arc_observer = original_inset_arc.clone();
let handling_mode_arc_observer = handling_mode_arc.clone();
let webview_arc_observer = webview_arc.clone();
create_observer(
&notification_center,
&UIKeyboardWillHideNotification,
move |notification| {
let current_mode = *handling_mode_arc_observer.lock().unwrap();
if current_mode == KeyboardHandlingMode::Adjust {
let keyboard_height = keyboard_height_arc_observer.lock().unwrap();
if *keyboard_height <= 0.0 {
return;
}
drop(keyboard_height);
let user_info: *mut NSDictionary<NSString, AnyObject> =
msg_send![notification, userInfo];
let duration_key = NSString::from_str("UIKeyboardAnimationDurationUserInfoKey");
let duration_value: *mut NSNumber =
msg_send![user_info, objectForKey: &*duration_key];
let duration: NSTimeInterval = {
duration_value
.as_ref()
.map(|number| number.doubleValue() as NSTimeInterval)
.unwrap_or(0.25)
};
let curve_key = NSString::from_str("UIKeyboardAnimationCurveUserInfoKey");
let curve_value: *mut NSNumber =
msg_send![user_info, objectForKey: &*curve_key];
let curve_bits = {
curve_value
.as_ref()
.map(|number| (number.integerValue() as u64) << 16)
.unwrap_or(UIViewAnimationOptions::CurveEaseInOut.bits() as u64)
};
let mut options = UIViewAnimationOptions::BeginFromCurrentState
| UIViewAnimationOptions::AllowUserInteraction;
options |= UIViewAnimationOptions::from_bits_retain(curve_bits as _);
let original_frame = *original_frame_arc_observer.lock().unwrap();
let original_inset = *original_inset_arc_observer.lock().unwrap();
let scroll_view_for_animation = scroll_view_arc_observer.clone();
let animation_block = block2::StackBlock::new(move || {
// 跟随键盘动画恢复初始布局
scroll_view_for_animation.setContentInset(original_inset);
scroll_view_for_animation.setScrollIndicatorInsets(original_inset);
webview.setFrame(original_frame);
})
.copy();
unsafe {
UIView::animateWithDuration_delay_options_animations_completion(
duration,
0.0,
options,
&animation_block,
None,
main_thread,
);
}
let mut keyboard_height = keyboard_height_arc_observer.lock().unwrap();
*keyboard_height = 0.0;
let mut mode = handling_mode_arc_observer.lock().unwrap();
*mode = KeyboardHandlingMode::None;
} else if current_mode == KeyboardHandlingMode::Lock {
// 在锁定模式下,恢复滚动视图的默认行为并保持键盘隐藏
let webview_ptr =
(&**webview_arc_observer) as *const UIWebView as *mut UIWebView;
let _: () = msg_send![webview_ptr, endEditing: true];
unsafe {
let scroll_view_ptr = (&**scroll_view_arc_observer)
as *const UIScrollView
as *mut UIScrollView;
let _: () = msg_send![
scroll_view_ptr,
setShowsVerticalScrollIndicator: true
];
let _: () = msg_send![
scroll_view_ptr,
setShowsHorizontalScrollIndicator: true
];
let _: () = msg_send![scroll_view_ptr, setBounces: true];
}
KEYBOARD_DELEGATE.with(|cell| {
*cell.borrow_mut() = None;
});
let mut old_delegate = old_delegate_arc_will_hide.lock().unwrap();
if let Some(delegate) = old_delegate.take() {
scroll_view_arc_observer.setDelegate(Some(delegate.as_ref()));
} else {
scroll_view_arc_observer.setDelegate(None);
}
let mut mode = handling_mode_arc_observer.lock().unwrap();
*mode = KeyboardHandlingMode::None;
}
},
);
let scroll_view_arc_observer = scroll_view_arc.clone();
let old_delegate_arc_did_show = old_delegate_arc.clone();
let keyboard_height_arc_observer = keyboard_height_arc.clone();
let original_frame_arc_observer = original_frame_arc.clone();
let original_inset_arc_observer = original_inset_arc.clone();
let handling_mode_arc_observer = handling_mode_arc.clone();
create_observer(
&notification_center,
&UIKeyboardDidShowNotification,
move |notification| {
let current_mode = *handling_mode_arc_observer.lock().unwrap();
if current_mode != KeyboardHandlingMode::Adjust {
return;
}
let user_info: *mut NSDictionary<NSString, AnyObject> = msg_send![notification, userInfo];
if user_info.is_null() {
return;
}
let key = NSString::from_str("UIKeyboardFrameEndUserInfoKey");
let value: *mut NSValue = msg_send![user_info, objectForKey: &*key];
if value.is_null() {
return;
}
let keyboard_rect: CGRect = msg_send![value, CGRectValue];
let new_keyboard_height = keyboard_rect.size.height;
// 基于原始状态进行调整,避免累积
let original_frame = original_frame_arc_observer.lock().unwrap();
let mut adjusted_frame = *original_frame;
adjusted_frame.size.height -= new_keyboard_height;
webview.setFrame(adjusted_frame);
// 基于原始 inset 进行调整
let original_inset = original_inset_arc_observer.lock().unwrap();
let mut adjusted_inset = *original_inset;
adjusted_inset.bottom += new_keyboard_height;
scroll_view_arc_observer.setContentInset(adjusted_inset);
scroll_view_arc_observer.setScrollIndicatorInsets(adjusted_inset);
// 更新键盘高度记录
let mut keyboard_height = keyboard_height_arc_observer.lock().unwrap();
*keyboard_height = new_keyboard_height;
let mut old_delegate = old_delegate_arc_did_show.lock().unwrap();
if let Some(delegate) = old_delegate.take() {
scroll_view_arc_observer.setDelegate(Some(delegate.as_ref()));
} else {
scroll_view_arc_observer.setDelegate(None);
}
KEYBOARD_DELEGATE.with(|cell| {
*cell.borrow_mut() = None;
});
},
);
});
}
pub fn set_keyboard_adjustment(enabled: bool) {
SHOULD_ADJUST.store(enabled, Ordering::SeqCst);
}
pub struct KeyboardScrollPreventDelegateState {
pub scroll_view: Arc<Retained<UIScrollView>>,
pub offset: CGPoint,
}
define_class! {
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "KeyboardScrollPreventDelegate"]
#[ivars = KeyboardScrollPreventDelegateState]
pub(crate) struct KeyboardScrollPreventDelegate;
unsafe impl NSObjectProtocol for KeyboardScrollPreventDelegate {}
unsafe impl UIScrollViewDelegate for KeyboardScrollPreventDelegate {
#[unsafe(method(scrollViewDidScroll:))]
unsafe fn scroll_view_did_scroll(&self, _scroll_view: &UIScrollView) {
self.ivars()
.scroll_view
.setContentOffset(self.ivars().offset);
}
}
}
impl KeyboardScrollPreventDelegate {
fn new(
mtm: MainThreadMarker,
scroll_view: Arc<Retained<UIScrollView>>,
offset: CGPoint,
) -> Retained<Self> {
let delegate = Self::alloc(mtm).set_ivars(KeyboardScrollPreventDelegateState {
scroll_view,
offset,
});
unsafe { msg_send![super(delegate), init] }
}
}
pub struct KeyboardLockDelegateState {
pub scroll_view: Arc<Retained<UIScrollView>>,
pub webview: Arc<Retained<UIWebView>>,
pub locked_offset: CGPoint,
}
define_class! {
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "KeyboardLockDelegate"]
#[ivars = KeyboardLockDelegateState]
pub(crate) struct KeyboardLockDelegate;
unsafe impl NSObjectProtocol for KeyboardLockDelegate {}
unsafe impl UIScrollViewDelegate for KeyboardLockDelegate {
#[unsafe(method(scrollViewWillBeginDragging:))]
unsafe fn scroll_view_will_begin_dragging(&self, _scroll_view: &UIScrollView) {
let ivars = self.ivars();
let webview_ptr =
(&**ivars.webview) as *const UIWebView as *mut UIWebView;
let _: () = msg_send![webview_ptr, endEditing: true];
ivars
.scroll_view
.setContentOffset(ivars.locked_offset);
}
#[unsafe(method(scrollViewDidScroll:))]
unsafe fn scroll_view_did_scroll(&self, _scroll_view: &UIScrollView) {
let ivars = self.ivars();
ivars
.scroll_view
.setContentOffset(ivars.locked_offset);
}
}
}
impl KeyboardLockDelegate {
fn new(
mtm: MainThreadMarker,
scroll_view: Arc<Retained<UIScrollView>>,
webview: Arc<Retained<UIWebView>>,
offset: CGPoint,
) -> Retained<Self> {
let delegate = Self::alloc(mtm).set_ivars(KeyboardLockDelegateState {
scroll_view,
webview,
locked_offset: offset,
});
unsafe { msg_send![super(delegate), init] }
}
}
fn create_observer(
center: &NSNotificationCenter,
name: &NSNotificationName,
handler: impl Fn(&NSNotification) + 'static,
) -> Retained<ProtocolObject<dyn NSObjectProtocol>> {
let block = block2::RcBlock::new(move |notification: NonNull<NSNotification>| {
handler(unsafe { notification.as_ref() });
});
unsafe { center.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block) }
}
+5
View File
@@ -0,0 +1,5 @@
#[cfg(target_os = "ios")]
pub mod ios;
#[cfg(target_os = "ios")]
pub use ios::{initialize_keyboard_adjustment, set_keyboard_adjustment};
+17 -1
View File
@@ -31,7 +31,7 @@ import { useMitt } from '@/hooks/useMitt.ts'
import { useWindow } from '@/hooks/useWindow.ts'
import { useGlobalStore } from '@/stores/global'
import { useSettingStore } from '@/stores/setting.ts'
import { isDesktop, isMobile, isWindows } from '@/utils/PlatformConstants'
import { isDesktop, isIOS, isMobile, isWindows } from '@/utils/PlatformConstants'
import LockScreen from '@/views/LockScreen.vue'
import {
@@ -382,7 +382,23 @@ const listenMobileReLogin = async () => {
}
}
/**
* iOS网络权限预请求
* 在应用启动时发起一个轻量级网络请求,触发iOS的网络权限弹窗
*/
const requestNetworkPermissionForIOS = async () => {
if (!isIOS()) return
await fetch('https://www.apple.com/favicon.ico', {
method: 'HEAD',
cache: 'no-cache'
})
}
onMounted(() => {
// iOS应用启动时预请求网络权限(必须在最开始执行)
requestNetworkPermissionForIOS()
// 仅在windows上使用
if (isWindows()) {
fixedScale.enable()
+1 -1
View File
@@ -94,7 +94,7 @@ export const useReplaceMsg = () => {
*/
const getMessageSenderName = (message: MessageType, defaultName: string = '', roomId: string) => {
if (!message?.fromUser?.uid) return defaultName
const session = chatStore.getSession(roomId)
const session = chatStore.getSession(roomId)!
if (session.type === RoomTypeEnum.GROUP) {
const user = groupStore.getUser(roomId, message.fromUser.uid)
return user?.myName || user?.name || ''
@@ -17,7 +17,9 @@
<script setup lang="ts">
import 'vant/es/dialog/style'
import { invoke } from '@tauri-apps/api/core'
import { useGlobalStore } from '@/stores/global'
import { isIOS } from '@/utils/PlatformConstants'
const globalStore = useGlobalStore()
const emit = defineEmits(['focus', 'blur', 'updateHeight'])
@@ -38,6 +40,16 @@ onMounted(() => {
resizeObserver.disconnect()
})
}
if (isIOS()) {
invoke('set_webview_keyboard_adjustment', { enabled: true })
}
})
onUnmounted(() => {
if (isIOS()) {
invoke('set_webview_keyboard_adjustment', { enabled: false })
}
})
// ==== 对外暴露 ====
+20 -7
View File
@@ -116,12 +116,7 @@
</n-button>
<!-- 协议 -->
<n-flex
align="center"
justify="center"
:style="isAndroid() ? { bottom: safeArea.bottom + 10 + 'px' } : {}"
:size="6"
class="absolute bottom-0 w-[80%]">
<n-flex align="center" justify="center" :style="agreementStyle" :size="6" class="absolute bottom-0 w-[80%]">
<n-checkbox v-model:checked="protocol" />
<div class="text-12px color-#909090 cursor-default lh-14px">
<span>已阅读并同意</span>
@@ -270,6 +265,7 @@
<script setup lang="ts">
import { debounce } from 'lodash-es'
import { invoke } from '@tauri-apps/api/core'
import Validation from '@/components/common/Validation.vue'
import router from '@/router'
import type { RegisterUserReq, UserInfoType } from '@/services/types'
@@ -277,7 +273,7 @@ import { useLoginHistoriesStore } from '@/stores/loginHistory.ts'
import { useMobileStore } from '@/stores/mobile'
import { AvatarUtils } from '@/utils/AvatarUtils'
import { register, sendCaptcha } from '@/utils/ImRequestUtils'
import { isAndroid } from '@/utils/PlatformConstants'
import { isAndroid, isIOS } from '@/utils/PlatformConstants'
import { validateAlphaNumeric, validateSpecialChar } from '@/utils/Validate'
import { useMitt } from '../hooks/useMitt'
import { WsResponseMessageType } from '../services/wsType'
@@ -347,6 +343,17 @@ const sendCodeDisabled = computed(() => {
return sendCodeLoading.value || sendCodeCountdown.value > 0 || !registerInfo.value.email || !isEmailValid.value
})
const agreementStyle = computed(() => {
const inset = safeArea.value.bottom || 0
if (isAndroid()) {
return { bottom: `${inset + 10}px` }
}
if (inset > 0) {
return { bottom: `${inset}px` }
}
return { bottom: 'var(--safe-area-inset-bottom)' }
})
const stopSendCodeCountdown = () => {
timerWorker.postMessage({
type: 'clearTimer',
@@ -674,6 +681,9 @@ const refreshAvatar = debounce((newAccount: string) => {
onMounted(async () => {
window.addEventListener('click', closeMenu, true)
if (isIOS()) {
invoke('set_webview_keyboard_adjustment', { enabled: false })
}
// 只有在需要登录的情况下才显示登录窗口
if (isJumpDirectly.value) {
loading.value = false
@@ -697,6 +707,9 @@ onUnmounted(() => {
window.removeEventListener('click', closeMenu, true)
stopSendCodeCountdown()
timerWorker.terminate()
if (isIOS()) {
invoke('set_webview_keyboard_adjustment', { enabled: false })
}
})
</script>
-17
View File
@@ -120,21 +120,4 @@ $font-family-sans: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-family: $font-family-system, $font-family-windows, $font-family-sans;
color: #757775;
}
// .page-view {
// flex: 1;
// // 进入时的动画
// animation: fade-slide-in 0.3s ease;
// }
// @keyframes fade-slide-in {
// from {
// transform: translateX(20px);
// opacity: 0;
// }
// to {
// transform: translateX(0);
// opacity: 1;
// }
// }
</style>
+1 -5
View File
@@ -3,7 +3,7 @@ import { info } from '@tauri-apps/plugin-log'
import { sendNotification } from '@tauri-apps/plugin-notification'
import { defineStore } from 'pinia'
import { useRoute } from 'vue-router'
import { AppException, ErrorType } from '@/common/exception'
import { ErrorType } from '@/common/exception'
import { type MessageStatusEnum, MsgEnum, RoomTypeEnum, StoresEnum, TauriCommand } from '@/enums'
import type { MarkItemType, MessageType, RevokedMsgType, SessionItem } from '@/services/types'
import { useGlobalStore } from '@/stores/global.ts'
@@ -320,10 +320,6 @@ export const useChatStore = defineStore(
// 通过房间ID获取会话信息
const getSession = (roomId: string) => {
const currentSession = roomId ? sessionList.value.find((item) => item.roomId === roomId) : sessionList.value[0]
if (!currentSession) {
throw new AppException(`根据房间 ID: ${roomId}无法找到会话`)
}
return currentSession
}
+1 -1
View File
@@ -33,7 +33,7 @@ export const useGlobalStore = defineStore(
const currentSessionRoomId = ref('')
// 当前会话信息:包含房间ID和房间类型
const currentSession = computed((): SessionItem => {
return chatStore.getSession(currentSessionRoomId.value)
return chatStore.getSession(currentSessionRoomId.value)!
})
/** 当前选中的联系人信息 */