Compare commits
No commits in common. "49fa3c4d5a8c734f245f2c35d4746ff4db404560" and "43421f6e5441635728cefd6041707dc3b9c220f0" have entirely different histories.
49fa3c4d5a
...
43421f6e54
|
@ -21,7 +21,6 @@ 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
|
||||||
|
|
||||||
|
@ -164,8 +163,6 @@ fun TalariaApp(modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val chatViewModel = remember { ChatViewModel() }
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
|
@ -196,12 +193,11 @@ 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, chatViewModel = chatViewModel)
|
ChatScreen(selectedDialog)
|
||||||
}
|
}
|
||||||
composable(route = TalariaScreen.Login.name) {
|
composable(route = TalariaScreen.Login.name) {
|
||||||
LoginScreen(onConfirmOtp = {
|
LoginScreen(onConfirmOtp = {
|
||||||
|
|
|
@ -1,46 +1,11 @@
|
||||||
package dev.lonami.talaria.data
|
package dev.lonami.talaria.data
|
||||||
|
|
||||||
import uniffi.talaria.Message
|
import dev.lonami.talaria.models.Message
|
||||||
import uniffi.talaria.getMessages
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
interface MessageRepository {
|
object MessageRepository {
|
||||||
fun loadMessages(chat: String): List<Message>
|
fun loadMessages(): List<Message> {
|
||||||
fun sendMessage(chat: String, message: String): Message
|
return generateSequence {
|
||||||
}
|
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(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
package dev.lonami.talaria.models
|
||||||
|
|
||||||
|
data class Message(val sender: String, val text: String)
|
|
@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.text.ClickableText
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
|
@ -13,79 +12,17 @@ import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TextField
|
import androidx.compose.material.TextField
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
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.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.data.MockMessageRepository
|
import dev.lonami.talaria.models.Message
|
||||||
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.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
|
@Composable
|
||||||
fun MessageCard(message: Message, modifier: Modifier = Modifier) {
|
fun MessageCard(message: Message, modifier: Modifier = Modifier) {
|
||||||
|
@ -101,7 +38,7 @@ fun MessageCard(message: Message, modifier: Modifier = Modifier) {
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
Text(message.sender, fontWeight = FontWeight.Bold)
|
Text(message.sender, fontWeight = FontWeight.Bold)
|
||||||
FormattedText(message.text, message.formatting, onFormatClicked = {})
|
Text(message.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,9 +110,7 @@ fun ChatScreen(
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatPreview() {
|
fun ChatPreview() {
|
||||||
val viewModel = remember { ChatViewModel(MockMessageRepository()).apply { loadMessages("") } }
|
|
||||||
|
|
||||||
TalariaTheme {
|
TalariaTheme {
|
||||||
ChatScreen("", chatViewModel = viewModel)
|
ChatScreen("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
package dev.lonami.talaria.ui.state
|
package dev.lonami.talaria.ui.state
|
||||||
|
|
||||||
import uniffi.talaria.Message
|
import dev.lonami.talaria.models.Message
|
||||||
|
|
||||||
data class ChatUiState(val messages: MutableList<Message> = mutableListOf())
|
data class ChatUiState(val messages: MutableList<Message> = mutableListOf())
|
||||||
|
|
|
@ -2,30 +2,29 @@ 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.data.NativeMessageRepository
|
import dev.lonami.talaria.models.Message
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
class ChatViewModel(private val repository: MessageRepository = NativeMessageRepository()) :
|
class ChatViewModel : ViewModel() {
|
||||||
ViewModel() {
|
|
||||||
private val _uiState = MutableStateFlow(ChatUiState())
|
private val _uiState = MutableStateFlow(ChatUiState())
|
||||||
val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadMessages(chat: String) {
|
private fun loadMessages() {
|
||||||
_uiState.update {
|
_uiState.value = ChatUiState(MessageRepository.loadMessages().toMutableList())
|
||||||
it.messages.clear()
|
|
||||||
it.messages.addAll(repository.loadMessages(chat))
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendMessage(dialog: String, message: String) {
|
fun sendMessage(dialog: String, message: String) {
|
||||||
val sent = repository.sendMessage(dialog, message)
|
sendMessage(dialog, message)
|
||||||
_uiState.update {
|
_uiState.update { state ->
|
||||||
it.messages.add(sent)
|
state.messages.add(Message("You", message))
|
||||||
it
|
state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadMessages()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
mod db;
|
mod db;
|
||||||
|
|
||||||
use grammers_client::types::{LoginToken, Message as OgMessage};
|
use grammers_client::types::LoginToken;
|
||||||
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,48 +75,6 @@ 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,
|
||||||
|
@ -365,87 +323,9 @@ pub fn get_dialogs() -> Result<Vec<Dialog>> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn adapt_message(m: OgMessage) -> Message {
|
pub fn send_message(packed: String, text: String) -> Result<()> {
|
||||||
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)?;
|
||||||
block_on(async {
|
Ok(())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,45 +11,6 @@ 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;
|
||||||
|
@ -82,7 +43,5 @@ namespace talaria {
|
||||||
[Throws=NativeError]
|
[Throws=NativeError]
|
||||||
sequence<Dialog> get_dialogs();
|
sequence<Dialog> get_dialogs();
|
||||||
[Throws=NativeError]
|
[Throws=NativeError]
|
||||||
sequence<Message> get_messages(string packed);
|
void send_message(string packed, string text);
|
||||||
[Throws=NativeError]
|
|
||||||
Message send_message(string packed, string text);
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue