From 43ff936b24e21429001e086ce9bbb90856ad9b46 Mon Sep 17 00:00:00 2001 From: bmondream Date: Sun, 7 Jun 2026 20:21:04 +0200 Subject: [PATCH] load conversation from API instead of FakeData --- .../smsremote/conversation/Conversation.kt | 12 ++-- .../conversation/ConversationFragment.kt | 19 +++--- .../conversation/ConversationViewModel.kt | 59 +++++++++++++++++++ .../magicalbits/smsremote/data/FakeData.kt | 28 +++------ .../smsremote/network/NetworkClient.kt | 19 ++++++ backend/app.py | 2 + 6 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/ConversationViewModel.kt diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/Conversation.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/Conversation.kt index 6ac55b9..71c83b3 100644 --- a/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/Conversation.kt +++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/Conversation.kt @@ -92,11 +92,12 @@ 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 /** * Entry point for a conversation screen. * - * @param uiState [ConversationUiState] that contains messages to display + * @param uiState [ConversationScreenState] that contains messages to display * @param navigateToProfile User action when navigation to a profile is requested * @param modifier [Modifier] to apply to this layout node * @param onNavIconPressed Sends an event up when the user clicks on the menu @@ -104,7 +105,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun ConversationContent( - uiState: ConversationUiState, + uiState: ConversationScreenState, navigateToProfile: (String) -> Unit, modifier: Modifier = Modifier, onNavIconPressed: () -> Unit = { }, @@ -167,8 +168,9 @@ fun ConversationContent( Scaffold( topBar = { ChannelNameBar( - channelName = uiState.channelName, - channelMembers = uiState.channelMembers, + channelName = uiState.phoneNumber, + // TODO remove? + channelMembers = 2, onNavIconPressed = onNavIconPressed, scrollBehavior = scrollBehavior, ) @@ -540,7 +542,7 @@ fun ClickableMessage(message: Message, isUserMe: Boolean, authorClicked: (String fun ConversationPreview() { JetchatTheme { ConversationContent( - uiState = exampleUiState, + uiState = exampleUiStateNew, navigateToProfile = { }, ) } diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/ConversationFragment.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/ConversationFragment.kt index b946e66..811e606 100644 --- a/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/ConversationFragment.kt +++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/ConversationFragment.kt @@ -23,6 +23,8 @@ import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.ComposeView import androidx.core.os.bundleOf import androidx.fragment.app.Fragment @@ -31,11 +33,12 @@ import androidx.navigation.findNavController import xyz.magicalbits.smsremote.MainViewModel import xyz.magicalbits.smsremote.R import xyz.magicalbits.smsremote.data.exampleUiState -import xyz.magicalbits.smsremote.data.exampleUiState2 +import xyz.magicalbits.smsremote.data.exampleUiStateNew import xyz.magicalbits.smsremote.theme.JetchatTheme class ConversationFragment : Fragment() { private val activityViewModel: MainViewModel by activityViewModels() + private val conversationViewModel: ConversationViewModel by activityViewModels() var phoneNumber: String = "" @@ -43,8 +46,9 @@ class ConversationFragment : Fragment() { super.onAttach(context) // Consider using safe args plugin val phoneNumber = arguments?.getString("phoneNumber") -// viewModel.setDeviceId(deviceId) this.phoneNumber = phoneNumber!! + // update view model with latest messages + conversationViewModel.setConversationData(phoneNumber) } override fun onCreateView( @@ -55,18 +59,11 @@ class ConversationFragment : Fragment() { ComposeView(inflater.context).apply { layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT) - val uiState = - if (phoneNumber == "+420123456789") { - exampleUiState - } else { - exampleUiState2 - } - uiState.channelName = phoneNumber - setContent { + val conversationData by conversationViewModel.conversationData.observeAsState() JetchatTheme { ConversationContent( - uiState = uiState, + uiState = conversationData ?: exampleUiStateNew, navigateToProfile = { user -> // Click callback val bundle = bundleOf("userId" to user) diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/ConversationViewModel.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/ConversationViewModel.kt new file mode 100644 index 0000000..ed77983 --- /dev/null +++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/conversation/ConversationViewModel.kt @@ -0,0 +1,59 @@ +package xyz.magicalbits.smsremote.conversation + +import androidx.compose.runtime.toMutableStateList +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 ConversationViewModel : ViewModel() { + private var phoneNumber: String = "" + private val _conversationData = MutableLiveData() + val conversationData: LiveData = _conversationData + + fun setConversationData(phoneNumber: String?) { + if (phoneNumber != null) { + this.phoneNumber = phoneNumber + + var messageDtoList: List = listOf() + viewModelScope.launch { + val networkClient = NetworkClient() + messageDtoList = networkClient.getSmsMessagesByLocalPhoneNumber(phoneNumber) + }.invokeOnCompletion { + _conversationData.value = + ConversationScreenState( + phoneNumber = this.phoneNumber, + initialMessages = messageDtoList.map { + Message( + if (it.msg_type == "INCOMING") { + it.remote_phone_number + } else { + it.local_phone_number + }, + it.content, + // FIXME convert to HH:MM AM/PM + it.ts_sent.toString(), + null, + ) + }, + ) + println("sims live: ${_conversationData.value!!.initialMessages}") + } + } + } +} + +data class ConversationScreenState( + val phoneNumber: String, + val initialMessages: List, +) { + private val _messages: MutableList = initialMessages.toMutableStateList() + + val messages: List = _messages + + fun addMessage(msg: Message) { + _messages.add(0, msg) + } +} diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/data/FakeData.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/data/FakeData.kt index d8a7d86..62b2521 100644 --- a/android/src/main/kotlin/xyz/magicalbits/smsremote/data/FakeData.kt +++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/data/FakeData.kt @@ -17,9 +17,9 @@ package xyz.magicalbits.smsremote.data import xyz.magicalbits.smsremote.R +import xyz.magicalbits.smsremote.conversation.ConversationScreenState import xyz.magicalbits.smsremote.conversation.ConversationUiState import xyz.magicalbits.smsremote.conversation.Message -import xyz.magicalbits.smsremote.device.DeviceScreenState import xyz.magicalbits.smsremote.profile.ProfileScreenState val initialMessages = @@ -31,30 +31,16 @@ val initialMessages = ), ) -val initialMessages2 = - listOf( - Message( - "uahguoidahfg", - "yolo", - "8:05 PM", - ), - ) - val unreadMessages = initialMessages.filter { it.author != "me" } -val exampleUiState = - ConversationUiState( - initialMessages = initialMessages, - channelName = "Samsung A14", - channelMembers = 42, +val exampleUiStateNew = + ConversationScreenState( + phoneNumber = "", + initialMessages = mutableListOf() ) -val exampleUiState2 = - ConversationUiState( - initialMessages = initialMessages2, - channelName = "iPhone XYZ", - channelMembers = 69, - ) +val exampleUiState = + ConversationUiState("name", 123, listOf()) /** * Example colleague profile diff --git a/android/src/main/kotlin/xyz/magicalbits/smsremote/network/NetworkClient.kt b/android/src/main/kotlin/xyz/magicalbits/smsremote/network/NetworkClient.kt index 117f2a5..e6f9460 100644 --- a/android/src/main/kotlin/xyz/magicalbits/smsremote/network/NetworkClient.kt +++ b/android/src/main/kotlin/xyz/magicalbits/smsremote/network/NetworkClient.kt @@ -7,8 +7,11 @@ import io.ktor.client.plugins.auth.providers.BearerTokens import io.ktor.client.plugins.auth.providers.bearer import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import java.net.URLEncoder class NetworkClient { // TODO make apiBaseUrl configurable with fallback to my real domain @@ -37,6 +40,9 @@ class NetworkClient { @Serializable data class SimCardDto(val device_access_key: String, val phone_number: String) + @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) + // GET /api/v1/devices suspend fun getDevices(): List { // TODO handle non-200 status codes @@ -48,7 +54,20 @@ class NetworkClient { // GET /api/v1/sim-cards suspend fun getSimCardsByAccessKey(accessKey: String): List { // TODO handle non-200 status codes + // TODO handle '{"msg":"Token has expired"}' messages val data = networkClient.get("$apiBaseUrl/api/v1/sim-cards?access_key=$accessKey").bodyAsText() return Json.decodeFromString(data) } + + // GET /api/v1/sms-messages + suspend fun getSmsMessagesByLocalPhoneNumber(phoneNumber: String): List { + // TODO handle non-200 status codes + // TODO handle '{"msg":"Token has expired"}' messages + // TODO extract encoder to a function here or to a utility class + val encodedPhoneNumber = withContext(Dispatchers.IO) { + URLEncoder.encode(phoneNumber, "UTF-8") + } + val data = networkClient.get("$apiBaseUrl/api/v1/sms-messages?local_phone_number=$encodedPhoneNumber").bodyAsText() + return Json.decodeFromString(data) + } } diff --git a/backend/app.py b/backend/app.py index aae2324..4d020e9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -93,6 +93,8 @@ def get_sms_messages_by_local_phone_number(): return make_response(jsonify(msg=msg_403_not_primary), 403) local_phone_number = request.args.get("local_phone_number", None) + # TODO set up access logging + #print(f"/api/v1/sms-messages - local_phone_number='{local_phone_number}'") return make_response(jsonify([n.to_dict() for n in db.get_sms_messages_by_local_phone_number(cur, local_phone_number)]), 200)