Load messages from API

master
Lonami Exo 2022-10-29 13:06:38 +02:00
parent 43421f6e54
commit d350d1a048
8 changed files with 232 additions and 31 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.ChatScreen
import dev.lonami.talaria.ui.screens.DialogScreen import dev.lonami.talaria.ui.screens.DialogScreen
import dev.lonami.talaria.ui.screens.LoginScreen import dev.lonami.talaria.ui.screens.LoginScreen
import dev.lonami.talaria.ui.state.ChatViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uniffi.talaria.needLogin import uniffi.talaria.needLogin
@ -163,6 +164,8 @@ fun TalariaApp(modifier: Modifier = Modifier) {
} }
} }
val chatViewModel = remember { ChatViewModel() }
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
topBar = { topBar = {
@ -193,11 +196,12 @@ fun TalariaApp(modifier: Modifier = Modifier) {
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) ChatScreen(selectedDialog, chatViewModel = chatViewModel)
} }
composable(route = TalariaScreen.Login.name) { composable(route = TalariaScreen.Login.name) {
LoginScreen(onConfirmOtp = { LoginScreen(onConfirmOtp = {

View File

@ -1,11 +1,46 @@
package dev.lonami.talaria.data 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 { interface MessageRepository {
fun loadMessages(): List<Message> { fun loadMessages(chat: String): List<Message>
return generateSequence { fun sendMessage(chat: String, message: String): Message
Message("Alice", "Testing") }
}.take(50).toList()
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

@ -19,10 +19,11 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import dev.lonami.talaria.R 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.state.ChatViewModel
import dev.lonami.talaria.ui.theme.TalariaTheme import dev.lonami.talaria.ui.theme.TalariaTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uniffi.talaria.Message
@Composable @Composable
fun MessageCard(message: Message, modifier: Modifier = Modifier) { fun MessageCard(message: Message, modifier: Modifier = Modifier) {
@ -110,7 +111,9 @@ fun ChatScreen(
@Preview @Preview
@Composable @Composable
fun ChatPreview() { fun ChatPreview() {
val viewModel = remember { ChatViewModel(MockMessageRepository()).apply { loadMessages("") } }
TalariaTheme { TalariaTheme {
ChatScreen("") ChatScreen("", chatViewModel = viewModel)
} }
} }

View File

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

View File

@ -3,7 +3,7 @@
mod db; mod db;
use grammers_client::types::LoginToken; use grammers_client::types::{LoginToken, Message as OgMessage};
use grammers_client::{Client, Config, InitParams}; use grammers_client::{Client, Config, InitParams};
use grammers_session::{PackedChat, Session, UpdateState}; use grammers_session::{PackedChat, Session, UpdateState};
use grammers_tl_types as tl; use grammers_tl_types as tl;
@ -75,6 +75,48 @@ pub enum MessageAck {
Sent, 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)] #[derive(Debug, Clone)]
pub struct MessagePreview { pub struct MessagePreview {
sender: String, 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 chat = PackedChat::from_hex(&packed).unwrap();
let client = CLIENT.get().ok_or(NativeError::Initialization)?; 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", "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 { dictionary MessagePreview {
string sender; string sender;
string text; string text;
@ -43,5 +82,7 @@ namespace talaria {
[Throws=NativeError] [Throws=NativeError]
sequence<Dialog> get_dialogs(); sequence<Dialog> get_dialogs();
[Throws=NativeError] [Throws=NativeError]
void send_message(string packed, string text); sequence<Message> get_messages(string packed);
[Throws=NativeError]
Message send_message(string packed, string text);
}; };