Compare commits
No commits in common. "master" and "master" have entirely different histories.
|
@ -1,7 +1,6 @@
|
||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
<codeStyleSettings language="XML">
|
<codeStyleSettings language="XML">
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# Building
|
# Building
|
||||||
|
|
||||||
Clone the repo, then open the project in Android Studio.
|
|
||||||
|
|
||||||
Make sure the required Android NDK platforms are installed, and the environment
|
Make sure the required Android NDK platforms are installed, and the environment
|
||||||
variable `ANDROID_NDK_TOOLCHAIN_DIR` is configured correctly.
|
variable `ANDROID_NDK_TOOLCHAIN_DIR` is configured correctly.
|
||||||
|
|
||||||
|
@ -10,9 +8,3 @@ On Windows, this might be a path such as the following (NDK "Side by side" SDK t
|
||||||
```
|
```
|
||||||
%LOCALAPPDATA%\Android\Sdk\ndk\25.1.8937393\toolchains\llvm\prebuilt\windows-x86_64\bin
|
%LOCALAPPDATA%\Android\Sdk\ndk\25.1.8937393\toolchains\llvm\prebuilt\windows-x86_64\bin
|
||||||
```
|
```
|
||||||
|
|
||||||
Set your API ID and hash in the rust code.
|
|
||||||
|
|
||||||
Change the Cargo.toml to point to a local grammers source tree and remove the version argument.
|
|
||||||
|
|
||||||
Sync gradle files, and Android Studio's "build" and "run" should Just Work.
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "dev.lonami.talaria"
|
applicationId "dev.lonami.talaria"
|
||||||
minSdk 26
|
minSdk 21
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "0.1.0"
|
versionName "0.1.0"
|
||||||
|
@ -57,7 +57,6 @@ dependencies {
|
||||||
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"
|
implementation "androidx.navigation:navigation-compose:2.5.2"
|
||||||
implementation "net.java.dev.jna:jna:5.12.0@aar"
|
|
||||||
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'
|
||||||
|
@ -69,8 +68,8 @@ dependencies {
|
||||||
// See https://github.com/mozilla/rust-android-gradle for other targets and required toolchains
|
// See https://github.com/mozilla/rust-android-gradle for other targets and required toolchains
|
||||||
cargo {
|
cargo {
|
||||||
module = "../native"
|
module = "../native"
|
||||||
libname = "uniffi_talaria"
|
libname = "talaria"
|
||||||
targets = ["arm64"]
|
targets = ["arm64", "arm", "x86"]
|
||||||
profile = 'release'
|
profile = 'release'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,14 +78,3 @@ tasks.whenTaskAdded { task ->
|
||||||
task.dependsOn 'cargoBuild'
|
task.dependsOn 'cargoBuild'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://mozilla.github.io/uniffi-rs/kotlin/gradle.html for more details on this snippet
|
|
||||||
android.applicationVariants.all { variant ->
|
|
||||||
def t = tasks.register("generate${variant.name.capitalize()}UniFFIBindings", Exec) {
|
|
||||||
workingDir "${project.projectDir}"
|
|
||||||
commandLine 'uniffi-bindgen', 'generate', '../native/src/talaria.udl', '--language', 'kotlin', '--no-format', '--out-dir', "${buildDir}/generated/source/uniffi/${variant.name}/java"
|
|
||||||
}
|
|
||||||
variant.javaCompileProvider.get().dependsOn(t)
|
|
||||||
def sourceSet = variant.sourceSets.find { it.name == variant.name }
|
|
||||||
sourceSet.java.srcDir new File(buildDir, "generated/source/uniffi/${variant.name}/java")
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,15 +7,15 @@ 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.bindings.Native
|
||||||
import dev.lonami.talaria.ui.theme.TalariaTheme
|
import dev.lonami.talaria.ui.theme.TalariaTheme
|
||||||
import uniffi.talaria.initClient
|
|
||||||
import uniffi.talaria.initDatabase
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
TalariaTheme {
|
TalariaTheme {
|
||||||
|
// A surface container using the 'background' color from the theme
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colors.background
|
color = MaterialTheme.colors.background
|
||||||
|
@ -25,7 +25,6 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initDatabase(getDatabasePath("talaria.db").path)
|
Native.initClient()
|
||||||
initClient()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,20 @@
|
||||||
package dev.lonami.talaria
|
package dev.lonami.talaria
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import dev.lonami.talaria.ui.screens.ChatScreen
|
import dev.lonami.talaria.ui.ChatScreen
|
||||||
import dev.lonami.talaria.ui.screens.DialogScreen
|
import dev.lonami.talaria.ui.DialogScreen
|
||||||
import dev.lonami.talaria.ui.screens.LoginScreen
|
import dev.lonami.talaria.ui.LoginScreen
|
||||||
import dev.lonami.talaria.ui.state.ChatViewModel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import uniffi.talaria.needLogin
|
|
||||||
|
|
||||||
enum class TalariaScreen(@StringRes val title: Int) {
|
enum class TalariaScreen(@StringRes val title: Int) {
|
||||||
Login(title = R.string.app_name),
|
Login(title = R.string.app_name),
|
||||||
|
@ -31,96 +22,9 @@ enum class TalariaScreen(@StringRes val title: Int) {
|
||||||
Chat(title = R.string.chat),
|
Chat(title = R.string.chat),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class DrawerAction {
|
|
||||||
SelectAccount,
|
|
||||||
AddAccount,
|
|
||||||
SavedMessages,
|
|
||||||
Contacts,
|
|
||||||
NewChat,
|
|
||||||
Settings,
|
|
||||||
Help,
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DrawerAction(
|
fun TalariaAppBar(currentScreen: TalariaScreen, canNavigateBack: Boolean, navigateUp: () -> Unit) {
|
||||||
icon: ImageVector,
|
|
||||||
text: String,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(0.dp, 8.dp)
|
|
||||||
) {
|
|
||||||
Icon(imageVector = icon, contentDescription = null)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Drawer(onSelect: (DrawerAction) -> Unit, modifier: Modifier = Modifier) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(24.dp, 48.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
|
||||||
contentDescription = "App Icon"
|
|
||||||
)
|
|
||||||
DrawerAction(
|
|
||||||
icon = Icons.Filled.AccountBox,
|
|
||||||
text = "Main Account",
|
|
||||||
onClick = { onSelect(DrawerAction.SelectAccount) }
|
|
||||||
)
|
|
||||||
DrawerAction(
|
|
||||||
icon = Icons.Filled.Add,
|
|
||||||
text = "Add Account",
|
|
||||||
onClick = { onSelect(DrawerAction.AddAccount) }
|
|
||||||
)
|
|
||||||
Divider()
|
|
||||||
DrawerAction(
|
|
||||||
icon = Icons.Filled.Star,
|
|
||||||
text = "Saved Messages",
|
|
||||||
onClick = { onSelect(DrawerAction.SavedMessages) }
|
|
||||||
)
|
|
||||||
DrawerAction(
|
|
||||||
icon = Icons.Filled.Call,
|
|
||||||
text = "Contacts",
|
|
||||||
onClick = { onSelect(DrawerAction.Contacts) }
|
|
||||||
)
|
|
||||||
DrawerAction(
|
|
||||||
icon = Icons.Filled.Create,
|
|
||||||
text = "New Chat",
|
|
||||||
onClick = { onSelect(DrawerAction.NewChat) }
|
|
||||||
)
|
|
||||||
Divider()
|
|
||||||
DrawerAction(
|
|
||||||
icon = Icons.Filled.Settings,
|
|
||||||
text = "Settings",
|
|
||||||
onClick = { onSelect(DrawerAction.Settings) }
|
|
||||||
)
|
|
||||||
DrawerAction(
|
|
||||||
icon = Icons.Filled.Info,
|
|
||||||
text = "Help",
|
|
||||||
onClick = { onSelect(DrawerAction.Help) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TalariaAppBar(
|
|
||||||
currentScreen: TalariaScreen,
|
|
||||||
canNavigateBack: Boolean,
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
openDrawer: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = modifier,
|
|
||||||
title = { Text(stringResource(currentScreen.title)) },
|
title = { Text(stringResource(currentScreen.title)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (canNavigateBack) {
|
if (canNavigateBack) {
|
||||||
|
@ -130,78 +34,42 @@ fun TalariaAppBar(
|
||||||
contentDescription = stringResource(R.string.back_button)
|
contentDescription = stringResource(R.string.back_button)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
IconButton(onClick = openDrawer) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Menu,
|
|
||||||
contentDescription = stringResource(androidx.compose.ui.R.string.navigation_menu)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TalariaApp(modifier: Modifier = Modifier) {
|
fun TalariaApp() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val backStackEntry by navController.currentBackStackEntryAsState()
|
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentScreen =
|
val currentScreen =
|
||||||
TalariaScreen.valueOf(backStackEntry?.destination?.route ?: TalariaScreen.Login.name)
|
TalariaScreen.valueOf(backStackEntry?.destination?.route ?: TalariaScreen.Login.name)
|
||||||
|
|
||||||
val loggedIn by remember { mutableStateOf(!needLogin()) }
|
|
||||||
var selectedDialog by remember { mutableStateOf("") }
|
var selectedDialog by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val toggleDrawer = { open: Boolean ->
|
|
||||||
scope.launch {
|
|
||||||
if (open) {
|
|
||||||
drawerState.open()
|
|
||||||
} else {
|
|
||||||
drawerState.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val chatViewModel = remember { ChatViewModel() }
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
|
||||||
topBar = {
|
topBar = {
|
||||||
TalariaAppBar(
|
TalariaAppBar(
|
||||||
currentScreen,
|
currentScreen,
|
||||||
canNavigateBack = navController.previousBackStackEntry != null,
|
canNavigateBack = navController.previousBackStackEntry != null,
|
||||||
navigateUp = { navController.navigateUp() },
|
navigateUp = { navController.navigateUp() }
|
||||||
openDrawer = { toggleDrawer(drawerState.isClosed) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
ModalDrawer(
|
|
||||||
drawerState = drawerState,
|
|
||||||
gesturesEnabled = drawerState.isOpen,
|
|
||||||
drawerContent = {
|
|
||||||
Drawer(onSelect = {})
|
|
||||||
}) {
|
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = if (loggedIn) {
|
startDestination = TalariaScreen.Login.name,
|
||||||
TalariaScreen.Login.name
|
|
||||||
} else {
|
|
||||||
TalariaScreen.Dialog.name
|
|
||||||
},
|
|
||||||
Modifier.padding(innerPadding)
|
Modifier.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
composable(route = TalariaScreen.Dialog.name) {
|
composable(route = TalariaScreen.Dialog.name) {
|
||||||
DialogScreen(onDialogSelected = {
|
DialogScreen(onDialogSelected = {
|
||||||
selectedDialog = it
|
selectedDialog = it
|
||||||
chatViewModel.loadMessages(it)
|
|
||||||
navController.navigate(TalariaScreen.Chat.name)
|
navController.navigate(TalariaScreen.Chat.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
composable(route = TalariaScreen.Chat.name) {
|
composable(route = TalariaScreen.Chat.name) {
|
||||||
ChatScreen(selectedDialog, chatViewModel = chatViewModel)
|
ChatScreen(selectedDialog)
|
||||||
}
|
}
|
||||||
composable(route = TalariaScreen.Login.name) {
|
composable(route = TalariaScreen.Login.name) {
|
||||||
LoginScreen(onConfirmOtp = {
|
LoginScreen(onConfirmOtp = {
|
||||||
|
@ -211,23 +79,3 @@ fun TalariaApp(modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun TopBarPreview() {
|
|
||||||
TalariaAppBar(
|
|
||||||
currentScreen = TalariaScreen.Dialog,
|
|
||||||
canNavigateBack = false,
|
|
||||||
navigateUp = {},
|
|
||||||
openDrawer = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun NavDrawerPreview() {
|
|
||||||
Surface {
|
|
||||||
Drawer(onSelect = {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -1,56 +0,0 @@
|
||||||
package dev.lonami.talaria.data
|
|
||||||
|
|
||||||
import uniffi.talaria.Dialog
|
|
||||||
import uniffi.talaria.MessageAck
|
|
||||||
import uniffi.talaria.MessagePreview
|
|
||||||
import uniffi.talaria.getDialogs
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
interface DialogRepository {
|
|
||||||
fun loadDialogs(): List<Dialog>
|
|
||||||
}
|
|
||||||
|
|
||||||
class NativeDialogRepository : DialogRepository {
|
|
||||||
override fun loadDialogs(): List<Dialog> {
|
|
||||||
return getDialogs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MockDialogRepository : DialogRepository {
|
|
||||||
override fun loadDialogs(): List<Dialog> {
|
|
||||||
val dialogs = mutableListOf<Dialog>()
|
|
||||||
for (i in 0 until 10) {
|
|
||||||
dialogs.add(
|
|
||||||
Dialog(
|
|
||||||
id = "$i",
|
|
||||||
title = "Sample Dialog $i",
|
|
||||||
lastMessage = if (i % 4 == 3) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
MessagePreview(
|
|
||||||
sender = if (i % 2 == 0) {
|
|
||||||
"Sender A"
|
|
||||||
} else {
|
|
||||||
"Sender B"
|
|
||||||
},
|
|
||||||
text = if (i % 3 == 2) {
|
|
||||||
"Very Long Sample Message $i, with a Lot of Text, which makes it hard to Preview"
|
|
||||||
} else {
|
|
||||||
"Sample Message $i"
|
|
||||||
},
|
|
||||||
date = Instant.now(),
|
|
||||||
ack = when (i % 3) {
|
|
||||||
0 -> MessageAck.RECEIVED
|
|
||||||
1 -> MessageAck.SENT
|
|
||||||
2 -> MessageAck.SEEN
|
|
||||||
else -> throw RuntimeException()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
pinned = i < 4
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return dialogs
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package dev.lonami.talaria.data
|
||||||
|
|
||||||
|
import dev.lonami.talaria.bindings.Native
|
||||||
|
import dev.lonami.talaria.model.Dialog
|
||||||
|
|
||||||
|
object DialogSource {
|
||||||
|
fun loadDialogs(): List<Dialog> {
|
||||||
|
val dialogs = mutableListOf<Dialog>()
|
||||||
|
|
||||||
|
val dialogPtr = Native.getDialogs()
|
||||||
|
val dialogCount = Native.dialogCount(dialogPtr)
|
||||||
|
for (i in 0 until dialogCount) {
|
||||||
|
dialogs.add(
|
||||||
|
Dialog(
|
||||||
|
Native.dialogTitle(dialogPtr, i),
|
||||||
|
Native.dialogPacked(dialogPtr, i),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Native.freeDialogs(dialogPtr)
|
||||||
|
|
||||||
|
return dialogs
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,46 +0,0 @@
|
||||||
package dev.lonami.talaria.data
|
|
||||||
|
|
||||||
import uniffi.talaria.Message
|
|
||||||
import uniffi.talaria.getMessages
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
interface MessageRepository {
|
|
||||||
fun loadMessages(chat: String): List<Message>
|
|
||||||
fun sendMessage(chat: String, message: String): Message
|
|
||||||
}
|
|
||||||
|
|
||||||
class NativeMessageRepository : MessageRepository {
|
|
||||||
override fun loadMessages(chat: String): List<Message> {
|
|
||||||
return getMessages(chat)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sendMessage(chat: String, message: String): Message {
|
|
||||||
return uniffi.talaria.sendMessage(chat, message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MockMessageRepository(private var msgCounter: Int = 50) : MessageRepository {
|
|
||||||
override fun loadMessages(chat: String): List<Message> {
|
|
||||||
return (0 until 50).map {
|
|
||||||
Message(
|
|
||||||
id = it,
|
|
||||||
sender = "Alice",
|
|
||||||
text = "Testing",
|
|
||||||
date = Instant.now(),
|
|
||||||
editDate = null,
|
|
||||||
formatting = listOf(),
|
|
||||||
)
|
|
||||||
}.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sendMessage(chat: String, message: String): Message {
|
|
||||||
return Message(
|
|
||||||
id = msgCounter++,
|
|
||||||
sender = "You",
|
|
||||||
text = message,
|
|
||||||
date = Instant.now(),
|
|
||||||
editDate = null,
|
|
||||||
formatting = listOf(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package dev.lonami.talaria.data
|
||||||
|
|
||||||
|
import dev.lonami.talaria.model.Message
|
||||||
|
|
||||||
|
object MessageSource {
|
||||||
|
fun loadMessages(): List<Message> {
|
||||||
|
return generateSequence {
|
||||||
|
Message("Alice", "Testing")
|
||||||
|
}.take(50).toList()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package dev.lonami.talaria.model
|
||||||
|
|
||||||
|
data class Dialog(val title: String, val lastMessage: String, val pinned: Boolean)
|
|
@ -0,0 +1,3 @@
|
||||||
|
package dev.lonami.talaria.model
|
||||||
|
|
||||||
|
data class Message(val sender: String, val text: String)
|
|
@ -0,0 +1,110 @@
|
||||||
|
package dev.lonami.talaria.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextField
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import dev.lonami.talaria.R
|
||||||
|
import dev.lonami.talaria.model.Message
|
||||||
|
import dev.lonami.talaria.ui.theme.TalariaTheme
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessageCard(message: Message) {
|
||||||
|
Card(
|
||||||
|
elevation = 4.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Text(message.sender, fontWeight = FontWeight.Bold)
|
||||||
|
Text(message.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessageList(messages: List<Message>, modifier: Modifier, listState: LazyListState) {
|
||||||
|
LazyColumn(modifier, state = listState) {
|
||||||
|
items(messages.size) { MessageCard(messages[it]) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MessageInputField(
|
||||||
|
messageText: String,
|
||||||
|
onMessageChanged: (String) -> Unit,
|
||||||
|
onSendMessage: () -> Unit
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
TextField(
|
||||||
|
messageText,
|
||||||
|
placeholder = { Text(stringResource(R.string.write_message)) },
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
imeAction = ImeAction.Send
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = { onSendMessage() }
|
||||||
|
),
|
||||||
|
modifier = Modifier.weight(1.0f),
|
||||||
|
onValueChange = onMessageChanged
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = onSendMessage
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.send_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChatScreen(selectedDialog: String, chatViewModel: ChatViewModel = viewModel()) {
|
||||||
|
val chatUiState by chatViewModel.uiState.collectAsState()
|
||||||
|
var messageText by remember { mutableStateOf("") }
|
||||||
|
val messageListState = rememberLazyListState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MessageList(
|
||||||
|
chatUiState.messages,
|
||||||
|
modifier = Modifier.weight(1.0f),
|
||||||
|
listState = messageListState
|
||||||
|
)
|
||||||
|
MessageInputField(messageText, onMessageChanged = {
|
||||||
|
messageText = it
|
||||||
|
}, onSendMessage = {
|
||||||
|
chatViewModel.sendMessage(selectedDialog, messageText)
|
||||||
|
messageText = ""
|
||||||
|
coroutineScope.launch {
|
||||||
|
messageListState.animateScrollToItem(chatUiState.messages.size - 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ChatPreview() {
|
||||||
|
TalariaTheme {
|
||||||
|
ChatScreen("")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
package dev.lonami.talaria.ui.state
|
package dev.lonami.talaria.ui
|
||||||
|
|
||||||
import uniffi.talaria.Message
|
import dev.lonami.talaria.model.Message
|
||||||
|
|
||||||
data class ChatUiState(val messages: MutableList<Message> = mutableListOf())
|
data class ChatUiState(val messages: MutableList<Message> = mutableListOf())
|
|
@ -0,0 +1,31 @@
|
||||||
|
package dev.lonami.talaria.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dev.lonami.talaria.bindings.Native
|
||||||
|
import dev.lonami.talaria.data.MessageSource
|
||||||
|
import dev.lonami.talaria.model.Message
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
class ChatViewModel : ViewModel() {
|
||||||
|
private val _uiState = MutableStateFlow(ChatUiState())
|
||||||
|
val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private fun loadMessages() {
|
||||||
|
_uiState.value = ChatUiState(MessageSource.loadMessages().toMutableList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendMessage(dialog: String, message: String) {
|
||||||
|
Native.sendMessage(dialog, message)
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.messages.add(Message("You", message))
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadMessages()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package dev.lonami.talaria.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.Switch
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import dev.lonami.talaria.R
|
||||||
|
import dev.lonami.talaria.model.Dialog
|
||||||
|
import dev.lonami.talaria.ui.theme.TalariaTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogCard(dialog: Dialog, onDialogSelected: () -> Unit) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp, 16.dp)
|
||||||
|
.clickable(onClick = onDialogSelected)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||||
|
contentDescription = stringResource(R.string.profile_photo),
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.weight(1.0f)) {
|
||||||
|
Text(dialog.title, fontWeight = FontWeight.Bold)
|
||||||
|
Text(dialog.lastMessage)
|
||||||
|
}
|
||||||
|
Switch(dialog.pinned, enabled = false, onCheckedChange = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogList(dialogs: List<Dialog>, onDialogSelected: (String) -> Unit) {
|
||||||
|
LazyColumn {
|
||||||
|
items(dialogs.size) {
|
||||||
|
DialogCard(dialogs[it], onDialogSelected = {
|
||||||
|
onDialogSelected(dialogs[it].lastMessage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogScreen(
|
||||||
|
onDialogSelected: (String) -> Unit,
|
||||||
|
dialogViewModel: DialogViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val dialogUiState by dialogViewModel.uiState.collectAsState()
|
||||||
|
DialogList(dialogUiState.dialogs, onDialogSelected = onDialogSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DialogPreview() {
|
||||||
|
TalariaTheme {
|
||||||
|
DialogScreen(onDialogSelected = { })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package dev.lonami.talaria.ui
|
||||||
|
|
||||||
|
import dev.lonami.talaria.model.Dialog
|
||||||
|
|
||||||
|
data class DialogUiState(val dialogs: List<Dialog> = listOf())
|
|
@ -1,19 +1,17 @@
|
||||||
package dev.lonami.talaria.ui.state
|
package dev.lonami.talaria.ui
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import dev.lonami.talaria.data.DialogRepository
|
import dev.lonami.talaria.data.DialogSource
|
||||||
import dev.lonami.talaria.data.NativeDialogRepository
|
|
||||||
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
|
||||||
|
|
||||||
class DialogViewModel(private val repository: DialogRepository = NativeDialogRepository()) :
|
class DialogViewModel : ViewModel() {
|
||||||
ViewModel() {
|
|
||||||
private val _uiState = MutableStateFlow(DialogUiState())
|
private val _uiState = MutableStateFlow(DialogUiState())
|
||||||
val uiState: StateFlow<DialogUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<DialogUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private fun loadDialogs() {
|
private fun loadDialogs() {
|
||||||
_uiState.value = DialogUiState(repository.loadDialogs())
|
_uiState.value = DialogUiState(DialogSource.loadDialogs())
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.lonami.talaria.ui.screens
|
package dev.lonami.talaria.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
@ -18,8 +18,8 @@ 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
|
||||||
import uniffi.talaria.LoginProcedure
|
|
||||||
|
|
||||||
enum class LoginStage {
|
enum class LoginStage {
|
||||||
ASK_PHONE,
|
ASK_PHONE,
|
||||||
|
@ -30,15 +30,9 @@ fun isPhoneValid(phone: String): Boolean = phone.trim('+', ' ').isNotEmpty()
|
||||||
fun isLoginCodeValid(code: String): Boolean = code.trim().count { it.isDigit() } == 5
|
fun isLoginCodeValid(code: String): Boolean = code.trim().count { it.isDigit() } == 5
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PhoneInput(
|
fun PhoneInput(phone: String, onPhoneChanged: (String) -> Unit, onSendCode: () -> Unit) {
|
||||||
phone: String,
|
|
||||||
onPhoneChanged: (String) -> Unit,
|
|
||||||
onSendCode: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
Text(stringResource(R.string.enter_phone))
|
Text(stringResource(R.string.enter_phone))
|
||||||
TextField(
|
TextField(
|
||||||
phone,
|
phone,
|
||||||
|
@ -62,18 +56,11 @@ fun PhoneInput(
|
||||||
Text(stringResource(R.string.send_otp))
|
Text(stringResource(R.string.send_otp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OtpInput(
|
fun OtpInput(otp: String, onOtpChanged: (String) -> Unit, onConfirmOtp: () -> Unit) {
|
||||||
otp: String,
|
|
||||||
onOtpChanged: (String) -> Unit,
|
|
||||||
onConfirmOtp: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
Text(stringResource(R.string.enter_otp))
|
Text(stringResource(R.string.enter_otp))
|
||||||
TextField(
|
TextField(
|
||||||
otp,
|
otp,
|
||||||
|
@ -97,18 +84,17 @@ fun OtpInput(
|
||||||
Text(stringResource(R.string.do_login))
|
Text(stringResource(R.string.do_login))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
|
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("") }
|
||||||
|
|
||||||
val loginProcedure by remember { mutableStateOf(LoginProcedure()) }
|
var tokenPtr by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
|
@ -127,7 +113,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
phone,
|
phone,
|
||||||
onPhoneChanged = { phone = it },
|
onPhoneChanged = { phone = it },
|
||||||
onSendCode = {
|
onSendCode = {
|
||||||
loginProcedure.requestLoginCode(phone)
|
tokenPtr = Native.requestLoginCode(phone)
|
||||||
stage = LoginStage.ASK_CODE
|
stage = LoginStage.ASK_CODE
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -135,7 +121,7 @@ fun LoginScreen(onConfirmOtp: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
otp,
|
otp,
|
||||||
onOtpChanged = { otp = it },
|
onOtpChanged = { otp = it },
|
||||||
onConfirmOtp = {
|
onConfirmOtp = {
|
||||||
loginProcedure.signIn(otp)
|
Native.signIn(tokenPtr, otp)
|
||||||
onConfirmOtp()
|
onConfirmOtp()
|
||||||
}
|
}
|
||||||
)
|
)
|
|
@ -1,181 +0,0 @@
|
||||||
package dev.lonami.talaria.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.Card
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.TextField
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import dev.lonami.talaria.R
|
|
||||||
import dev.lonami.talaria.data.MockMessageRepository
|
|
||||||
import dev.lonami.talaria.ui.state.ChatViewModel
|
|
||||||
import dev.lonami.talaria.ui.theme.TalariaTheme
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import uniffi.talaria.Formatting
|
|
||||||
import uniffi.talaria.Message
|
|
||||||
import uniffi.talaria.TextFormat
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FormattedText(
|
|
||||||
text: String,
|
|
||||||
formatting: List<TextFormat>,
|
|
||||||
onFormatClicked: (TextFormat) -> Unit,
|
|
||||||
) {
|
|
||||||
val anno = AnnotatedString(text, spanStyles = formatting.map {
|
|
||||||
AnnotatedString.Range(SpanStyle(
|
|
||||||
fontWeight = when (it.format) {
|
|
||||||
Formatting.BOLD -> FontWeight.Bold
|
|
||||||
else -> FontWeight.Normal
|
|
||||||
},
|
|
||||||
fontStyle = when (it.format) {
|
|
||||||
Formatting.ITALIC -> FontStyle.Italic
|
|
||||||
else -> FontStyle.Normal
|
|
||||||
},
|
|
||||||
fontFamily = when (it.format) {
|
|
||||||
Formatting.CODE,
|
|
||||||
Formatting.PRE,
|
|
||||||
-> FontFamily.Monospace
|
|
||||||
else -> FontFamily.Default
|
|
||||||
},
|
|
||||||
textDecoration = when (it.format) {
|
|
||||||
Formatting.UNDERLINE -> TextDecoration.Underline
|
|
||||||
Formatting.STRIKE -> TextDecoration.LineThrough
|
|
||||||
else -> TextDecoration.None
|
|
||||||
},
|
|
||||||
color = when (it.format) {
|
|
||||||
Formatting.MENTION,
|
|
||||||
Formatting.HASH_TAG,
|
|
||||||
Formatting.BOT_COMMAND,
|
|
||||||
Formatting.URL,
|
|
||||||
Formatting.EMAIL,
|
|
||||||
Formatting.TEXT_URL,
|
|
||||||
Formatting.MENTION_NAME,
|
|
||||||
Formatting.PHONE,
|
|
||||||
Formatting.CASH_TAG,
|
|
||||||
-> Color(0xff0000ff)
|
|
||||||
else -> Color.Black
|
|
||||||
}), it.offset, it.offset + it.length)
|
|
||||||
})
|
|
||||||
ClickableText(
|
|
||||||
anno,
|
|
||||||
onClick = { offset ->
|
|
||||||
anno.spanStyles.indexOfFirst {
|
|
||||||
it.start <= offset && offset <= it.end
|
|
||||||
}.takeIf { it != -1 }?.also {
|
|
||||||
onFormatClicked(formatting[it])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageCard(message: Message, modifier: Modifier = Modifier) {
|
|
||||||
Card(
|
|
||||||
elevation = 4.dp,
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(message.sender, fontWeight = FontWeight.Bold)
|
|
||||||
FormattedText(message.text, message.formatting, onFormatClicked = {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageList(messages: List<Message>, listState: LazyListState, modifier: Modifier = Modifier) {
|
|
||||||
LazyColumn(modifier = modifier, state = listState) {
|
|
||||||
items(messages.size) { MessageCard(messages[it]) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageInputField(
|
|
||||||
messageText: String,
|
|
||||||
onMessageChanged: (String) -> Unit,
|
|
||||||
onSendMessage: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Row(modifier = modifier) {
|
|
||||||
TextField(
|
|
||||||
messageText,
|
|
||||||
placeholder = { Text(stringResource(R.string.write_message)) },
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
imeAction = ImeAction.Send
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
onDone = { onSendMessage() }
|
|
||||||
),
|
|
||||||
modifier = Modifier.weight(1.0f),
|
|
||||||
onValueChange = onMessageChanged
|
|
||||||
)
|
|
||||||
Button(
|
|
||||||
onClick = onSendMessage
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.send_message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ChatScreen(
|
|
||||||
selectedDialog: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
chatViewModel: ChatViewModel = viewModel(),
|
|
||||||
) {
|
|
||||||
val chatUiState by chatViewModel.uiState.collectAsState()
|
|
||||||
var messageText by remember { mutableStateOf("") }
|
|
||||||
val messageListState = rememberLazyListState()
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxSize()) {
|
|
||||||
MessageList(
|
|
||||||
chatUiState.messages,
|
|
||||||
modifier = Modifier.weight(1.0f),
|
|
||||||
listState = messageListState
|
|
||||||
)
|
|
||||||
MessageInputField(messageText, onMessageChanged = {
|
|
||||||
messageText = it
|
|
||||||
}, onSendMessage = {
|
|
||||||
chatViewModel.sendMessage(selectedDialog, messageText)
|
|
||||||
messageText = ""
|
|
||||||
coroutineScope.launch {
|
|
||||||
messageListState.animateScrollToItem(chatUiState.messages.size - 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun ChatPreview() {
|
|
||||||
val viewModel = remember { ChatViewModel(MockMessageRepository()).apply { loadMessages("") } }
|
|
||||||
|
|
||||||
TalariaTheme {
|
|
||||||
ChatScreen("", chatViewModel = viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
package dev.lonami.talaria.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.material.Divider
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.Surface
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import dev.lonami.talaria.R
|
|
||||||
import dev.lonami.talaria.data.MockDialogRepository
|
|
||||||
import dev.lonami.talaria.ui.state.DialogViewModel
|
|
||||||
import dev.lonami.talaria.ui.theme.TalariaTheme
|
|
||||||
import uniffi.talaria.Dialog
|
|
||||||
import uniffi.talaria.MessageAck
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.time.format.FormatStyle
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Dialog(dialog: Dialog, onDialogSelected: () -> Unit, modifier: Modifier = Modifier) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.clickable(onClick = onDialogSelected)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ic_launcher_foreground),
|
|
||||||
contentDescription = stringResource(R.string.profile_photo),
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
modifier = Modifier.height(48.dp),
|
|
||||||
)
|
|
||||||
Column(modifier = Modifier.weight(1.0f)) {
|
|
||||||
Text(
|
|
||||||
dialog.title,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
if (dialog.lastMessage != null) {
|
|
||||||
Text(
|
|
||||||
stringResource(
|
|
||||||
R.string.message_preview,
|
|
||||||
dialog.lastMessage!!.sender,
|
|
||||||
dialog.lastMessage!!.text
|
|
||||||
),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column {
|
|
||||||
if (dialog.lastMessage != null) {
|
|
||||||
Row {
|
|
||||||
when (dialog.lastMessage!!.ack) {
|
|
||||||
MessageAck.RECEIVED -> {}
|
|
||||||
MessageAck.SENT -> Icon(
|
|
||||||
painterResource(R.drawable.sent),
|
|
||||||
stringResource(R.string.sent)
|
|
||||||
)
|
|
||||||
MessageAck.SEEN -> Icon(
|
|
||||||
painterResource(R.drawable.seen),
|
|
||||||
stringResource(R.string.seen)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
LocalDateTime.ofInstant(dialog.lastMessage!!.date, ZoneOffset.UTC).format(
|
|
||||||
DateTimeFormatter.ofLocalizedTime(
|
|
||||||
FormatStyle.SHORT
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dialog.pinned) {
|
|
||||||
Icon(
|
|
||||||
painterResource(R.drawable.tack),
|
|
||||||
stringResource(R.string.pinned),
|
|
||||||
modifier = Modifier.align(Alignment.End)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DialogList(
|
|
||||||
dialogs: List<Dialog>,
|
|
||||||
onDialogSelected: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
LazyColumn(modifier = modifier) {
|
|
||||||
items(dialogs.size) {
|
|
||||||
Dialog(dialogs[it], onDialogSelected = {
|
|
||||||
onDialogSelected(dialogs[it].id)
|
|
||||||
})
|
|
||||||
Divider(startIndent = 52.dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DialogScreen(
|
|
||||||
onDialogSelected: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
dialogViewModel: DialogViewModel = viewModel(),
|
|
||||||
) {
|
|
||||||
val dialogUiState by dialogViewModel.uiState.collectAsState()
|
|
||||||
Surface(modifier = modifier) {
|
|
||||||
DialogList(dialogUiState.dialogs, onDialogSelected = onDialogSelected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun DialogPreview() {
|
|
||||||
val viewModel = DialogViewModel(MockDialogRepository())
|
|
||||||
TalariaTheme {
|
|
||||||
DialogScreen(onDialogSelected = { }, dialogViewModel = viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package dev.lonami.talaria.ui.state
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import dev.lonami.talaria.data.MessageRepository
|
|
||||||
import dev.lonami.talaria.data.NativeMessageRepository
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
|
|
||||||
class ChatViewModel(private val repository: MessageRepository = NativeMessageRepository()) :
|
|
||||||
ViewModel() {
|
|
||||||
private val _uiState = MutableStateFlow(ChatUiState())
|
|
||||||
val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
fun loadMessages(chat: String) {
|
|
||||||
_uiState.update {
|
|
||||||
it.messages.clear()
|
|
||||||
it.messages.addAll(repository.loadMessages(chat))
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendMessage(dialog: String, message: String) {
|
|
||||||
val sent = repository.sendMessage(dialog, message)
|
|
||||||
_uiState.update {
|
|
||||||
it.messages.add(sent)
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package dev.lonami.talaria.ui.state
|
|
||||||
|
|
||||||
import uniffi.talaria.Dialog
|
|
||||||
|
|
||||||
data class DialogUiState(val dialogs: List<Dialog> = listOf())
|
|
|
@ -16,6 +16,15 @@ private val LightColorPalette = lightColors(
|
||||||
primary = Purple500,
|
primary = Purple500,
|
||||||
primaryVariant = Purple700,
|
primaryVariant = Purple700,
|
||||||
secondary = Teal200
|
secondary = Teal200
|
||||||
|
|
||||||
|
/* Other default colors to override
|
||||||
|
background = Color.White,
|
||||||
|
surface = Color.White,
|
||||||
|
onPrimary = Color.White,
|
||||||
|
onSecondary = Color.Black,
|
||||||
|
onBackground = Color.Black,
|
||||||
|
onSurface = Color.Black,
|
||||||
|
*/
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
@ -6,10 +6,23 @@ import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
// Set of Material typography styles to start with
|
||||||
val Typography = Typography(
|
val Typography = Typography(
|
||||||
body1 = TextStyle(
|
body1 = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
)
|
)
|
||||||
|
/* Other default text styles to override
|
||||||
|
button = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.W500,
|
||||||
|
fontSize = 14.sp
|
||||||
|
),
|
||||||
|
caption = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
*/
|
||||||
)
|
)
|
|
@ -1,12 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:pathData="m11,16 l2,2 9,-9C23,8 22,7 21,8l-8,8 -1,-1z"
|
|
||||||
android:fillColor="@color/black" />
|
|
||||||
<path
|
|
||||||
android:pathData="m3,14 l4,4 9,-9C17,8 16,7 15,8L7,16 4,13C3,12 2,13 3,14Z"
|
|
||||||
android:fillColor="@color/black" />
|
|
||||||
</vector>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:pathData="m6,14 l4,4 9,-9C20,8 19,7 18,8L10,16 7,13C6,12 5,13 6,14Z"
|
|
||||||
android:fillColor="@color/black" />
|
|
||||||
</vector>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:pathData="m14,4 l-5,5 -6,1 5,5 -5,7 7,-5 5,5 1,-6 5,-5c1,-1 1,-1 0,-2L16,4C15,3 15,3 14,4Z"
|
|
||||||
android:fillColor="@color/black" />
|
|
||||||
</vector>
|
|
|
@ -15,8 +15,4 @@
|
||||||
<string name="back_button">Back</string>
|
<string name="back_button">Back</string>
|
||||||
<string name="dialog">All chats</string>
|
<string name="dialog">All chats</string>
|
||||||
<string name="chat">Messages</string>
|
<string name="chat">Messages</string>
|
||||||
<string name="pinned">Pinned</string>
|
|
||||||
<string name="sent">Sent</string>
|
|
||||||
<string name="seen">Seen</string>
|
|
||||||
<string name="message_preview"><em>%s</em>: %s</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -4,10 +4,8 @@ buildscript {
|
||||||
}
|
}
|
||||||
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
// uniffi-recommended Gradle script fails with Android plugin 7.3.1.
|
id 'com.android.application' version '7.3.0' apply false
|
||||||
// See https://github.com/mozilla/uniffi-rs/issues/1386 for details.
|
id 'com.android.library' version '7.3.0' apply false
|
||||||
id 'com.android.application' version '7.2.2' apply false
|
|
||||||
id 'com.android.library' version '7.2.2' 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"
|
id "org.mozilla.rust-android-gradle.rust-android" version "0.9.3"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,24 +4,19 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "uniffi_talaria"
|
name = "talaria"
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jni = { version = "0.10.2", default-features = false }
|
jni = { version = "0.10.2", default-features = false }
|
||||||
uniffi = "0.21.0"
|
|
||||||
# v0.4 of grammers-* is currently unreleased; clone the project and use path dependencies
|
# v0.4 of grammers-* is currently unreleased; clone the project and use path dependencies
|
||||||
grammers-client = { version = "0.4.1" }
|
grammers-client = { version = "0.4.0" }
|
||||||
grammers-tl-types = { version = "0.4.0" }
|
grammers-tl-types = { version = "0.4.0" }
|
||||||
grammers-session = { version = "0.4.1" }
|
grammers-session = { version = "0.4.0" }
|
||||||
tokio = { version = "1.5.0", features = ["full"] }
|
tokio = { version = "1.5.0", features = ["full"] }
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
android_logger = "0.11.1"
|
android_logger = "0.11.1"
|
||||||
once_cell = "1.15.0"
|
once_cell = "1.15.0"
|
||||||
sqlite = "0.27.0"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
uniffi_build = "0.21.0"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
fn main() {
|
|
||||||
uniffi_build::generate_scaffolding("src/talaria.udl").unwrap();
|
|
||||||
}
|
|
|
@ -1,172 +0,0 @@
|
||||||
mod model;
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
use model::Session;
|
|
||||||
use sqlite::{Connection, Error, State};
|
|
||||||
use std::net::IpAddr;
|
|
||||||
use utils::{fetch_many, fetch_one};
|
|
||||||
|
|
||||||
fn init_schema(conn: &Connection) -> Result<(), Error> {
|
|
||||||
let version = match fetch_one(&conn, "SELECT version FROM version LIMIT 1", |stmt| {
|
|
||||||
stmt.read::<i64>(0)
|
|
||||||
}) {
|
|
||||||
Ok(Some(version)) => version,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if version == 0 {
|
|
||||||
conn.execute(
|
|
||||||
"
|
|
||||||
BEGIN TRANSACTION;
|
|
||||||
CREATE TABLE version (version INTEGER NOT NULL);
|
|
||||||
CREATE TABLE session (
|
|
||||||
user_id INTEGER,
|
|
||||||
dc_id INTEGER,
|
|
||||||
bot INTEGER,
|
|
||||||
pts INTEGER,
|
|
||||||
qts INTEGER,
|
|
||||||
seq INTEGER,
|
|
||||||
date INTEGER
|
|
||||||
);
|
|
||||||
CREATE TABLE channel (
|
|
||||||
session_id INTEGER NOT NULL REFERENCES session (rowid),
|
|
||||||
id INTEGER NOT NULL,
|
|
||||||
hash INTEGER NOT NULL,
|
|
||||||
pts INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
CREATE TABLE datacenter (
|
|
||||||
session_id INTEGER NOT NULL REFERENCES session (rowid),
|
|
||||||
id INTEGER NOT NULL,
|
|
||||||
ipv4 TEXT,
|
|
||||||
ipv6 TEXT,
|
|
||||||
port INTEGER NOT NULL,
|
|
||||||
auth BLOB,
|
|
||||||
CONSTRAINT SingleIp CHECK(
|
|
||||||
(ipv4 IS NOT NULL AND ipv6 IS NULL) OR
|
|
||||||
(ipv6 IS NOT NULL AND ipv4 IS NULL))
|
|
||||||
);
|
|
||||||
INSERT INTO version VALUES (1);
|
|
||||||
COMMIT;
|
|
||||||
",
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_connection(db_path: &str) -> Result<Connection, Error> {
|
|
||||||
let conn = sqlite::open(db_path)?;
|
|
||||||
init_schema(&conn)?;
|
|
||||||
Ok(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_sessions(conn: &Connection) -> Result<Vec<Session>, Error> {
|
|
||||||
let query = "
|
|
||||||
SELECT s.rowid, s.*, COALESCE(d.ipv4, d.ipv6), d.port, d.auth
|
|
||||||
FROM session s
|
|
||||||
LEFT JOIN datacenter d ON d.session_id = s.rowid AND d.id = s.dc_id
|
|
||||||
";
|
|
||||||
fetch_many(conn, query, |stmt| {
|
|
||||||
Ok(Session {
|
|
||||||
id: stmt.read(0)?,
|
|
||||||
user_id: stmt.read(1)?,
|
|
||||||
dc_id: stmt.read::<Option<i64>>(2)?.map(|x| x as _),
|
|
||||||
bot: stmt.read::<Option<i64>>(3)?.map(|x| x != 0),
|
|
||||||
pts: stmt.read::<Option<i64>>(4)?.map(|x| x as _),
|
|
||||||
qts: stmt.read::<Option<i64>>(5)?.map(|x| x as _),
|
|
||||||
seq: stmt.read::<Option<i64>>(6)?.map(|x| x as _),
|
|
||||||
date: stmt.read::<Option<i64>>(7)?.map(|x| x as _),
|
|
||||||
dc_addr: stmt.read::<Option<String>>(8)?,
|
|
||||||
dc_port: stmt.read::<Option<i64>>(9)?.map(|x| x as _),
|
|
||||||
dc_auth: stmt
|
|
||||||
.read::<Option<Vec<u8>>>(10)?
|
|
||||||
.map(|x| x.try_into().unwrap()),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_session(conn: &Connection) -> Result<Session, Error> {
|
|
||||||
conn.execute("INSERT INTO session DEFAULT VALUES;")?;
|
|
||||||
let id = fetch_one(conn, "SELECT LAST_INSERT_ROWID()", |stmt| {
|
|
||||||
stmt.read::<i64>(0)
|
|
||||||
})?
|
|
||||||
.unwrap();
|
|
||||||
Ok(Session {
|
|
||||||
id,
|
|
||||||
user_id: None,
|
|
||||||
dc_id: None,
|
|
||||||
bot: None,
|
|
||||||
pts: None,
|
|
||||||
qts: None,
|
|
||||||
seq: None,
|
|
||||||
date: None,
|
|
||||||
dc_addr: None,
|
|
||||||
dc_port: None,
|
|
||||||
dc_auth: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_session(conn: &Connection, session: &Session) -> Result<(), Error> {
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare(
|
|
||||||
"
|
|
||||||
UPDATE session SET
|
|
||||||
user_id = ?,
|
|
||||||
dc_id = ?,
|
|
||||||
bot = ?,
|
|
||||||
pts = ?,
|
|
||||||
qts = ?,
|
|
||||||
seq = ?,
|
|
||||||
date = ?
|
|
||||||
WHERE rowid = ?
|
|
||||||
",
|
|
||||||
)?
|
|
||||||
.bind(1, session.user_id)?
|
|
||||||
.bind(2, session.dc_id.map(|x| x as i64))?
|
|
||||||
.bind(3, session.bot.map(|x| x as i64))?
|
|
||||||
.bind(4, session.pts.map(|x| x as i64))?
|
|
||||||
.bind(5, session.qts.map(|x| x as i64))?
|
|
||||||
.bind(6, session.seq.map(|x| x as i64))?
|
|
||||||
.bind(7, session.date.map(|x| x as i64))?
|
|
||||||
.bind(8, session.id)?;
|
|
||||||
while let State::Row = stmt.next()? {}
|
|
||||||
|
|
||||||
match (
|
|
||||||
session.dc_id,
|
|
||||||
session.dc_addr.as_ref(),
|
|
||||||
session.dc_port,
|
|
||||||
session.dc_auth,
|
|
||||||
) {
|
|
||||||
(Some(id), Some(addr), Some(port), Some(auth)) => {
|
|
||||||
let (ipv4, ipv6) = match addr.parse().unwrap() {
|
|
||||||
IpAddr::V4(ipv4) => (Some(ipv4.to_string()), None),
|
|
||||||
IpAddr::V6(ipv6) => (None, Some(ipv6.to_string())),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare(
|
|
||||||
"
|
|
||||||
DELETE FROM datacenter WHERE session_id = ? AND id = ?
|
|
||||||
",
|
|
||||||
)?
|
|
||||||
.bind(1, session.id)?
|
|
||||||
.bind(2, id as i64)?;
|
|
||||||
|
|
||||||
while let State::Row = stmt.next()? {}
|
|
||||||
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare("INSERT INTO datacenter VALUES (?, ?, ?, ?, ?, ?)")?
|
|
||||||
.bind(1, session.id)?
|
|
||||||
.bind(2, id as i64)?
|
|
||||||
.bind(3, ipv4.as_deref())?
|
|
||||||
.bind(4, ipv6.as_deref())?
|
|
||||||
.bind(5, port as i64)?
|
|
||||||
.bind(6, auth.as_ref())?;
|
|
||||||
|
|
||||||
while let State::Row = stmt.next()? {}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Session {
|
|
||||||
pub id: i64,
|
|
||||||
pub user_id: Option<i64>,
|
|
||||||
pub dc_id: Option<i32>,
|
|
||||||
pub bot: Option<bool>,
|
|
||||||
pub pts: Option<i32>,
|
|
||||||
pub qts: Option<i32>,
|
|
||||||
pub seq: Option<i32>,
|
|
||||||
pub date: Option<i32>,
|
|
||||||
pub dc_addr: Option<String>,
|
|
||||||
pub dc_port: Option<u16>,
|
|
||||||
pub dc_auth: Option<[u8; 256]>,
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
use sqlite::{Connection, Error, State, Statement};
|
|
||||||
|
|
||||||
pub fn fetch_one<T, F: FnOnce(&Statement) -> Result<T, Error>>(
|
|
||||||
conn: &Connection,
|
|
||||||
query: &str,
|
|
||||||
adaptor: F,
|
|
||||||
) -> Result<Option<T>, Error> {
|
|
||||||
let mut stmt = conn.prepare(query)?;
|
|
||||||
if let State::Row = stmt.next()? {
|
|
||||||
adaptor(&stmt).map(Some)
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_many<T, F: FnMut(&Statement) -> Result<T, Error>>(
|
|
||||||
conn: &Connection,
|
|
||||||
query: &str,
|
|
||||||
mut adaptor: F,
|
|
||||||
) -> Result<Vec<T>, Error> {
|
|
||||||
let mut result = Vec::new();
|
|
||||||
let mut stmt = conn.prepare(query)?;
|
|
||||||
while let State::Row = stmt.next()? {
|
|
||||||
result.push(adaptor(&stmt)?);
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
|
@ -1,139 +1,30 @@
|
||||||
#![cfg(target_os = "android")]
|
#![cfg(target_os = "android")]
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
mod db;
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
use grammers_client::types::{LoginToken, Message as OgMessage};
|
use grammers_client::{Client, Config};
|
||||||
use grammers_client::{Client, Config, InitParams};
|
use grammers_client::types::{Dialog, LoginToken};
|
||||||
use grammers_session::{PackedChat, Session, UpdateState};
|
use grammers_session::{PackedChat, Session};
|
||||||
use grammers_tl_types as tl;
|
use jni::JNIEnv;
|
||||||
|
use jni::objects::{JObject, JString};
|
||||||
|
use jni::sys::{jboolean, jint, jlong, jstring};
|
||||||
use log;
|
use log;
|
||||||
use log::{error, info, Level};
|
use log::{error, info, Level};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::time::SystemTime;
|
|
||||||
use tokio::runtime;
|
use tokio::runtime;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/talaria.uniffi.rs"));
|
|
||||||
|
|
||||||
const LOG_MIN_LEVEL: Level = Level::Trace;
|
const LOG_MIN_LEVEL: Level = Level::Trace;
|
||||||
const LOG_TAG: &str = ".native.talari";
|
const LOG_TAG: &str = ".native.talari";
|
||||||
const API_ID: i32 = {
|
const API_ID: i32 = 0;
|
||||||
let mut index = 0;
|
const API_HASH: &str = "";
|
||||||
let mut value = 0;
|
|
||||||
let api_id = env!("TALARIA_API_ID");
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
let bytes = api_id.as_bytes();
|
|
||||||
while index < bytes.len() {
|
|
||||||
match bytes[index] {
|
|
||||||
b @ b'0'..=b'9' => value = value * 10 + (b - b'0') as i32,
|
|
||||||
_ => panic!("non-digit character found in API ID"),
|
|
||||||
}
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
value
|
|
||||||
};
|
|
||||||
const API_HASH: &str = env!("TALARIA_API_HASH");
|
|
||||||
const SERVER_ADDR: &str = env!("TALARIA_SERVER_ADDR");
|
|
||||||
|
|
||||||
static RUNTIME: OnceCell<Runtime> = OnceCell::new();
|
static RUNTIME: OnceCell<Runtime> = OnceCell::new();
|
||||||
static CLIENT: OnceCell<Client> = OnceCell::new();
|
static CLIENT: OnceCell<Client> = OnceCell::new();
|
||||||
static DATABASE: Mutex<Option<sqlite::Connection>> = Mutex::new(None);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum NativeError {
|
|
||||||
Initialization,
|
|
||||||
Database,
|
|
||||||
Network,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for NativeError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Initialization => write!(f, "a resource could not be initialized correctly"),
|
|
||||||
Self::Database => write!(f, "a query to the local database failed"),
|
|
||||||
Self::Network => write!(f, "a network request could not complete successfully"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for NativeError {}
|
|
||||||
|
|
||||||
struct LoginProcedure {
|
|
||||||
token: Mutex<Option<LoginToken>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum MessageAck {
|
|
||||||
Received,
|
|
||||||
Seen,
|
|
||||||
Sent,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum Formatting {
|
|
||||||
Unknown,
|
|
||||||
Mention,
|
|
||||||
HashTag,
|
|
||||||
BotCommand,
|
|
||||||
Url,
|
|
||||||
Email,
|
|
||||||
Bold,
|
|
||||||
Italic,
|
|
||||||
Code,
|
|
||||||
Pre,
|
|
||||||
TextUrl,
|
|
||||||
MentionName,
|
|
||||||
Phone,
|
|
||||||
CashTag,
|
|
||||||
Underline,
|
|
||||||
Strike,
|
|
||||||
Blockquote,
|
|
||||||
BankCard,
|
|
||||||
Spoiler,
|
|
||||||
CustomEmoji,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TextFormat {
|
|
||||||
format: Formatting,
|
|
||||||
offset: i32,
|
|
||||||
length: i32,
|
|
||||||
extra: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Message {
|
|
||||||
id: i32,
|
|
||||||
sender: String,
|
|
||||||
text: String,
|
|
||||||
date: SystemTime,
|
|
||||||
edit_date: Option<SystemTime>,
|
|
||||||
formatting: Vec<TextFormat>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MessagePreview {
|
|
||||||
sender: String,
|
|
||||||
text: String,
|
|
||||||
date: SystemTime,
|
|
||||||
ack: MessageAck,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Dialog {
|
|
||||||
id: String,
|
|
||||||
title: String,
|
|
||||||
last_message: Option<MessagePreview>,
|
|
||||||
pinned: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, NativeError>;
|
|
||||||
|
|
||||||
fn block_on<F: Future>(future: F) -> F::Output {
|
fn block_on<F: Future>(future: F) -> F::Output {
|
||||||
if RUNTIME.get().is_none() {
|
if RUNTIME.get().is_none() {
|
||||||
|
@ -150,22 +41,7 @@ fn block_on<F: Future>(future: F) -> F::Output {
|
||||||
RUNTIME.get().unwrap().block_on(future)
|
RUNTIME.get().unwrap().block_on(future)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_database(path: String) -> Result<()> {
|
async fn init_client() -> Result<()> {
|
||||||
let mut guard = DATABASE.lock().unwrap();
|
|
||||||
if guard.is_some() {
|
|
||||||
info!("Database is already initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
match db::init_connection(&path) {
|
|
||||||
Ok(conn) => {
|
|
||||||
*guard = Some(conn);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(_) => Err(NativeError::Database),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_client() -> Result<()> {
|
|
||||||
android_logger::init_once(
|
android_logger::init_once(
|
||||||
android_logger::Config::default()
|
android_logger::Config::default()
|
||||||
.with_min_level(LOG_MIN_LEVEL)
|
.with_min_level(LOG_MIN_LEVEL)
|
||||||
|
@ -177,275 +53,188 @@ pub fn init_client() -> Result<()> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let guard = DATABASE.lock().unwrap();
|
|
||||||
let conn = match guard.as_ref() {
|
|
||||||
Some(c) => c,
|
|
||||||
None => {
|
|
||||||
error!("Database was not initialized");
|
|
||||||
return Err(NativeError::Initialization);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("Connecting to Telegram...");
|
info!("Connecting to Telegram...");
|
||||||
|
|
||||||
let session = Session::new();
|
let client = Client::connect(Config {
|
||||||
|
session: Session::new(),
|
||||||
let sessions = db::get_sessions(conn).map_err(|_| NativeError::Database)?;
|
|
||||||
if let Some(s) = sessions.get(0) {
|
|
||||||
match (s.user_id, s.dc_id, s.bot) {
|
|
||||||
(Some(id), Some(dc), Some(bot)) => session.set_user(id, dc, bot),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
match (s.pts, s.qts, s.seq, s.date) {
|
|
||||||
(Some(pts), Some(qts), Some(seq), Some(date)) => session.set_state(UpdateState {
|
|
||||||
pts,
|
|
||||||
qts,
|
|
||||||
seq,
|
|
||||||
date,
|
|
||||||
channels: HashMap::new(),
|
|
||||||
}),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
match (s.dc_id, s.dc_addr.as_ref(), s.dc_port, s.dc_auth) {
|
|
||||||
(Some(id), Some(addr), Some(port), Some(auth)) => {
|
|
||||||
session.insert_dc(id, SocketAddr::new(addr.parse().unwrap(), port), auth)
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = block_on(Client::connect(Config {
|
|
||||||
session,
|
|
||||||
api_id: API_ID,
|
api_id: API_ID,
|
||||||
api_hash: API_HASH.to_string(),
|
api_hash: API_HASH.to_string(),
|
||||||
params: InitParams {
|
params: Default::default(),
|
||||||
server_addr: if SERVER_ADDR.is_empty() {
|
})
|
||||||
None
|
.await?;
|
||||||
} else {
|
|
||||||
Some(SERVER_ADDR.parse().unwrap())
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.map_err(|_| NativeError::Network)?;
|
|
||||||
|
|
||||||
info!("Connected!");
|
info!("Connected!");
|
||||||
|
|
||||||
CLIENT
|
CLIENT
|
||||||
.set(client)
|
.set(client)
|
||||||
.map_err(|_| NativeError::Initialization)?;
|
.map_err(|_| "Client was already initialized")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn need_login() -> Result<bool> {
|
async fn need_login() -> Result<bool> {
|
||||||
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
|
let client = CLIENT.get().ok_or("Client not initialized")?;
|
||||||
block_on(client.is_authorized()).map_err(|_| NativeError::Network)
|
Ok(client.is_authorized().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoginProcedure {
|
async fn request_login_code(phone: &str) -> Result<LoginToken> {
|
||||||
fn new() -> Self {
|
let client = CLIENT.get().ok_or("Client not initialized")?;
|
||||||
Self {
|
let token = client.request_login_code(&phone, API_ID, API_HASH).await?;
|
||||||
token: Mutex::new(None),
|
Ok(token)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_login_code(&self, phone: String) -> Result<()> {
|
async fn sign_in(token: LoginToken, code: &str) -> Result<()> {
|
||||||
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
|
let client = CLIENT.get().ok_or("Client not initialized")?;
|
||||||
let token = block_on(client.request_login_code(&phone, API_ID, API_HASH))
|
client.sign_in(&token, &code).await?;
|
||||||
.map_err(|_| NativeError::Network)?;
|
|
||||||
*self.token.lock().unwrap() = Some(token);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_in(&self, code: String) -> Result<()> {
|
async fn get_dialogs() -> Result<Vec<Dialog>> {
|
||||||
let token = self
|
let client = CLIENT.get().ok_or("Client not initialized")?;
|
||||||
.token
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.take()
|
|
||||||
.ok_or(NativeError::Initialization)?;
|
|
||||||
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
|
|
||||||
|
|
||||||
block_on(client.sign_in(&token, &code)).map_err(|_| NativeError::Network)?;
|
|
||||||
|
|
||||||
let guard = DATABASE.lock().unwrap();
|
|
||||||
let conn = match guard.as_ref() {
|
|
||||||
Some(c) => c,
|
|
||||||
None => {
|
|
||||||
error!("Database was not initialized");
|
|
||||||
return Err(NativeError::Initialization);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut session = db::create_session(conn).map_err(|_| NativeError::Database)?;
|
|
||||||
let s = client.session();
|
|
||||||
if let Some(user) = s.get_user() {
|
|
||||||
session.user_id = Some(user.id);
|
|
||||||
session.dc_id = Some(user.dc);
|
|
||||||
session.bot = Some(user.bot);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(state) = s.get_state() {
|
|
||||||
session.pts = Some(state.pts);
|
|
||||||
session.qts = Some(state.qts);
|
|
||||||
session.seq = Some(state.seq);
|
|
||||||
session.date = Some(state.date);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(dc_id) = session.dc_id {
|
|
||||||
for dc in s.get_dcs() {
|
|
||||||
if dc.id == dc_id {
|
|
||||||
if let Some(ipv4) = dc.ipv4 {
|
|
||||||
session.dc_addr = Some(Ipv4Addr::from(ipv4.to_le_bytes()).to_string())
|
|
||||||
} else if let Some(ipv6) = dc.ipv6 {
|
|
||||||
session.dc_addr = Some(Ipv6Addr::from(ipv6).to_string())
|
|
||||||
}
|
|
||||||
session.dc_port = Some(dc.port as u16);
|
|
||||||
session.dc_auth = dc.auth.map(|b| b.try_into().unwrap());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
db::update_session(conn, &session).map_err(|_| NativeError::Database)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dialogs() -> Result<Vec<Dialog>> {
|
|
||||||
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
|
|
||||||
|
|
||||||
block_on(async {
|
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut dialogs = client.iter_dialogs();
|
let mut dialogs = client.iter_dialogs();
|
||||||
while let Some(dialog) = dialogs.next().await.map_err(|_| NativeError::Network)? {
|
while let Some(dialog) = dialogs.next().await? {
|
||||||
result.push(dialog);
|
result.push(dialog);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
})
|
|
||||||
.map(|dialogs| {
|
|
||||||
dialogs
|
|
||||||
.into_iter()
|
|
||||||
.map(|d| Dialog {
|
|
||||||
id: d.chat().pack().to_hex(),
|
|
||||||
title: d.chat().name().to_string(),
|
|
||||||
last_message: d.last_message.map(|m| MessagePreview {
|
|
||||||
sender: if let Some(sender) = m.sender() {
|
|
||||||
sender.name().to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
},
|
|
||||||
text: m.text().to_string(),
|
|
||||||
date: m.date().into(),
|
|
||||||
ack: if m.outgoing() {
|
|
||||||
match &d.dialog {
|
|
||||||
tl::enums::Dialog::Dialog(d) => {
|
|
||||||
if m.id() <= d.read_inbox_max_id {
|
|
||||||
MessageAck::Seen
|
|
||||||
} else {
|
|
||||||
MessageAck::Sent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tl::enums::Dialog::Folder(_) => MessageAck::Received,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MessageAck::Received
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
pinned: match d.dialog {
|
|
||||||
tl::enums::Dialog::Dialog(d) => d.pinned,
|
|
||||||
tl::enums::Dialog::Folder(f) => f.pinned,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn adapt_message(m: OgMessage) -> Message {
|
async fn send_message(chat: PackedChat, text: &str) -> Result<()> {
|
||||||
Message {
|
let client = CLIENT.get().ok_or("Client not initialized")?;
|
||||||
id: m.id(),
|
client.send_message(chat, text).await?;
|
||||||
sender: if let Some(sender) = m.sender() {
|
Ok(())
|
||||||
sender.name().to_string()
|
|
||||||
} else {
|
|
||||||
"unknown".to_string()
|
|
||||||
},
|
|
||||||
text: m.text().to_string(),
|
|
||||||
date: m.date().into(),
|
|
||||||
edit_date: m.edit_date().map(|d| d.into()),
|
|
||||||
formatting: if let Some(entities) = m.fmt_entities() {
|
|
||||||
use tl::enums::MessageEntity as ME;
|
|
||||||
|
|
||||||
macro_rules! tf {
|
|
||||||
($formatting:ident($entity:ident)) => {
|
|
||||||
tf!($formatting($entity).extra(None))
|
|
||||||
};
|
|
||||||
($formatting:ident($entity:ident).extra($extra:expr)) => {
|
|
||||||
TextFormat {
|
|
||||||
format: Formatting::$formatting,
|
|
||||||
offset: $entity.offset,
|
|
||||||
length: $entity.length,
|
|
||||||
extra: $extra,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entities
|
#[no_mangle]
|
||||||
.into_iter()
|
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_initClient(_: JNIEnv, _: JObject) {
|
||||||
.map(|e| match e {
|
match block_on(init_client()) {
|
||||||
ME::Unknown(e) => tf!(Unknown(e)),
|
Ok(_) => info!("Client initialized"),
|
||||||
ME::Mention(e) => tf!(Mention(e)),
|
Err(e) => error!("Failed to initialize client: {}", e),
|
||||||
ME::Hashtag(e) => tf!(HashTag(e)),
|
|
||||||
ME::BotCommand(e) => tf!(BotCommand(e)),
|
|
||||||
ME::Url(e) => tf!(Url(e)),
|
|
||||||
ME::Email(e) => tf!(Email(e)),
|
|
||||||
ME::Bold(e) => tf!(Bold(e)),
|
|
||||||
ME::Italic(e) => tf!(Italic(e)),
|
|
||||||
ME::Code(e) => tf!(Code(e)),
|
|
||||||
ME::Pre(e) => tf!(Pre(e).extra(Some(e.language.to_string()))),
|
|
||||||
ME::TextUrl(e) => tf!(TextUrl(e).extra(Some(e.url.to_string()))),
|
|
||||||
ME::MentionName(e) => tf!(MentionName(e).extra(Some(e.user_id.to_string()))),
|
|
||||||
ME::InputMessageEntityMentionName(e) => tf!(Unknown(e)),
|
|
||||||
ME::Phone(e) => tf!(Phone(e)),
|
|
||||||
ME::Cashtag(e) => tf!(CashTag(e)),
|
|
||||||
ME::Underline(e) => tf!(Underline(e)),
|
|
||||||
ME::Strike(e) => tf!(Strike(e)),
|
|
||||||
ME::Blockquote(e) => tf!(Blockquote(e)),
|
|
||||||
ME::BankCard(e) => tf!(BankCard(e)),
|
|
||||||
ME::Spoiler(e) => tf!(Spoiler(e)),
|
|
||||||
ME::CustomEmoji(e) => {
|
|
||||||
tf!(CustomEmoji(e).extra(Some(e.document_id.to_string())))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_messages(packed: String) -> Result<Vec<Message>> {
|
#[no_mangle]
|
||||||
let chat = PackedChat::from_hex(&packed).unwrap();
|
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_needLogin(
|
||||||
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
|
_: JNIEnv,
|
||||||
|
_: JObject,
|
||||||
block_on(async {
|
) -> jboolean {
|
||||||
let mut result = Vec::new();
|
match block_on(need_login()) {
|
||||||
let mut messages = client.iter_messages(chat);
|
Ok(login) => login.into(),
|
||||||
while let Some(message) = messages.next().await.map_err(|_| NativeError::Network)? {
|
Err(e) => {
|
||||||
result.push(message);
|
error!("Failed to check if user is authorized: {}", e);
|
||||||
|
false.into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(result)
|
|
||||||
})
|
|
||||||
.map(|messages| messages.into_iter().map(adapt_message).collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_message(packed: String, text: String) -> Result<Message> {
|
#[no_mangle]
|
||||||
let chat = PackedChat::from_hex(&packed).unwrap();
|
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_requestLoginCode(
|
||||||
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
|
env: JNIEnv,
|
||||||
block_on(client.send_message(chat, text))
|
_: JObject,
|
||||||
.map(adapt_message)
|
phone: JString,
|
||||||
.map_err(|_| NativeError::Network)
|
) -> 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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
[Error]
|
|
||||||
enum NativeError {
|
|
||||||
"Initialization",
|
|
||||||
"Database",
|
|
||||||
"Network",
|
|
||||||
};
|
|
||||||
|
|
||||||
enum MessageAck {
|
|
||||||
"Received",
|
|
||||||
"Seen",
|
|
||||||
"Sent",
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Formatting {
|
|
||||||
"Unknown",
|
|
||||||
"Mention",
|
|
||||||
"HashTag",
|
|
||||||
"BotCommand",
|
|
||||||
"Url",
|
|
||||||
"Email",
|
|
||||||
"Bold",
|
|
||||||
"Italic",
|
|
||||||
"Code",
|
|
||||||
"Pre", // language:string
|
|
||||||
"TextUrl", // url:string
|
|
||||||
"MentionName", // user_id:long
|
|
||||||
"Phone",
|
|
||||||
"CashTag",
|
|
||||||
"Underline",
|
|
||||||
"Strike",
|
|
||||||
"Blockquote",
|
|
||||||
"BankCard",
|
|
||||||
"Spoiler",
|
|
||||||
"CustomEmoji", // document_id:long
|
|
||||||
};
|
|
||||||
|
|
||||||
dictionary TextFormat {
|
|
||||||
Formatting format;
|
|
||||||
i32 offset;
|
|
||||||
i32 length;
|
|
||||||
string? extra;
|
|
||||||
};
|
|
||||||
|
|
||||||
dictionary Message {
|
|
||||||
i32 id;
|
|
||||||
string sender;
|
|
||||||
string text;
|
|
||||||
timestamp date;
|
|
||||||
timestamp? edit_date;
|
|
||||||
sequence<TextFormat> formatting;
|
|
||||||
};
|
|
||||||
|
|
||||||
dictionary MessagePreview {
|
|
||||||
string sender;
|
|
||||||
string text;
|
|
||||||
timestamp date;
|
|
||||||
MessageAck ack;
|
|
||||||
};
|
|
||||||
|
|
||||||
dictionary Dialog {
|
|
||||||
string id;
|
|
||||||
string title;
|
|
||||||
MessagePreview? last_message;
|
|
||||||
boolean pinned;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LoginProcedure {
|
|
||||||
constructor();
|
|
||||||
[Throws=NativeError]
|
|
||||||
void request_login_code(string phone);
|
|
||||||
[Throws=NativeError]
|
|
||||||
void sign_in(string code);
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace talaria {
|
|
||||||
[Throws=NativeError]
|
|
||||||
void init_database(string path);
|
|
||||||
[Throws=NativeError]
|
|
||||||
void init_client();
|
|
||||||
[Throws=NativeError]
|
|
||||||
boolean need_login();
|
|
||||||
[Throws=NativeError]
|
|
||||||
sequence<Dialog> get_dialogs();
|
|
||||||
[Throws=NativeError]
|
|
||||||
sequence<Message> get_messages(string packed);
|
|
||||||
[Throws=NativeError]
|
|
||||||
Message send_message(string packed, string text);
|
|
||||||
};
|
|
Loading…
Reference in New Issue