Compare commits

..

5 Commits

17 changed files with 476 additions and 33 deletions

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# Building
Make sure the required Android NDK platforms are installed, and the environment
variable `ANDROID_NDK_TOOLCHAIN_DIR` is configured correctly.
On Windows, this might be a path such as the following (NDK "Side by side" SDK tool):
```
%LOCALAPPDATA%\Android\Sdk\ndk\25.1.8937393\toolchains\llvm\prebuilt\windows-x86_64\bin
```

View File

@ -1,6 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id "org.mozilla.rust-android-gradle.rust-android"
} }
android { android {
@ -44,6 +45,7 @@ android {
excludes += '/META-INF/{AL2.0,LGPL2.1}' excludes += '/META-INF/{AL2.0,LGPL2.1}'
} }
} }
ndkVersion '25.1.8937393'
} }
dependencies { dependencies {
@ -54,6 +56,7 @@ dependencies {
implementation "androidx.compose.ui:ui:$compose_ui_version" implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
implementation 'androidx.compose.material:material:1.2.1' implementation 'androidx.compose.material:material:1.2.1'
implementation "androidx.navigation:navigation-compose:2.5.2"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@ -61,3 +64,17 @@ dependencies {
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
} }
// See https://github.com/mozilla/rust-android-gradle for other targets and required toolchains
cargo {
module = "../native"
libname = "talaria"
targets = ["arm64", "arm", "x86"]
profile = 'release'
}
tasks.whenTaskAdded { task ->
if ((task.name == 'javaPreCompileDebug' || task.name == 'javaPreCompileRelease')) {
task.dependsOn 'cargoBuild'
}
}

View File

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"

View File

@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import dev.lonami.talaria.ui.LoginScreen import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.ui.theme.TalariaTheme import dev.lonami.talaria.ui.theme.TalariaTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -20,9 +20,11 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background color = MaterialTheme.colors.background
) { ) {
LoginScreen() TalariaApp()
} }
} }
} }
Native.initClient()
} }
} }

View File

@ -0,0 +1,81 @@
package dev.lonami.talaria
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.padding
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import dev.lonami.talaria.ui.ChatScreen
import dev.lonami.talaria.ui.DialogScreen
import dev.lonami.talaria.ui.LoginScreen
enum class TalariaScreen(@StringRes val title: Int) {
Login(title = R.string.app_name),
Dialog(title = R.string.dialog),
Chat(title = R.string.chat),
}
@Composable
fun TalariaAppBar(currentScreen: TalariaScreen, canNavigateBack: Boolean, navigateUp: () -> Unit) {
TopAppBar(
title = { Text(stringResource(currentScreen.title)) },
navigationIcon = {
if (canNavigateBack) {
IconButton(onClick = navigateUp) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_button)
)
}
}
}
)
}
@Composable
fun TalariaApp() {
val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState()
val currentScreen =
TalariaScreen.valueOf(backStackEntry?.destination?.route ?: TalariaScreen.Login.name)
var selectedDialog by remember { mutableStateOf("") }
Scaffold(
topBar = {
TalariaAppBar(
currentScreen,
canNavigateBack = navController.previousBackStackEntry != null,
navigateUp = { navController.navigateUp() }
)
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = TalariaScreen.Login.name,
Modifier.padding(innerPadding)
) {
composable(route = TalariaScreen.Dialog.name) {
DialogScreen(onDialogSelected = {
selectedDialog = it
navController.navigate(TalariaScreen.Chat.name)
})
}
composable(route = TalariaScreen.Chat.name) {
ChatScreen(selectedDialog)
}
composable(route = TalariaScreen.Login.name) {
LoginScreen(onConfirmOtp = {
navController.navigate(TalariaScreen.Dialog.name)
})
}
}
}
}

View File

@ -0,0 +1,18 @@
package dev.lonami.talaria.bindings
object Native {
init {
System.loadLibrary("talaria")
}
external fun initClient()
external fun needLogin(): Boolean
external fun requestLoginCode(phone: String): Long
external fun signIn(tokenPtr: Long, code: String)
external fun getDialogs(): Long
external fun dialogCount(dialogsPtr: Long): Int
external fun dialogPacked(dialogsPtr: Long, index: Int): String
external fun dialogTitle(dialogsPtr: Long, index: Int): String
external fun freeDialogs(dialogsPtr: Long)
external fun sendMessage(packed: String, text: String)
}

View File

@ -1,20 +1,25 @@
package dev.lonami.talaria.data package dev.lonami.talaria.data
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.model.Dialog import dev.lonami.talaria.model.Dialog
object DialogSource { object DialogSource {
fun loadDialogs(): List<Dialog> { fun loadDialogs(): List<Dialog> {
return listOf( val dialogs = mutableListOf<Dialog>()
Dialog("Saved Messages", "Secret launch-code: banana", pinned = true),
Dialog("First Sample Dialog", "Photo", pinned = false), val dialogPtr = Native.getDialogs()
Dialog("Second Sample Dialog", "Video", pinned = false), val dialogCount = Native.dialogCount(dialogPtr)
Dialog("Third Sample Dialog", "Audio", pinned = false), for (i in 0 until dialogCount) {
Dialog("Fourth Sample Dialog", "Sticker (just kidding who uses that)", pinned = false), dialogs.add(
Dialog("Fifth Sample Dialog", "Photo", pinned = false), Dialog(
Dialog("Sixth Sample Dialog", "Video", pinned = false), Native.dialogTitle(dialogPtr, i),
Dialog("Seventh Sample Dialog", "Audio", pinned = false), Native.dialogPacked(dialogPtr, i),
Dialog("Eighth Sample Dialog", "Sticker (just kidding who uses that)", pinned = false), false
Dialog("Ninth Sample Dialog", "Hello, scroll!", pinned = false), )
) )
} }
Native.freeDialogs(dialogPtr)
return dialogs
}
} }

View File

@ -77,7 +77,7 @@ fun MessageInputField(
} }
@Composable @Composable
fun ChatScreen(chatViewModel: ChatViewModel = viewModel()) { fun ChatScreen(selectedDialog: String, chatViewModel: ChatViewModel = viewModel()) {
val chatUiState by chatViewModel.uiState.collectAsState() val chatUiState by chatViewModel.uiState.collectAsState()
var messageText by remember { mutableStateOf("") } var messageText by remember { mutableStateOf("") }
val messageListState = rememberLazyListState() val messageListState = rememberLazyListState()
@ -92,7 +92,7 @@ fun ChatScreen(chatViewModel: ChatViewModel = viewModel()) {
MessageInputField(messageText, onMessageChanged = { MessageInputField(messageText, onMessageChanged = {
messageText = it messageText = it
}, onSendMessage = { }, onSendMessage = {
chatViewModel.sendMessage(messageText) chatViewModel.sendMessage(selectedDialog, messageText)
messageText = "" messageText = ""
coroutineScope.launch { coroutineScope.launch {
messageListState.animateScrollToItem(chatUiState.messages.size - 1) messageListState.animateScrollToItem(chatUiState.messages.size - 1)
@ -105,6 +105,6 @@ fun ChatScreen(chatViewModel: ChatViewModel = viewModel()) {
@Composable @Composable
fun ChatPreview() { fun ChatPreview() {
TalariaTheme { TalariaTheme {
ChatScreen() ChatScreen("")
} }
} }

View File

@ -1,11 +1,13 @@
package dev.lonami.talaria.ui package dev.lonami.talaria.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.data.MessageSource import dev.lonami.talaria.data.MessageSource
import dev.lonami.talaria.model.Message import dev.lonami.talaria.model.Message
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class ChatViewModel : ViewModel() { class ChatViewModel : ViewModel() {
private val _uiState = MutableStateFlow(ChatUiState()) private val _uiState = MutableStateFlow(ChatUiState())
@ -15,8 +17,12 @@ class ChatViewModel : ViewModel() {
_uiState.value = ChatUiState(MessageSource.loadMessages().toMutableList()) _uiState.value = ChatUiState(MessageSource.loadMessages().toMutableList())
} }
fun sendMessage(message: String) { fun sendMessage(dialog: String, message: String) {
_uiState.value.messages.add(Message("You", message)) Native.sendMessage(dialog, message)
_uiState.update { state ->
state.messages.add(Message("You", message))
state
}
} }
init { init {

View File

@ -1,6 +1,7 @@
package dev.lonami.talaria.ui package dev.lonami.talaria.ui
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -24,11 +25,12 @@ import dev.lonami.talaria.model.Dialog
import dev.lonami.talaria.ui.theme.TalariaTheme import dev.lonami.talaria.ui.theme.TalariaTheme
@Composable @Composable
fun DialogCard(dialog: Dialog) { fun DialogCard(dialog: Dialog, onDialogSelected: () -> Unit) {
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp, 16.dp) .padding(8.dp, 16.dp)
.clickable(onClick = onDialogSelected)
) { ) {
Row { Row {
Image( Image(
@ -45,22 +47,29 @@ fun DialogCard(dialog: Dialog) {
} }
@Composable @Composable
fun DialogList(dialogs: List<Dialog>) { fun DialogList(dialogs: List<Dialog>, onDialogSelected: (String) -> Unit) {
LazyColumn { LazyColumn {
items(dialogs.size) { DialogCard(dialogs[it]) } items(dialogs.size) {
DialogCard(dialogs[it], onDialogSelected = {
onDialogSelected(dialogs[it].lastMessage)
})
}
} }
} }
@Composable @Composable
fun DialogScreen(dialogViewModel: DialogViewModel = viewModel()) { fun DialogScreen(
onDialogSelected: (String) -> Unit,
dialogViewModel: DialogViewModel = viewModel()
) {
val dialogUiState by dialogViewModel.uiState.collectAsState() val dialogUiState by dialogViewModel.uiState.collectAsState()
DialogList(dialogUiState.dialogs) DialogList(dialogUiState.dialogs, onDialogSelected = onDialogSelected)
} }
@Preview @Preview
@Composable @Composable
fun DialogPreview() { fun DialogPreview() {
TalariaTheme { TalariaTheme {
DialogScreen() DialogScreen(onDialogSelected = { })
} }
} }

View File

@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import dev.lonami.talaria.R import dev.lonami.talaria.R
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.ui.theme.TalariaTheme import dev.lonami.talaria.ui.theme.TalariaTheme
enum class LoginStage { enum class LoginStage {
@ -85,11 +86,13 @@ fun OtpInput(otp: String, onOtpChanged: (String) -> Unit, onConfirmOtp: () -> Un
} }
@Composable @Composable
fun LoginScreen() { fun LoginScreen(onConfirmOtp: () -> Unit) {
var stage by remember { mutableStateOf(LoginStage.ASK_PHONE) } var stage by remember { mutableStateOf(LoginStage.ASK_PHONE) }
var phone by remember { mutableStateOf("") } var phone by remember { mutableStateOf("") }
var otp by remember { mutableStateOf("") } var otp by remember { mutableStateOf("") }
var tokenPtr by remember { mutableStateOf(0L) }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -109,10 +112,19 @@ fun LoginScreen() {
LoginStage.ASK_PHONE -> PhoneInput( LoginStage.ASK_PHONE -> PhoneInput(
phone, phone,
onPhoneChanged = { phone = it }, onPhoneChanged = { phone = it },
onSendCode = { stage = LoginStage.ASK_CODE }) onSendCode = {
LoginStage.ASK_CODE -> OtpInput(otp, onOtpChanged = { otp = it }, onConfirmOtp = { tokenPtr = Native.requestLoginCode(phone)
stage = LoginStage.ASK_CODE
}) }
)
LoginStage.ASK_CODE -> OtpInput(
otp,
onOtpChanged = { otp = it },
onConfirmOtp = {
Native.signIn(tokenPtr, otp)
onConfirmOtp()
}
)
} }
} }
} }
@ -121,6 +133,6 @@ fun LoginScreen() {
@Composable @Composable
fun LoginPreview() { fun LoginPreview() {
TalariaTheme { TalariaTheme {
LoginScreen() LoginScreen(onConfirmOtp = { })
} }
} }

View File

@ -12,4 +12,7 @@
<string name="profile_photo">Profile Picture</string> <string name="profile_photo">Profile Picture</string>
<string name="write_message">Write a message…</string> <string name="write_message">Write a message…</string>
<string name="send_message">Send</string> <string name="send_message">Send</string>
<string name="back_button">Back</string>
<string name="dialog">All chats</string>
<string name="chat">Messages</string>
</resources> </resources>

View File

@ -7,4 +7,5 @@ plugins {
id 'com.android.application' version '7.3.0' apply false id 'com.android.application' version '7.3.0' apply false
id 'com.android.library' version '7.3.0' apply false id 'com.android.library' version '7.3.0' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3"
} }

14
native/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

22
native/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "talaria"
version = "0.1.0"
edition = "2021"
[lib]
name = "talaria"
crate-type = ["cdylib"]
[dependencies]
jni = { version = "0.10.2", default-features = false }
# v0.4 of grammers-* is currently unreleased; clone the project and use path dependencies
grammers-client = { version = "0.4.0" }
grammers-tl-types = { version = "0.4.0" }
grammers-session = { version = "0.4.0" }
tokio = { version = "1.5.0", features = ["full"] }
log = "0.4.14"
android_logger = "0.11.1"
once_cell = "1.15.0"
[profile.release]
lto = true

240
native/src/lib.rs Normal file
View File

@ -0,0 +1,240 @@
#![cfg(target_os = "android")]
#![allow(non_snake_case)]
use std::ffi::{CStr, CString};
use std::future::Future;
use grammers_client::{Client, Config};
use grammers_client::types::{Dialog, LoginToken};
use grammers_session::{PackedChat, Session};
use jni::JNIEnv;
use jni::objects::{JObject, JString};
use jni::sys::{jboolean, jint, jlong, jstring};
use log;
use log::{error, info, Level};
use once_cell::sync::OnceCell;
use tokio::runtime;
use tokio::runtime::Runtime;
const LOG_MIN_LEVEL: Level = Level::Trace;
const LOG_TAG: &str = ".native.talari";
const API_ID: i32 = 0;
const API_HASH: &str = "";
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
static RUNTIME: OnceCell<Runtime> = OnceCell::new();
static CLIENT: OnceCell<Client> = OnceCell::new();
fn block_on<F: Future>(future: F) -> F::Output {
if RUNTIME.get().is_none() {
RUNTIME
.set(
runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap(),
)
.ok();
}
RUNTIME.get().unwrap().block_on(future)
}
async fn init_client() -> Result<()> {
android_logger::init_once(
android_logger::Config::default()
.with_min_level(LOG_MIN_LEVEL)
.with_tag(LOG_TAG),
);
if CLIENT.get().is_some() {
info!("Client is already initialized");
return Ok(());
}
info!("Connecting to Telegram...");
let client = Client::connect(Config {
session: Session::new(),
api_id: API_ID,
api_hash: API_HASH.to_string(),
params: Default::default(),
})
.await?;
info!("Connected!");
CLIENT
.set(client)
.map_err(|_| "Client was already initialized")?;
Ok(())
}
async fn need_login() -> Result<bool> {
let client = CLIENT.get().ok_or("Client not initialized")?;
Ok(client.is_authorized().await?)
}
async fn request_login_code(phone: &str) -> Result<LoginToken> {
let client = CLIENT.get().ok_or("Client not initialized")?;
let token = client.request_login_code(&phone, API_ID, API_HASH).await?;
Ok(token)
}
async fn sign_in(token: LoginToken, code: &str) -> Result<()> {
let client = CLIENT.get().ok_or("Client not initialized")?;
client.sign_in(&token, &code).await?;
Ok(())
}
async fn get_dialogs() -> Result<Vec<Dialog>> {
let client = CLIENT.get().ok_or("Client not initialized")?;
let mut result = Vec::new();
let mut dialogs = client.iter_dialogs();
while let Some(dialog) = dialogs.next().await? {
result.push(dialog);
}
Ok(result)
}
async fn send_message(chat: PackedChat, text: &str) -> Result<()> {
let client = CLIENT.get().ok_or("Client not initialized")?;
client.send_message(chat, text).await?;
Ok(())
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_initClient(_: JNIEnv, _: JObject) {
match block_on(init_client()) {
Ok(_) => info!("Client initialized"),
Err(e) => error!("Failed to initialize client: {}", e),
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_needLogin(
_: JNIEnv,
_: JObject,
) -> jboolean {
match block_on(need_login()) {
Ok(login) => login.into(),
Err(e) => {
error!("Failed to check if user is authorized: {}", e);
false.into()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_requestLoginCode(
env: JNIEnv,
_: JObject,
phone: JString,
) -> jlong {
let phone = CString::from(CStr::from_ptr(env.get_string(phone).unwrap().as_ptr()));
match block_on(request_login_code(phone.to_str().unwrap())) {
Ok(token) => Box::into_raw(Box::new(token)) as jlong,
Err(e) => {
error!("Failed to request login code: {}", e);
0 as jlong
}
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_signIn(
env: JNIEnv,
_: JObject,
token_ptr: jlong,
code: JString,
) {
let token = *Box::from_raw(token_ptr as *mut LoginToken);
let code = CString::from(CStr::from_ptr(env.get_string(code).unwrap().as_ptr()));
match block_on(sign_in(token, code.to_str().unwrap())) {
Ok(_) => info!("Sign in success"),
Err(e) => error!("Failed to sign in: {}", e),
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_getDialogs(
_: JNIEnv,
_: JObject,
) -> jlong {
match block_on(get_dialogs()) {
Ok(dialogs) => Box::into_raw(Box::new(dialogs)) as jlong,
Err(e) => {
error!("Failed to get dialogs: {}", e);
0 as jlong
}
}
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogCount(
_: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
) -> jint {
let dialogs = Box::leak(Box::from_raw(dialogs_ptr as *mut Vec<Dialog>));
dialogs.len() as jint
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogPacked(
env: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jstring {
let dialogs = Box::leak(Box::from_raw(dialogs_ptr as *mut Vec<Dialog>));
let packed = dialogs[index as usize].chat().pack().to_hex();
let output = env.new_string(packed).unwrap();
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogTitle(
env: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jstring {
let dialogs = Box::leak(Box::from_raw(dialogs_ptr as *mut Vec<Dialog>));
let title = dialogs[index as usize].chat().name();
let output = env.new_string(title).unwrap();
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_freeDialogs(
_: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
) {
let _ = Box::from_raw(dialogs_ptr as *mut Vec<Dialog>);
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_sendMessage(
env: JNIEnv,
_: JObject,
packed: JString,
text: JString,
) {
let packed = CString::from(CStr::from_ptr(env.get_string(packed).unwrap().as_ptr()));
let text = CString::from(CStr::from_ptr(env.get_string(text).unwrap().as_ptr()));
let packed = PackedChat::from_hex(packed.to_str().unwrap()).unwrap();
match block_on(send_message(packed, text.to_str().unwrap())) {
Ok(_) => info!("Message sent"),
Err(e) => error!("Failed to send message: {}", e),
}
}