Compare commits

...

2 Commits

Author SHA1 Message Date
Lonami Exo 49fa3c4d5a Render formatted text in the ChatScreen 2022-10-29 13:35:05 +02:00
Lonami Exo d350d1a048 Load messages from API 2022-10-29 13:06:38 +02:00
8 changed files with 295 additions and 32 deletions

View File

@ -21,6 +21,7 @@ import androidx.navigation.compose.rememberNavController
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.state.ChatViewModel
import kotlinx.coroutines.launch
import uniffi.talaria.needLogin
@ -163,6 +164,8 @@ fun TalariaApp(modifier: Modifier = Modifier) {
}
}
val chatViewModel = remember { ChatViewModel() }
Scaffold(
modifier = modifier,
topBar = {
@ -193,11 +196,12 @@ fun TalariaApp(modifier: Modifier = Modifier) {
composable(route = TalariaScreen.Dialog.name) {
DialogScreen(onDialogSelected = {
selectedDialog = it
chatViewModel.loadMessages(it)
navController.navigate(TalariaScreen.Chat.name)
})
}
composable(route = TalariaScreen.Chat.name) {
ChatScreen(selectedDialog)
ChatScreen(selectedDialog, chatViewModel = chatViewModel)
}
composable(route = TalariaScreen.Login.name) {
LoginScreen(onConfirmOtp = {

View File

@ -1,11 +1,46 @@
package dev.lonami.talaria.data
import dev.lonami.talaria.models.Message
import uniffi.talaria.Message
import uniffi.talaria.getMessages
import java.time.Instant
object MessageRepository {
fun loadMessages(): List<Message> {
return generateSequence {
Message("Alice", "Testing")
}.take(50).toList()
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(),
)
}
}

View File

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

View File

@ -4,6 +4,7 @@ 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
@ -12,17 +13,79 @@ 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.models.Message
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) {
@ -38,7 +101,7 @@ fun MessageCard(message: Message, modifier: Modifier = Modifier) {
.padding(8.dp)
) {
Text(message.sender, fontWeight = FontWeight.Bold)
Text(message.text)
FormattedText(message.text, message.formatting, onFormatClicked = {})
}
}
}
@ -110,7 +173,9 @@ fun ChatScreen(
@Preview
@Composable
fun ChatPreview() {
val viewModel = remember { ChatViewModel(MockMessageRepository()).apply { loadMessages("") } }
TalariaTheme {
ChatScreen("")
ChatScreen("", chatViewModel = viewModel)
}
}

View File

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

View File

@ -2,29 +2,30 @@ package dev.lonami.talaria.ui.state
import androidx.lifecycle.ViewModel
import dev.lonami.talaria.data.MessageRepository
import dev.lonami.talaria.models.Message
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 : ViewModel() {
class ChatViewModel(private val repository: MessageRepository = NativeMessageRepository()) :
ViewModel() {
private val _uiState = MutableStateFlow(ChatUiState())
val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow()
private fun loadMessages() {
_uiState.value = ChatUiState(MessageRepository.loadMessages().toMutableList())
}
fun sendMessage(dialog: String, message: String) {
sendMessage(dialog, message)
_uiState.update { state ->
state.messages.add(Message("You", message))
state
fun loadMessages(chat: String) {
_uiState.update {
it.messages.clear()
it.messages.addAll(repository.loadMessages(chat))
it
}
}
init {
loadMessages()
fun sendMessage(dialog: String, message: String) {
val sent = repository.sendMessage(dialog, message)
_uiState.update {
it.messages.add(sent)
it
}
}
}

View File

@ -3,7 +3,7 @@
mod db;
use grammers_client::types::LoginToken;
use grammers_client::types::{LoginToken, Message as OgMessage};
use grammers_client::{Client, Config, InitParams};
use grammers_session::{PackedChat, Session, UpdateState};
use grammers_tl_types as tl;
@ -75,6 +75,48 @@ pub enum MessageAck {
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,
@ -323,9 +365,87 @@ pub fn get_dialogs() -> Result<Vec<Dialog>> {
})
}
pub fn send_message(packed: String, text: String) -> Result<()> {
fn adapt_message(m: OgMessage) -> Message {
Message {
id: m.id(),
sender: if let Some(sender) = m.sender() {
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
.into_iter()
.map(|e| match e {
ME::Unknown(e) => tf!(Unknown(e)),
ME::Mention(e) => tf!(Mention(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>> {
let chat = PackedChat::from_hex(&packed).unwrap();
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
block_on(client.send_message(chat, text)).map_err(|_| NativeError::Network)?;
Ok(())
block_on(async {
let mut result = Vec::new();
let mut messages = client.iter_messages(chat);
while let Some(message) = messages.next().await.map_err(|_| NativeError::Network)? {
result.push(message);
}
Ok(result)
})
.map(|messages| messages.into_iter().map(adapt_message).collect())
}
pub fn send_message(packed: String, text: String) -> Result<Message> {
let chat = PackedChat::from_hex(&packed).unwrap();
let client = CLIENT.get().ok_or(NativeError::Initialization)?;
block_on(client.send_message(chat, text))
.map(adapt_message)
.map_err(|_| NativeError::Network)
}

View File

@ -11,6 +11,45 @@ enum MessageAck {
"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;
@ -43,5 +82,7 @@ namespace talaria {
[Throws=NativeError]
sequence<Dialog> get_dialogs();
[Throws=NativeError]
void send_message(string packed, string text);
sequence<Message> get_messages(string packed);
[Throws=NativeError]
Message send_message(string packed, string text);
};