Add fake iOS FFI XCFramework build
This commit is contained in:
4
.github/workflows/ios-rust.yml
vendored
4
.github/workflows/ios-rust.yml
vendored
@@ -19,10 +19,14 @@ jobs:
|
||||
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
- name: Workspace tests
|
||||
run: cargo test --workspace --all-features
|
||||
- name: Fake iOS FFI tests
|
||||
run: cargo test -p tele-ios-ffi --no-default-features --features standalone-fake
|
||||
- name: Generate iOS FFI bindings
|
||||
run: scripts/generate-ios-ffi-bindings.sh /tmp/tele-ios-ffi
|
||||
- name: Swift bindings typecheck
|
||||
run: swiftc -typecheck -I /tmp/tele-ios-ffi/Headers /tmp/tele-ios-ffi/Swift/tele_ios_ffi.swift
|
||||
- name: Build fake iOS FFI XCFramework
|
||||
run: scripts/build-ios-fake-ffi-xcframework.sh /tmp/tele-ios-fake-ffi-xcframework
|
||||
|
||||
ios-shell:
|
||||
runs-on: macos-latest
|
||||
|
||||
@@ -10,8 +10,13 @@ repository = "https://github.com/your-username/tele-tui"
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["core-session"]
|
||||
core-session = ["dep:tele-core"]
|
||||
standalone-fake = []
|
||||
|
||||
[dependencies]
|
||||
tele-core = { path = "../tele-core", features = ["test-support"] }
|
||||
tele-core = { path = "../tele-core", features = ["test-support"], optional = true }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
thiserror = "1.0"
|
||||
uniffi = { version = "0.31.1", features = ["tokio"] }
|
||||
|
||||
@@ -18,6 +18,12 @@ The script builds `target/release/libtele_ios_ffi.a` and writes Swift sources,
|
||||
headers, a Swift typecheck-friendly `tele_ios_ffiFFI` module map, and an
|
||||
XCFramework-compatible module map under `build/ios-ffi/`.
|
||||
|
||||
Build the fake-only iOS simulator XCFramework without linking TDLib:
|
||||
|
||||
```bash
|
||||
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer scripts/build-ios-fake-ffi-xcframework.sh
|
||||
```
|
||||
|
||||
Current linking status:
|
||||
|
||||
- Xcode is installed at `/Applications/Xcode.app`, and `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -version` reports Xcode 26.5.
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[cfg(not(feature = "core-session"))]
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
use tele_core::session::{
|
||||
CoreAuthState, CoreChatSummary, CoreDownloadedFile, CoreDraft, CoreEvent, CoreFolder,
|
||||
CoreMedia, CoreMessage, CoreNetworkState, CoreProfile, CoreReaction, CoreSearchResult,
|
||||
CoreSession,
|
||||
};
|
||||
#[cfg(feature = "core-session")]
|
||||
use tele_core::tdlib::{ChatInfo, MessageBuilder, NetworkState, ProfileInfo};
|
||||
#[cfg(feature = "core-session")]
|
||||
use tele_core::test_support::FakeTdClient;
|
||||
#[cfg(feature = "core-session")]
|
||||
use tele_core::types::{ChatId, MessageId};
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
@@ -42,6 +49,7 @@ pub enum IosAuthState {
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreAuthState> for IosAuthState {
|
||||
fn from(value: CoreAuthState) -> Self {
|
||||
match value {
|
||||
@@ -65,6 +73,7 @@ pub enum IosNetworkState {
|
||||
Ready,
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreNetworkState> for IosNetworkState {
|
||||
fn from(value: CoreNetworkState) -> Self {
|
||||
match value {
|
||||
@@ -91,6 +100,7 @@ pub struct IosFolder {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreFolder> for IosFolder {
|
||||
fn from(value: CoreFolder) -> Self {
|
||||
Self { id: value.id, name: value.name }
|
||||
@@ -103,6 +113,7 @@ pub struct IosDraft {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreDraft> for IosDraft {
|
||||
fn from(value: CoreDraft) -> Self {
|
||||
Self { chat_id: value.chat_id.as_i64(), text: value.text }
|
||||
@@ -126,6 +137,7 @@ pub struct IosChatSummary {
|
||||
pub draft: Option<IosDraft>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreChatSummary> for IosChatSummary {
|
||||
fn from(value: CoreChatSummary) -> Self {
|
||||
Self {
|
||||
@@ -153,6 +165,7 @@ pub struct IosReaction {
|
||||
pub is_chosen: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreReaction> for IosReaction {
|
||||
fn from(value: CoreReaction) -> Self {
|
||||
Self {
|
||||
@@ -187,6 +200,7 @@ pub struct IosMedia {
|
||||
pub download_state: IosDownloadState,
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreMedia> for IosMedia {
|
||||
fn from(value: CoreMedia) -> Self {
|
||||
match value {
|
||||
@@ -259,6 +273,7 @@ pub struct IosMessage {
|
||||
pub reactions: Vec<IosReaction>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreMessage> for IosMessage {
|
||||
fn from(value: CoreMessage) -> Self {
|
||||
Self {
|
||||
@@ -293,6 +308,7 @@ pub struct IosSearchResult {
|
||||
pub message: IosMessage,
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreSearchResult> for IosSearchResult {
|
||||
fn from(value: CoreSearchResult) -> Self {
|
||||
Self {
|
||||
@@ -317,6 +333,7 @@ pub struct IosProfile {
|
||||
pub online_status: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreProfile> for IosProfile {
|
||||
fn from(value: CoreProfile) -> Self {
|
||||
Self {
|
||||
@@ -341,6 +358,7 @@ pub struct IosDownloadedFile {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreDownloadedFile> for IosDownloadedFile {
|
||||
fn from(value: CoreDownloadedFile) -> Self {
|
||||
Self { file_id: value.file_id, path: value.path }
|
||||
@@ -396,6 +414,7 @@ pub enum IosEvent {
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
impl From<CoreEvent> for IosEvent {
|
||||
fn from(value: CoreEvent) -> Self {
|
||||
match value {
|
||||
@@ -442,12 +461,14 @@ impl From<CoreEvent> for IosEvent {
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
#[cfg(feature = "core-session")]
|
||||
pub struct SessionHandle {
|
||||
session: Mutex<CoreSession<FakeTdClient>>,
|
||||
runtime: tokio::runtime::Runtime,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[cfg(feature = "core-session")]
|
||||
pub fn create_session(config: IosSessionConfig) -> Result<SessionHandle, IosFfiError> {
|
||||
if !config.use_fake_tdlib {
|
||||
return Err(IosFfiError::Operation {
|
||||
@@ -466,6 +487,7 @@ pub fn create_session(config: IosSessionConfig) -> Result<SessionHandle, IosFfiE
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[cfg(feature = "core-session")]
|
||||
impl SessionHandle {
|
||||
pub fn auth_state(&self) -> IosAuthState {
|
||||
self.with_session(|session| session.auth_state().into())
|
||||
@@ -694,12 +716,14 @@ impl SessionHandle {
|
||||
}
|
||||
|
||||
impl SessionHandle {
|
||||
#[cfg(feature = "core-session")]
|
||||
fn with_session<R>(&self, f: impl FnOnce(&mut CoreSession<FakeTdClient>) -> R) -> R {
|
||||
let mut session = self.session.lock().expect("session mutex poisoned");
|
||||
f(&mut session)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "core-session")]
|
||||
fn seeded_fake_client() -> FakeTdClient {
|
||||
let chat = ChatInfo {
|
||||
id: ChatId::new(1),
|
||||
@@ -744,6 +768,479 @@ fn seeded_fake_client() -> FakeTdClient {
|
||||
.with_downloaded_file(200, "/tmp/fake-voice.ogg")
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
#[cfg(not(feature = "core-session"))]
|
||||
pub struct SessionHandle {
|
||||
state: Mutex<StandaloneFakeState>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "core-session"))]
|
||||
struct StandaloneFakeState {
|
||||
auth: IosAuthState,
|
||||
chats: Vec<IosChatSummary>,
|
||||
folders: Vec<IosFolder>,
|
||||
messages: HashMap<i64, Vec<IosMessage>>,
|
||||
profiles: HashMap<i64, IosProfile>,
|
||||
events: Vec<IosEvent>,
|
||||
downloaded_files: HashMap<i32, String>,
|
||||
next_message_id: i64,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "core-session"))]
|
||||
impl StandaloneFakeState {
|
||||
fn seeded() -> Self {
|
||||
let chat = IosChatSummary {
|
||||
id: 1,
|
||||
title: "Saved Messages".to_string(),
|
||||
username: Some("saved".to_string()),
|
||||
last_message: "Hello from fake TDLib".to_string(),
|
||||
last_message_date: 1_700_000_000,
|
||||
unread_count: 1,
|
||||
unread_mention_count: 0,
|
||||
is_pinned: true,
|
||||
order: 1,
|
||||
last_read_outbox_message_id: 1,
|
||||
folder_ids: vec![0],
|
||||
is_muted: false,
|
||||
draft: None,
|
||||
};
|
||||
let message = IosMessage {
|
||||
id: 1,
|
||||
sender_name: "Alice".to_string(),
|
||||
date: 1_700_000_000,
|
||||
edit_date: None,
|
||||
media_album_id: None,
|
||||
text: "Hello from fake TDLib".to_string(),
|
||||
media: None,
|
||||
is_outgoing: false,
|
||||
is_read: false,
|
||||
can_be_edited: false,
|
||||
can_be_deleted_only_for_self: true,
|
||||
can_be_deleted_for_all_users: false,
|
||||
reply: None,
|
||||
forward: None,
|
||||
reactions: Vec::new(),
|
||||
};
|
||||
let profile = IosProfile {
|
||||
chat_id: chat.id,
|
||||
title: chat.title.clone(),
|
||||
username: chat.username.clone(),
|
||||
bio: Some("Fake profile for iOS bridge tests".to_string()),
|
||||
phone_number: None,
|
||||
chat_type: "Private".to_string(),
|
||||
member_count: None,
|
||||
description: None,
|
||||
invite_link: None,
|
||||
is_group: false,
|
||||
online_status: Some("online".to_string()),
|
||||
};
|
||||
|
||||
let mut messages = HashMap::new();
|
||||
messages.insert(chat.id, vec![message]);
|
||||
|
||||
let mut profiles = HashMap::new();
|
||||
profiles.insert(chat.id, profile);
|
||||
|
||||
let mut downloaded_files = HashMap::new();
|
||||
downloaded_files.insert(100, "/tmp/fake-photo.jpg".to_string());
|
||||
downloaded_files.insert(200, "/tmp/fake-voice.ogg".to_string());
|
||||
|
||||
Self {
|
||||
auth: IosAuthState::WaitPhoneNumber,
|
||||
chats: vec![chat.clone()],
|
||||
folders: vec![IosFolder { id: 0, name: "All".to_string() }],
|
||||
messages,
|
||||
profiles,
|
||||
events: vec![IosEvent::ChatListChanged { chats: vec![chat] }],
|
||||
downloaded_files,
|
||||
next_message_id: 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn message_mut(
|
||||
&mut self,
|
||||
chat_id: i64,
|
||||
message_id: i64,
|
||||
) -> Result<&mut IosMessage, IosFfiError> {
|
||||
self.messages
|
||||
.get_mut(&chat_id)
|
||||
.and_then(|messages| messages.iter_mut().find(|message| message.id == message_id))
|
||||
.ok_or_else(|| IosFfiError::Operation { message: "message not found".to_string() })
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[cfg(not(feature = "core-session"))]
|
||||
pub fn create_session(config: IosSessionConfig) -> Result<SessionHandle, IosFfiError> {
|
||||
if !config.use_fake_tdlib {
|
||||
return Err(IosFfiError::Operation {
|
||||
message: "real TDLib sessions require the core-session build".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(SessionHandle { state: Mutex::new(StandaloneFakeState::seeded()) })
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[cfg(not(feature = "core-session"))]
|
||||
impl SessionHandle {
|
||||
pub fn auth_state(&self) -> IosAuthState {
|
||||
self.state
|
||||
.lock()
|
||||
.expect("session mutex poisoned")
|
||||
.auth
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn poll_events(&self) -> Vec<IosEvent> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
std::mem::take(&mut state.events)
|
||||
}
|
||||
|
||||
pub fn send_phone_number(&self, _phone: String) -> Result<(), IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
state.auth = IosAuthState::WaitCode;
|
||||
let auth = state.auth.clone();
|
||||
state.events.push(IosEvent::AuthChanged { state: auth });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_code(&self, _code: String) -> Result<(), IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
state.auth = IosAuthState::WaitPassword;
|
||||
let auth = state.auth.clone();
|
||||
state.events.push(IosEvent::AuthChanged { state: auth });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_password(&self, _password: String) -> Result<(), IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
state.auth = IosAuthState::Ready;
|
||||
let auth = state.auth.clone();
|
||||
state.events.push(IosEvent::AuthChanged { state: auth });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_chats(&self, limit: i32) -> Result<Vec<IosChatSummary>, IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
let chats: Vec<_> = state
|
||||
.chats
|
||||
.iter()
|
||||
.take(limit.max(0) as usize)
|
||||
.cloned()
|
||||
.collect();
|
||||
state
|
||||
.events
|
||||
.push(IosEvent::ChatListChanged { chats: chats.clone() });
|
||||
Ok(chats)
|
||||
}
|
||||
|
||||
pub fn load_folders(&self) -> Vec<IosFolder> {
|
||||
self.state
|
||||
.lock()
|
||||
.expect("session mutex poisoned")
|
||||
.folders
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn load_folder_chats(
|
||||
&self,
|
||||
folder_id: i32,
|
||||
limit: i32,
|
||||
) -> Result<Vec<IosChatSummary>, IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
let chats: Vec<_> = state
|
||||
.chats
|
||||
.iter()
|
||||
.filter(|chat| chat.folder_ids.contains(&folder_id))
|
||||
.take(limit.max(0) as usize)
|
||||
.cloned()
|
||||
.collect();
|
||||
state
|
||||
.events
|
||||
.push(IosEvent::ChatListChanged { chats: chats.clone() });
|
||||
Ok(chats)
|
||||
}
|
||||
|
||||
pub fn load_history(&self, chat_id: i64, limit: i32) -> Result<Vec<IosMessage>, IosFfiError> {
|
||||
let state = self.state.lock().expect("session mutex poisoned");
|
||||
Ok(state
|
||||
.messages
|
||||
.get(&chat_id)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.take(limit.max(0) as usize)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn search_messages(
|
||||
&self,
|
||||
chat_id: i64,
|
||||
query: String,
|
||||
) -> Result<Vec<IosSearchResult>, IosFfiError> {
|
||||
let state = self.state.lock().expect("session mutex poisoned");
|
||||
Ok(state
|
||||
.messages
|
||||
.get(&chat_id)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|message| {
|
||||
query.is_empty()
|
||||
|| message.text.contains(&query)
|
||||
|| message.sender_name.contains(&query)
|
||||
})
|
||||
.map(|message| IosSearchResult { chat_id, message })
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn open_profile(&self, chat_id: i64) -> Result<IosProfile, IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
let profile = state
|
||||
.profiles
|
||||
.get(&chat_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| IosFfiError::Operation { message: "profile not found".to_string() })?;
|
||||
state
|
||||
.events
|
||||
.push(IosEvent::ProfileLoaded { profile: profile.clone() });
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub fn send_message(
|
||||
&self,
|
||||
chat_id: i64,
|
||||
text: String,
|
||||
reply_to_message_id: Option<i64>,
|
||||
) -> Result<IosMessage, IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
let message = IosMessage {
|
||||
id: state.next_message_id,
|
||||
sender_name: "Me".to_string(),
|
||||
date: 1_700_000_001,
|
||||
edit_date: None,
|
||||
media_album_id: None,
|
||||
text: text.clone(),
|
||||
media: None,
|
||||
is_outgoing: true,
|
||||
is_read: true,
|
||||
can_be_edited: true,
|
||||
can_be_deleted_only_for_self: true,
|
||||
can_be_deleted_for_all_users: true,
|
||||
reply: reply_to_message_id.map(|message_id| IosReply {
|
||||
message_id,
|
||||
sender_name: "Alice".to_string(),
|
||||
text: format!("Reply to #{message_id}"),
|
||||
}),
|
||||
forward: None,
|
||||
reactions: Vec::new(),
|
||||
};
|
||||
state.next_message_id += 1;
|
||||
state
|
||||
.messages
|
||||
.entry(chat_id)
|
||||
.or_default()
|
||||
.push(message.clone());
|
||||
if let Some(chat) = state.chats.iter_mut().find(|chat| chat.id == chat_id) {
|
||||
chat.last_message = text;
|
||||
chat.draft = None;
|
||||
}
|
||||
state
|
||||
.events
|
||||
.push(IosEvent::MessageAdded { chat_id, message: message.clone() });
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub fn edit_message(
|
||||
&self,
|
||||
chat_id: i64,
|
||||
message_id: i64,
|
||||
text: String,
|
||||
) -> Result<IosMessage, IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
let updated = {
|
||||
let message = state.message_mut(chat_id, message_id)?;
|
||||
message.text = text;
|
||||
message.edit_date = Some(1_700_000_002);
|
||||
message.clone()
|
||||
};
|
||||
state
|
||||
.events
|
||||
.push(IosEvent::MessageUpdated { chat_id, message: updated.clone() });
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub fn delete_messages(
|
||||
&self,
|
||||
chat_id: i64,
|
||||
message_ids: Vec<i64>,
|
||||
_revoke: bool,
|
||||
) -> Result<(), IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
state
|
||||
.messages
|
||||
.entry(chat_id)
|
||||
.or_default()
|
||||
.retain(|message| !message_ids.contains(&message.id));
|
||||
state
|
||||
.events
|
||||
.push(IosEvent::MessageDeleted { chat_id, message_ids });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn forward_messages(
|
||||
&self,
|
||||
to_chat_id: i64,
|
||||
from_chat_id: i64,
|
||||
message_ids: Vec<i64>,
|
||||
) -> Result<(), IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
let source_messages: Vec<_> = state
|
||||
.messages
|
||||
.get(&from_chat_id)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|message| message_ids.contains(&message.id))
|
||||
.collect();
|
||||
for source in source_messages {
|
||||
let forwarded = IosMessage {
|
||||
id: state.next_message_id,
|
||||
sender_name: "Me".to_string(),
|
||||
date: 1_700_000_003,
|
||||
edit_date: None,
|
||||
media_album_id: source.media_album_id,
|
||||
text: source.text,
|
||||
media: source.media,
|
||||
is_outgoing: true,
|
||||
is_read: true,
|
||||
can_be_edited: true,
|
||||
can_be_deleted_only_for_self: true,
|
||||
can_be_deleted_for_all_users: true,
|
||||
reply: None,
|
||||
forward: Some(IosForward { sender_name: source.sender_name }),
|
||||
reactions: Vec::new(),
|
||||
};
|
||||
state.next_message_id += 1;
|
||||
state
|
||||
.messages
|
||||
.entry(to_chat_id)
|
||||
.or_default()
|
||||
.push(forwarded.clone());
|
||||
state
|
||||
.events
|
||||
.push(IosEvent::MessageAdded { chat_id: to_chat_id, message: forwarded });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn react(
|
||||
&self,
|
||||
chat_id: i64,
|
||||
message_id: i64,
|
||||
reaction: String,
|
||||
) -> Result<Vec<IosReaction>, IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
let reactions = {
|
||||
let message = state.message_mut(chat_id, message_id)?;
|
||||
if let Some(index) = message
|
||||
.reactions
|
||||
.iter()
|
||||
.position(|item| item.emoji == reaction)
|
||||
{
|
||||
message.reactions.remove(index);
|
||||
} else {
|
||||
message
|
||||
.reactions
|
||||
.push(IosReaction { emoji: reaction, count: 1, is_chosen: true });
|
||||
}
|
||||
message.reactions.clone()
|
||||
};
|
||||
state.events.push(IosEvent::ReactionChanged {
|
||||
chat_id,
|
||||
message_id,
|
||||
reactions: reactions.clone(),
|
||||
});
|
||||
Ok(reactions)
|
||||
}
|
||||
|
||||
pub fn set_draft(&self, chat_id: i64, text: String) -> Result<(), IosFfiError> {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
let draft = IosDraft { chat_id, text };
|
||||
if let Some(chat) = state.chats.iter_mut().find(|chat| chat.id == chat_id) {
|
||||
chat.draft = Some(draft.clone());
|
||||
}
|
||||
state.events.push(IosEvent::DraftChanged { draft });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn download_photo(&self, file_id: i32) -> Result<IosDownloadedFile, IosFfiError> {
|
||||
self.standalone_download_file(file_id)
|
||||
}
|
||||
|
||||
pub fn download_voice(&self, file_id: i32) -> Result<IosDownloadedFile, IosFfiError> {
|
||||
self.standalone_download_file(file_id)
|
||||
}
|
||||
|
||||
pub fn simulate_incoming_message(&self, chat_id: i64, text: String, sender_name: String) {
|
||||
let mut state = self.state.lock().expect("session mutex poisoned");
|
||||
let message = IosMessage {
|
||||
id: state.next_message_id,
|
||||
sender_name: sender_name.clone(),
|
||||
date: 1_700_000_004,
|
||||
edit_date: None,
|
||||
media_album_id: None,
|
||||
text: text.clone(),
|
||||
media: None,
|
||||
is_outgoing: false,
|
||||
is_read: false,
|
||||
can_be_edited: false,
|
||||
can_be_deleted_only_for_self: true,
|
||||
can_be_deleted_for_all_users: false,
|
||||
reply: None,
|
||||
forward: None,
|
||||
reactions: Vec::new(),
|
||||
};
|
||||
state.next_message_id += 1;
|
||||
state
|
||||
.messages
|
||||
.entry(chat_id)
|
||||
.or_default()
|
||||
.push(message.clone());
|
||||
let notification_chat =
|
||||
if let Some(chat) = state.chats.iter_mut().find(|chat| chat.id == chat_id) {
|
||||
chat.last_message = text;
|
||||
Some(chat.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(chat) = notification_chat {
|
||||
state.events.push(IosEvent::IncomingNotificationCandidate {
|
||||
chat,
|
||||
message: message.clone(),
|
||||
sender_name,
|
||||
});
|
||||
}
|
||||
state
|
||||
.events
|
||||
.push(IosEvent::MessageAdded { chat_id, message });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "core-session"))]
|
||||
impl SessionHandle {
|
||||
fn standalone_download_file(&self, file_id: i32) -> Result<IosDownloadedFile, IosFfiError> {
|
||||
let state = self.state.lock().expect("session mutex poisoned");
|
||||
state
|
||||
.downloaded_files
|
||||
.get(&file_id)
|
||||
.cloned()
|
||||
.map(|path| IosDownloadedFile { file_id, path })
|
||||
.ok_or_else(|| IosFfiError::Operation { message: "file not found".to_string() })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
- `swift run TeleTuiIOSSmokeTests`
|
||||
- `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer scripts/build-ios-simulator-app.sh`
|
||||
- `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer scripts/smoke-ios-simulator-ui.sh`
|
||||
- `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer scripts/build-ios-fake-ffi-xcframework.sh`
|
||||
- `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer scripts/check-ios-tdlib-linking.sh` once TDLib iOS artifacts are available
|
||||
|
||||
`swift test` is not currently a CI gate on this host because the CLI Swift toolchain available to the package does not expose `XCTest` or `Testing`. The deterministic view-model coverage lives in the executable `TeleTuiIOSSmokeTests` target until an Xcode test target is introduced.
|
||||
|
||||
42
scripts/build-ios-fake-ffi-xcframework.sh
Executable file
42
scripts/build-ios-fake-ffi-xcframework.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
out_dir="${1:-${repo_root}/build/ios-fake-ffi-xcframework}"
|
||||
target="${IOS_RUST_TARGET:-aarch64-apple-ios-sim}"
|
||||
lib_path="${repo_root}/target/${target}/release/libtele_ios_ffi.a"
|
||||
framework_name="${IOS_FFI_FRAMEWORK_NAME:-tele_ios_ffi}"
|
||||
|
||||
if [[ -z "${DEVELOPER_DIR:-}" && -d /Applications/Xcode.app/Contents/Developer ]]; then
|
||||
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
|
||||
fi
|
||||
|
||||
cd "${repo_root}"
|
||||
|
||||
if ! rustup target list --installed | grep -qx "${target}"; then
|
||||
rustup target add "${target}"
|
||||
fi
|
||||
|
||||
cargo build \
|
||||
-p tele-ios-ffi \
|
||||
--no-default-features \
|
||||
--features standalone-fake \
|
||||
--target "${target}" \
|
||||
--release
|
||||
|
||||
rm -rf "${out_dir}"
|
||||
mkdir -p "${out_dir}/Swift" "${out_dir}/Headers"
|
||||
|
||||
cargo run -p uniffi-bindgen-swift -- "${lib_path}" "${out_dir}/Swift" --swift-sources
|
||||
cargo run -p uniffi-bindgen-swift -- "${lib_path}" "${out_dir}/Headers" --headers
|
||||
cargo run -p uniffi-bindgen-swift -- "${lib_path}" "${out_dir}/Headers" \
|
||||
--modulemap \
|
||||
--module-name tele_ios_ffiFFI \
|
||||
--modulemap-filename module.modulemap
|
||||
|
||||
xcodebuild -create-xcframework \
|
||||
-library "${lib_path}" \
|
||||
-headers "${out_dir}/Headers" \
|
||||
-output "${out_dir}/${framework_name}.xcframework"
|
||||
|
||||
printf 'Generated fake-only iOS FFI XCFramework in %s\n' "${out_dir}"
|
||||
Reference in New Issue
Block a user