wip: put SimFragment between DeviceFragment and ConversationFragment
This commit is contained in:
@@ -89,10 +89,10 @@ import androidx.compose.ui.unit.dp
|
||||
import xyz.magicalbits.smsremote.FunctionalityNotAvailablePopup
|
||||
import xyz.magicalbits.smsremote.R
|
||||
import xyz.magicalbits.smsremote.components.JetchatAppBar
|
||||
import xyz.magicalbits.smsremote.data.exampleUiState
|
||||
import xyz.magicalbits.smsremote.theme.JetchatTheme
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.magicalbits.smsremote.data.exampleUiStateNew
|
||||
import xyz.magicalbits.smsremote.network.NetworkClient
|
||||
|
||||
/**
|
||||
* Entry point for a conversation screen.
|
||||
@@ -168,9 +168,8 @@ fun ConversationContent(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
ChannelNameBar(
|
||||
channelName = uiState.phoneNumber,
|
||||
// TODO remove?
|
||||
channelMembers = 2,
|
||||
channelName = uiState.remotePhoneNumber,
|
||||
channelMembers = 2, // TODO remove?
|
||||
onNavIconPressed = onNavIconPressed,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
@@ -205,6 +204,11 @@ fun ConversationContent(
|
||||
uiState.addMessage(
|
||||
Message(authorMe, content, timeNow),
|
||||
)
|
||||
scope.launch {
|
||||
val networkClient = NetworkClient()
|
||||
// FIXME
|
||||
// networkClient.sendSmsMessage(content, uiState.localPhoneNumber, uiState.remotePhoneNumber)
|
||||
}
|
||||
},
|
||||
resetScroll = {
|
||||
scope.launch {
|
||||
|
||||
+4
-4
@@ -9,13 +9,13 @@ import kotlinx.coroutines.launch
|
||||
import xyz.magicalbits.smsremote.network.NetworkClient
|
||||
|
||||
class ConversationViewModel : ViewModel() {
|
||||
private var phoneNumber: String = ""
|
||||
private var remotePhoneNumber: String = ""
|
||||
private val _conversationData = MutableLiveData<ConversationScreenState>()
|
||||
val conversationData: LiveData<ConversationScreenState> = _conversationData
|
||||
|
||||
fun setConversationData(phoneNumber: String?) {
|
||||
if (phoneNumber != null) {
|
||||
this.phoneNumber = phoneNumber
|
||||
this.remotePhoneNumber = phoneNumber
|
||||
|
||||
var messageDtoList: List<NetworkClient.SmsMessageDto> = listOf()
|
||||
viewModelScope.launch {
|
||||
@@ -24,7 +24,7 @@ class ConversationViewModel : ViewModel() {
|
||||
}.invokeOnCompletion {
|
||||
_conversationData.value =
|
||||
ConversationScreenState(
|
||||
phoneNumber = this.phoneNumber,
|
||||
remotePhoneNumber = this.remotePhoneNumber,
|
||||
initialMessages = messageDtoList.map {
|
||||
Message(
|
||||
if (it.msg_type == "INCOMING") {
|
||||
@@ -46,7 +46,7 @@ class ConversationViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
data class ConversationScreenState(
|
||||
val phoneNumber: String,
|
||||
val remotePhoneNumber: String,
|
||||
val initialMessages: List<Message>,
|
||||
) {
|
||||
private val _messages: MutableList<Message> = initialMessages.toMutableStateList()
|
||||
|
||||
@@ -35,7 +35,7 @@ val unreadMessages = initialMessages.filter { it.author != "me" }
|
||||
|
||||
val exampleUiStateNew =
|
||||
ConversationScreenState(
|
||||
phoneNumber = "",
|
||||
remotePhoneNumber = "",
|
||||
initialMessages = mutableListOf()
|
||||
)
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class DeviceFragment : Fragment() {
|
||||
onPhoneNumberClicked = {
|
||||
val args = Bundle(1)
|
||||
args.putString("phoneNumber", it)
|
||||
navController.navigate(R.id.action_device_to_conversation, args)
|
||||
navController.navigate(R.id.action_device_to_sim, args)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,14 @@ import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.auth.Auth
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
import io.ktor.client.plugins.auth.providers.bearer
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -20,6 +26,9 @@ class NetworkClient {
|
||||
// TODO ask user for creds, call login endpoint, store tokens in local encrypted storage
|
||||
|
||||
private val networkClient = HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
install(Auth) {
|
||||
bearer {
|
||||
// TODO configure refresh token so it refreshes the access token
|
||||
@@ -43,6 +52,12 @@ class NetworkClient {
|
||||
@Serializable
|
||||
data class SmsMessageDto(val content: String, val ts_received: Int, val ts_sent: Int, val msg_type: String, val local_phone_number: String, val remote_phone_number: String)
|
||||
|
||||
@Serializable
|
||||
data class OutgoingMessage(val content: String, val local_phone_number: String, val remote_phone_number: String)
|
||||
|
||||
@Serializable
|
||||
data class ConversationPreviewDto(val remote_phone_number: String, val last_message_content: String, val message_timestamp: Int)
|
||||
|
||||
// GET /api/v1/devices
|
||||
suspend fun getDevices(): List<DeviceDto> {
|
||||
// TODO handle non-200 status codes
|
||||
@@ -70,4 +85,24 @@ class NetworkClient {
|
||||
val data = networkClient.get("$apiBaseUrl/api/v1/sms-messages?local_phone_number=$encodedPhoneNumber").bodyAsText()
|
||||
return Json.decodeFromString(data)
|
||||
}
|
||||
|
||||
// POST /api/v1/send-message
|
||||
suspend fun sendSmsMessage(content: String, localPhoneNumber: String, remotePhoneNumber: String) {
|
||||
println("sending SMS message: content=$content, lPN=$localPhoneNumber, rPN=$remotePhoneNumber")
|
||||
val response = networkClient.post("$apiBaseUrl/api/v1/send-message") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(OutgoingMessage(content, localPhoneNumber, remotePhoneNumber))
|
||||
}
|
||||
println("sending SMS message: status code ${response.status}")
|
||||
}
|
||||
|
||||
// GET /api/v1/conversation-previews
|
||||
suspend fun getConversationPreviews(simPhoneNumber: String): List<ConversationPreviewDto> {
|
||||
// TODO extract encoder to a function here or to a utility class
|
||||
val encodedPhoneNumber = withContext(Dispatchers.IO) {
|
||||
URLEncoder.encode(simPhoneNumber, "UTF-8")
|
||||
}
|
||||
val response = networkClient.get("$apiBaseUrl/api/v1/conversation-previews?local_phone_number=$encodedPhoneNumber")
|
||||
return Json.decodeFromString(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package xyz.magicalbits.smsremote.sim
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import xyz.magicalbits.smsremote.R
|
||||
import xyz.magicalbits.smsremote.components.baselineHeight
|
||||
import xyz.magicalbits.smsremote.theme.JetchatTheme
|
||||
|
||||
@Composable
|
||||
fun SimScreen(
|
||||
simData: SimScreenState,
|
||||
nestedScrollInteropConnection: NestedScrollConnection = rememberNestedScrollInteropConnection(),
|
||||
onConversationClicked: (String) -> Unit = { },
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().nestedScroll(nestedScrollInteropConnection)
|
||||
) {
|
||||
Surface {
|
||||
Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(horizontal = 16.dp)) {
|
||||
Name(simData, Modifier.baselineHeight(32.dp))
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
simData.conversations.forEach {
|
||||
ConversationPreview(it, onConversationClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Name(
|
||||
simData: SimScreenState,
|
||||
modifier: Modifier
|
||||
) {
|
||||
Text(
|
||||
text = simData.phoneNumber,
|
||||
modifier = modifier,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConversationPreview(
|
||||
conversation: ConversationRowState,
|
||||
onConversationClicked: (String) -> Unit,
|
||||
) {
|
||||
Column(modifier = Modifier
|
||||
.clickable(
|
||||
onClick = {
|
||||
onConversationClicked(conversation.phoneNumber)
|
||||
}
|
||||
)
|
||||
) {
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.padding(start = 16.dp, end = 16.dp)
|
||||
) {
|
||||
Text(text = conversation.phoneNumber)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(text = conversation.messageTimestamp.toString())
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp)
|
||||
) {
|
||||
Text(text = conversation.lastMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SimScreenPreview() {
|
||||
JetchatTheme {
|
||||
SimScreen(
|
||||
simData = SimScreenState(
|
||||
phoneNumber = "+420123456789",
|
||||
conversations = listOf(
|
||||
ConversationRowState("+15558880000", "last msg", 12345),
|
||||
ConversationRowState("+15558880000", "last msg", 12345),
|
||||
ConversationRowState("+15558880000", "last msg", 12345),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimError() {
|
||||
Text(stringResource(R.string.sim_error))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package xyz.magicalbits.smsremote.sim
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import xyz.magicalbits.smsremote.MainViewModel
|
||||
import xyz.magicalbits.smsremote.R
|
||||
import xyz.magicalbits.smsremote.components.JetchatAppBar
|
||||
import xyz.magicalbits.smsremote.theme.JetchatTheme
|
||||
|
||||
class SimFragment : Fragment() {
|
||||
private val viewModel: SimViewModel by viewModels()
|
||||
private val activityViewModel: MainViewModel by activityViewModels()
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Consider using safe args plugin
|
||||
val phoneNumber = arguments?.getString("phoneNumber")
|
||||
viewModel.setSimData(phoneNumber)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val rootView: View = inflater.inflate(R.layout.fragment_profile, container, false)
|
||||
|
||||
rootView.findViewById<ComposeView>(R.id.toolbar_compose_view).apply {
|
||||
setContent {
|
||||
JetchatTheme {
|
||||
JetchatAppBar(
|
||||
// Reset the minimum bounds that are passed to the root of a compose tree
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
onNavIconPressed = { activityViewModel.openDrawer() },
|
||||
title = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootView.findViewById<ComposeView>(R.id.profile_compose_view).apply {
|
||||
setContent {
|
||||
val simData by viewModel.simData.observeAsState()
|
||||
val nestedScrollInteropConnection = rememberNestedScrollInteropConnection()
|
||||
|
||||
JetchatTheme {
|
||||
// TODO flip if condition after integrating API call
|
||||
if (simData != null) {
|
||||
SimError()
|
||||
println("calling sim error")
|
||||
} else {
|
||||
val navController: NavController = rootView.findNavController()
|
||||
SimScreen(
|
||||
// simData = simData!!,
|
||||
// TODO remove fake data after integrating API call
|
||||
simData = SimScreenState(
|
||||
phoneNumber = "+420123456789",
|
||||
conversations = listOf(
|
||||
ConversationRowState("+15558880000", "last msg", 12345),
|
||||
ConversationRowState("+15558880111", "last msg", 12345),
|
||||
ConversationRowState("+15558880333", "last msg", 12345),
|
||||
)
|
||||
),
|
||||
nestedScrollInteropConnection = nestedScrollInteropConnection,
|
||||
onConversationClicked = {
|
||||
val args = Bundle(1)
|
||||
args.putString("phoneNumber", it)
|
||||
navController.navigate(R.id.action_sim_to_conversation, args)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootView
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package xyz.magicalbits.smsremote.sim
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.magicalbits.smsremote.network.NetworkClient
|
||||
|
||||
class SimViewModel : ViewModel() {
|
||||
private var simPhoneNumber: String = ""
|
||||
private val _simConversations = MutableLiveData<SimScreenState>()
|
||||
val simData: LiveData<SimScreenState> = _simConversations
|
||||
|
||||
fun setSimData(phoneNumber: String?) {
|
||||
if (phoneNumber != null) {
|
||||
simPhoneNumber = phoneNumber
|
||||
|
||||
var conversationDtoList: List<NetworkClient.ConversationPreviewDto> = listOf()
|
||||
// TODO uncomment after adding the corresponding API endpoint
|
||||
// viewModelScope.launch {
|
||||
// val networkClient = NetworkClient()
|
||||
// conversationDtoList = networkClient.getConversationPreviews(simPhoneNumber)
|
||||
// println("conv previews: $conversationDtoList")
|
||||
// }.invokeOnCompletion {
|
||||
// _simConversations.value =
|
||||
// SimScreenState(
|
||||
// phoneNumber = simPhoneNumber,
|
||||
// conversations = conversationDtoList.map { convertFromDto(it) }.toList()
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun convertFromDto(conversation: NetworkClient.ConversationPreviewDto): ConversationRowState {
|
||||
return ConversationRowState(
|
||||
phoneNumber = conversation.remote_phone_number,
|
||||
lastMessage = conversation.last_message_content,
|
||||
messageTimestamp = conversation.message_timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class SimScreenState(
|
||||
val phoneNumber: String,
|
||||
val conversations: List<ConversationRowState>,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class ConversationRowState(
|
||||
val phoneNumber: String,
|
||||
val lastMessage: String,
|
||||
val messageTimestamp: Int,
|
||||
)
|
||||
Reference in New Issue
Block a user