Compare commits

..

No commits in common. "07d0e5e505116435a8e29718b94bd4f7af64c802" and "189c1e8db8f243b754a3600f7b866c544a203c37" have entirely different histories.

26 changed files with 145 additions and 418 deletions

View File

@ -10,7 +10,7 @@ android {
defaultConfig {
applicationId "dev.lonami.talaria"
minSdk 26
minSdk 21
targetSdk 33
versionCode 1
versionName "0.1.0"

View File

@ -13,9 +13,9 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.ui.screens.ChatScreen
import dev.lonami.talaria.ui.screens.DialogScreen
import dev.lonami.talaria.ui.screens.LoginScreen
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),

View File

@ -14,11 +14,6 @@ object Native {
external fun dialogCount(dialogsPtr: Long): Int
external fun dialogPacked(dialogsPtr: Long, index: Int): String
external fun dialogTitle(dialogsPtr: Long, index: Int): String
external fun dialogSender(dialogsPtr: Long, index: Int): String
external fun dialogText(dialogsPtr: Long, index: Int): String
external fun dialogTime(dialogsPtr: Long, index: Int): String
external fun dialogAck(dialogsPtr: Long, index: Int): Int
external fun dialogPin(dialogsPtr: Long, index: Int): Int
external fun freeDialogs(dialogsPtr: Long)
external fun sendMessage(packed: String, text: String)
}

View File

@ -1,89 +0,0 @@
package dev.lonami.talaria.data
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.models.Dialog
import dev.lonami.talaria.models.MessageAck
import dev.lonami.talaria.models.MessagePreview
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
interface DialogRepository {
fun loadDialogs(): List<Dialog>;
}
class NativeDialogRepository : DialogRepository {
override fun loadDialogs(): List<Dialog> {
val dialogs = mutableListOf<Dialog>()
val dialogPtr = Native.getDialogs()
try {
val dialogCount = Native.dialogCount(dialogPtr)
for (i in 0 until dialogCount) {
dialogs.add(
Dialog(
id = Native.dialogPacked(dialogPtr, i),
title = Native.dialogTitle(dialogPtr, i),
lastMessage = MessagePreview(
sender = Native.dialogSender(dialogPtr, i),
text = Native.dialogText(dialogPtr, i),
date = LocalDateTime.parse(
Native.dialogTime(dialogPtr, i),
DateTimeFormatter.ISO_OFFSET_DATE_TIME
),
ack = when (Native.dialogAck(dialogPtr, i)) {
0 -> MessageAck.RECEIVED
1 -> MessageAck.SENT
2 -> MessageAck.SEEN
else -> MessageAck.RECEIVED
}
),
pinned = Native.dialogPin(dialogPtr, i) != 0
)
)
}
} finally {
Native.freeDialogs(dialogPtr)
}
return dialogs
}
}
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 = LocalDateTime.now(),
ack = when (i % 3) {
0 -> MessageAck.RECEIVED
1 -> MessageAck.SENT
2 -> MessageAck.SEEN
else -> throw RuntimeException()
}
)
},
pinned = i < 4
)
)
}
return dialogs
}
}

View File

@ -0,0 +1,39 @@
package dev.lonami.talaria.data
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.model.Dialog
interface DialogSource {
fun loadDialogs(): List<Dialog>;
}
class NativeDialogSource : DialogSource {
override 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
}
}
class MockDialogSource : DialogSource {
override fun loadDialogs(): List<Dialog> {
val dialogs = mutableListOf<Dialog>()
for (i in 0 until 10) {
dialogs.add(Dialog("Sample Dialog $i", "Sample Message $i", i == 0))
}
return dialogs
}
}

View File

@ -1,8 +1,8 @@
package dev.lonami.talaria.data
import dev.lonami.talaria.models.Message
import dev.lonami.talaria.model.Message
object MessageRepository {
object MessageSource {
fun loadMessages(): List<Message> {
return generateSequence {
Message("Alice", "Testing")

View File

@ -0,0 +1,3 @@
package dev.lonami.talaria.model
data class Dialog(val title: String, val lastMessage: String, val pinned: Boolean)

View File

@ -1,3 +1,3 @@
package dev.lonami.talaria.models
package dev.lonami.talaria.model
data class Message(val sender: String, val text: String)

View File

@ -1,8 +0,0 @@
package dev.lonami.talaria.models
data class Dialog(
val id: String,
val title: String,
val lastMessage: MessagePreview?,
val pinned: Boolean
)

View File

@ -1,7 +0,0 @@
package dev.lonami.talaria.models
enum class MessageAck {
RECEIVED,
SENT,
SEEN,
}

View File

@ -1,10 +0,0 @@
package dev.lonami.talaria.models
import java.time.LocalDateTime
data class MessagePreview(
val sender: String,
val text: String,
val date: LocalDateTime,
val ack: MessageAck
)

View File

@ -1,4 +1,4 @@
package dev.lonami.talaria.ui.screens
package dev.lonami.talaria.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@ -19,8 +19,7 @@ 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.models.Message
import dev.lonami.talaria.ui.state.ChatViewModel
import dev.lonami.talaria.model.Message
import dev.lonami.talaria.ui.theme.TalariaTheme
import kotlinx.coroutines.launch

View File

@ -1,5 +1,5 @@
package dev.lonami.talaria.ui.state
package dev.lonami.talaria.ui
import dev.lonami.talaria.models.Message
import dev.lonami.talaria.model.Message
data class ChatUiState(val messages: MutableList<Message> = mutableListOf())

View File

@ -1,9 +1,9 @@
package dev.lonami.talaria.ui.state
package dev.lonami.talaria.ui
import androidx.lifecycle.ViewModel
import dev.lonami.talaria.bindings.Native
import dev.lonami.talaria.data.MessageRepository
import dev.lonami.talaria.models.Message
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
@ -14,7 +14,7 @@ class ChatViewModel : ViewModel() {
val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow()
private fun loadMessages() {
_uiState.value = ChatUiState(MessageRepository.loadMessages().toMutableList())
_uiState.value = ChatUiState(MessageSource.loadMessages().toMutableList())
}
fun sendMessage(dialog: String, message: String) {

View File

@ -0,0 +1,77 @@
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.data.MockDialogSource
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() {
val viewModel = DialogViewModel(MockDialogSource())
TalariaTheme {
DialogScreen(onDialogSelected = { }, dialogViewModel = viewModel)
}
}

View File

@ -0,0 +1,5 @@
package dev.lonami.talaria.ui
import dev.lonami.talaria.model.Dialog
data class DialogUiState(val dialogs: List<Dialog> = listOf())

View File

@ -1,14 +1,13 @@
package dev.lonami.talaria.ui.state
package dev.lonami.talaria.ui
import androidx.lifecycle.ViewModel
import dev.lonami.talaria.data.DialogRepository
import dev.lonami.talaria.data.NativeDialogRepository
import dev.lonami.talaria.data.DialogSource
import dev.lonami.talaria.data.NativeDialogSource
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class DialogViewModel(private val repository: DialogRepository = NativeDialogRepository()) :
ViewModel() {
class DialogViewModel(private val repository: DialogSource = NativeDialogSource()) : ViewModel() {
private val _uiState = MutableStateFlow(DialogUiState())
val uiState: StateFlow<DialogUiState> = _uiState.asStateFlow()

View File

@ -1,4 +1,4 @@
package dev.lonami.talaria.ui.screens
package dev.lonami.talaria.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions

View File

@ -1,131 +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.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
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.models.Dialog
import dev.lonami.talaria.models.MessageAck
import dev.lonami.talaria.ui.state.DialogViewModel
import dev.lonami.talaria.ui.theme.TalariaTheme
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
@Composable
fun DialogCard(dialog: Dialog, onDialogSelected: () -> Unit) {
Card(
shape = MaterialTheme.shapes.large,
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 2.dp)
.clickable(onClick = onDialogSelected)
) {
Row(modifier = Modifier.padding(4.dp)) {
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(
dialog.lastMessage.date.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) {
LazyColumn {
items(dialogs.size) {
DialogCard(dialogs[it], onDialogSelected = {
onDialogSelected(dialogs[it].id)
})
}
}
}
@Composable
fun DialogScreen(
onDialogSelected: (String) -> Unit,
dialogViewModel: DialogViewModel = viewModel()
) {
val dialogUiState by dialogViewModel.uiState.collectAsState()
DialogList(dialogUiState.dialogs, onDialogSelected = onDialogSelected)
}
@Preview
@Composable
fun DialogPreview() {
val viewModel = DialogViewModel(MockDialogRepository())
TalariaTheme {
DialogScreen(onDialogSelected = { }, dialogViewModel = viewModel)
}
}

View File

@ -1,5 +0,0 @@
package dev.lonami.talaria.ui.state
import dev.lonami.talaria.models.Dialog
data class DialogUiState(val dialogs: List<Dialog> = listOf())

View File

@ -41,4 +41,4 @@ fun TalariaTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composabl
shapes = Shapes,
content = content
)
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -15,8 +15,4 @@
<string name="back_button">Back</string>
<string name="dialog">All chats</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>

View File

@ -6,7 +6,6 @@ mod db;
use grammers_client::types::{Dialog, LoginToken};
use grammers_client::{Client, Config, InitParams};
use grammers_session::{PackedChat, Session, UpdateState};
use grammers_tl_types as tl;
use jni::objects::{JObject, JString};
use jni::sys::{jboolean, jint, jlong, jstring};
use jni::JNIEnv;
@ -336,111 +335,6 @@ pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogTitle(
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogSender(
env: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jstring {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let sender = if let Some(msg) = dialogs[index as usize].last_message.as_ref() {
if let Some(sender) = msg.sender() {
sender.name().to_string()
} else {
"unknown".to_string()
}
} else {
String::new()
};
let output = env.new_string(sender).unwrap();
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogText(
env: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jstring {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let text = if let Some(msg) = dialogs[index as usize].last_message.as_ref() {
msg.text()
} else {
""
};
let output = env.new_string(text).unwrap();
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogTime(
env: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jstring {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let time = if let Some(msg) = dialogs[index as usize].last_message.as_ref() {
msg.date().to_rfc3339().to_string()
} else {
String::new()
};
let output = env.new_string(time).unwrap();
output.into_inner()
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogAck(
_: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jint {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let dialog = &dialogs[index as usize];
let ack = if let Some(msg) = dialog.last_message.as_ref() {
if msg.outgoing() {
match &dialog.dialog {
tl::enums::Dialog::Dialog(d) => {
if msg.id() <= d.read_inbox_max_id {
2
} else {
1
}
}
tl::enums::Dialog::Folder(_) => 0,
}
} else {
0
}
} else {
0
};
ack
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_dialogPin(
_: JNIEnv,
_: JObject,
dialogs_ptr: jlong,
index: jint,
) -> jint {
let dialogs = &mut *(dialogs_ptr as *mut Vec<Dialog>);
let pinned = match &dialogs[index as usize].dialog {
tl::enums::Dialog::Dialog(d) => d.pinned,
tl::enums::Dialog::Folder(f) => f.pinned,
};
pinned as jint
}
#[no_mangle]
pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_freeDialogs(
_: JNIEnv,