Skip to content

Commit 70febe8

Browse files
authored
Merge pull request #550 from code-payments/feat/create-chat-flow
chore: add initial create chat flow stub screen
2 parents 94e4c95 + ec97b21 commit 70febe8

7 files changed

Lines changed: 288 additions & 41 deletions

File tree

app/src/main/java/com/getcode/navigation/screens/ChatScreens.kt

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import android.widget.Toast
44
import androidx.compose.foundation.layout.Arrangement
55
import androidx.compose.foundation.layout.Column
66
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.Spacer
78
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.foundation.layout.requiredWidth
810
import androidx.compose.foundation.layout.size
911
import androidx.compose.foundation.shape.CircleShape
1012
import androidx.compose.material.Text
@@ -30,17 +32,14 @@ import com.getcode.model.chat.Reference
3032
import com.getcode.navigation.core.LocalCodeNavigator
3133
import com.getcode.theme.CodeTheme
3234
import com.getcode.ui.components.chat.UserAvatar
33-
import com.getcode.ui.components.chat.utils.localized
3435
import com.getcode.util.formatDateRelatively
35-
import com.getcode.view.main.chat.ChatScreen
36-
import com.getcode.view.main.chat.ChatViewModel
3736
import com.getcode.view.main.chat.conversation.ChatConversationScreen
3837
import com.getcode.view.main.chat.conversation.ConversationViewModel
38+
import com.getcode.view.main.chat.create.byusername.ChatByUsernameScreen
3939
import com.getcode.view.main.chat.list.ChatListScreen
4040
import com.getcode.view.main.chat.list.ChatListViewModel
4141
import kotlinx.coroutines.flow.filterIsInstance
4242
import kotlinx.coroutines.flow.launchIn
43-
import kotlinx.coroutines.flow.map
4443
import kotlinx.coroutines.flow.onEach
4544
import kotlinx.parcelize.IgnoredOnParcel
4645
import kotlinx.parcelize.Parcelize
@@ -61,48 +60,32 @@ data object ChatListModal: ChatGraph, ModalRoot {
6160
) {
6261
val viewModel = getViewModel<ChatListViewModel>()
6362
// val conversations = viewModel.conversations.collectAsLazyPagingItems()
64-
ChatListScreen(dispatch = {})
63+
ChatListScreen(viewModel)
6564
}
6665
}
6766
}
6867

6968
@Parcelize
70-
data class ChatScreen(val chatId: ID) : ChatGraph, ModalContent {
69+
data object ChatByUsernameScreen: ChatGraph, ModalContent {
7170
@IgnoredOnParcel
7271
override val key: ScreenKey = uniqueScreenKey
7372

73+
override val name: String
74+
@Composable get() = stringResource(id = R.string.title_whatsTheirUsername)
75+
7476
@Composable
7577
override fun Content() {
76-
val vm = getViewModel<ChatViewModel>()
77-
val state by vm.stateFlow.collectAsState()
78-
val navigator = LocalCodeNavigator.current
79-
8078
ModalContainer(
81-
titleString = { state.title.localized },
82-
backButtonEnabled = { it is ChatScreen },
79+
backButtonEnabled = { it is ChatByUsernameScreen },
8380
) {
84-
val messages = vm.chatMessages.collectAsLazyPagingItems()
85-
ChatScreen(state = state, messages = messages, dispatch = vm::dispatchEvent)
86-
}
87-
88-
LaunchedEffect(vm) {
89-
vm.eventFlow
90-
.filterIsInstance<ChatViewModel.Event.OpenMessageChat>()
91-
.map { it.reference }
92-
.filterIsInstance<Reference.IntentId>()
93-
.map { it.id }
94-
.onEach { navigator.push(ChatMessageConversationScreen(intentId = it)) }
95-
.launchIn(this)
96-
}
97-
98-
LaunchedEffect(chatId) {
99-
vm.dispatchEvent(ChatViewModel.Event.OnChatIdChanged(chatId))
81+
ChatByUsernameScreen(getViewModel())
10082
}
10183
}
10284
}
10385

10486
@Parcelize
10587
data class ChatMessageConversationScreen(
88+
val username: String? = null,
10689
val chatId: ID? = null,
10790
val intentId: ID? = null
10891
) : AppScreen(), ChatGraph, ModalContent {
@@ -144,6 +127,10 @@ data class ChatMessageConversationScreen(
144127
}
145128
)
146129
}
130+
131+
if (state.users.isEmpty()) {
132+
Spacer(modifier = Modifier.requiredWidth(CodeTheme.dimens.grid.x3))
133+
}
147134
}
148135

149136
Column {

app/src/main/java/com/getcode/navigation/screens/MainScreens.kt

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,28 @@ import androidx.compose.material.Icon
44
import androidx.compose.material.icons.Icons
55
import androidx.compose.material.icons.rounded.BubbleChart
66
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.LaunchedEffect
78
import androidx.compose.runtime.collectAsState
89
import androidx.compose.runtime.derivedStateOf
910
import androidx.compose.runtime.getValue
1011
import androidx.compose.runtime.remember
1112
import androidx.compose.ui.graphics.Color
1213
import androidx.compose.ui.res.stringResource
1314
import androidx.lifecycle.Lifecycle
15+
import androidx.paging.compose.collectAsLazyPagingItems
1416
import cafe.adriel.voyager.core.lifecycle.LifecycleEffect
1517
import cafe.adriel.voyager.core.screen.ScreenKey
1618
import cafe.adriel.voyager.core.screen.uniqueScreenKey
1719
import cafe.adriel.voyager.hilt.getViewModel
1820
import cafe.adriel.voyager.navigator.currentOrThrow
1921
import com.getcode.LocalSession
2022
import com.getcode.R
23+
import com.getcode.model.ID
24+
import com.getcode.model.chat.Reference
2125
import com.getcode.models.DeepLinkRequest
2226
import com.getcode.navigation.core.LocalCodeNavigator
2327
import com.getcode.ui.components.SheetTitleDefaults
28+
import com.getcode.ui.components.chat.utils.localized
2429
import com.getcode.ui.utils.RepeatOnLifecycle
2530
import com.getcode.ui.utils.getActivityScopedViewModel
2631
import com.getcode.utils.trace
@@ -29,11 +34,15 @@ import com.getcode.view.main.account.AccountHome
2934
import com.getcode.view.main.account.AccountSheetViewModel
3035
import com.getcode.view.main.balance.BalanceScreen
3136
import com.getcode.view.main.balance.BalanceSheetViewModel
37+
import com.getcode.view.main.chat.ChatScreen
38+
import com.getcode.view.main.chat.ChatViewModel
3239
import com.getcode.view.main.giveKin.GiveKinScreen
33-
import com.getcode.view.main.scanner.ScanScreen
3440
import com.getcode.view.main.requestKin.RequestKinScreen
41+
import com.getcode.view.main.scanner.ScanScreen
42+
import kotlinx.coroutines.flow.filterIsInstance
3543
import kotlinx.coroutines.flow.filterNotNull
3644
import kotlinx.coroutines.flow.launchIn
45+
import kotlinx.coroutines.flow.map
3746
import kotlinx.coroutines.flow.mapNotNull
3847
import kotlinx.coroutines.flow.onEach
3948
import kotlinx.parcelize.IgnoredOnParcel
@@ -261,6 +270,40 @@ data object BalanceModal : ChatGraph, ModalRoot {
261270
}
262271
}
263272

273+
@Parcelize
274+
data class ChatScreen(val chatId: ID) : MainGraph, ModalContent {
275+
@IgnoredOnParcel
276+
override val key: ScreenKey = uniqueScreenKey
277+
278+
@Composable
279+
override fun Content() {
280+
val vm = getViewModel<ChatViewModel>()
281+
val state by vm.stateFlow.collectAsState()
282+
val navigator = LocalCodeNavigator.current
283+
284+
ModalContainer(
285+
titleString = { state.title.localized },
286+
backButtonEnabled = { it is ChatScreen },
287+
) {
288+
val messages = vm.chatMessages.collectAsLazyPagingItems()
289+
ChatScreen(state = state, messages = messages, dispatch = vm::dispatchEvent)
290+
}
291+
292+
LaunchedEffect(vm) {
293+
vm.eventFlow
294+
.filterIsInstance<ChatViewModel.Event.OpenMessageChat>()
295+
.map { it.reference }
296+
.filterIsInstance<Reference.IntentId>()
297+
.map { it.id }
298+
.onEach { navigator.push(ChatMessageConversationScreen(intentId = it)) }
299+
.launchIn(this)
300+
}
301+
302+
LaunchedEffect(chatId) {
303+
vm.dispatchEvent(ChatViewModel.Event.OnChatIdChanged(chatId))
304+
}
305+
}
306+
}
264307

265308
@Composable
266309
fun <T> AppScreen.OnScreenResult(block: (T) -> Unit) {

app/src/main/java/com/getcode/view/main/chat/conversation/ConversationViewModel.kt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ class ConversationViewModel @Inject constructor(
6565
data class State(
6666
val conversationId: ID?,
6767
val reference: Reference.IntentId?,
68-
val title: String,
6968
val textFieldState: TextFieldState,
7069
val tipChatCash: Feature,
7170
val identityAvailable: Boolean,
@@ -88,7 +87,6 @@ class ConversationViewModel @Inject constructor(
8887
conversationId = null,
8988
reference = null,
9089
tipChatCash = ConversationCashFeature(),
91-
title = "Anonymous Tipper",
9290
textFieldState = TextFieldState(),
9391
identityAvailable = false,
9492
identityRevealed = null,
@@ -114,7 +112,6 @@ class ConversationViewModel @Inject constructor(
114112
data class OnTipsChatCashChanged(val module: Feature) : Event
115113

116114
data class OnUserActivity(val activity: Instant) : Event
117-
data class OnTitleChanged(val title: String) : Event
118115
data object SendCash : Event
119116
data object SendMessage : Event
120117
data object RevealIdentity : Event
@@ -335,7 +332,6 @@ class ConversationViewModel @Inject constructor(
335332

336333
state.copy(
337334
conversationId = conversation.id,
338-
title = conversation.name ?: "Anonymous Tipper",
339335
identityRevealed = conversation.hasRevealedIdentity,
340336
pointers = event.conversationWithPointers.pointers,
341337
users = members.map {
@@ -354,12 +350,6 @@ class ConversationViewModel @Inject constructor(
354350
)
355351
}
356352

357-
is Event.OnTitleChanged -> { state ->
358-
state.copy(
359-
title = event.title
360-
)
361-
}
362-
363353
is Event.OnPointersUpdated -> { state ->
364354
state.copy(pointers = event.pointers)
365355
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package com.getcode.view.main.chat.create.byusername
2+
3+
import android.graphics.Paint.Align
4+
import androidx.compose.foundation.ExperimentalFoundationApi
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.PaddingValues
8+
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.imePadding
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.LaunchedEffect
14+
import androidx.compose.runtime.collectAsState
15+
import androidx.compose.runtime.getValue
16+
import androidx.compose.runtime.mutableStateOf
17+
import androidx.compose.runtime.remember
18+
import androidx.compose.runtime.rememberCoroutineScope
19+
import androidx.compose.runtime.setValue
20+
import androidx.compose.ui.Alignment
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.focus.FocusRequester
23+
import androidx.compose.ui.focus.focusRequester
24+
import androidx.compose.ui.graphics.Color
25+
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
26+
import androidx.compose.ui.res.stringResource
27+
import androidx.compose.ui.text.style.TextAlign
28+
import androidx.compose.ui.unit.dp
29+
import com.getcode.R
30+
import com.getcode.navigation.core.LocalCodeNavigator
31+
import com.getcode.navigation.screens.ChatMessageConversationScreen
32+
import com.getcode.theme.CodeTheme
33+
import com.getcode.theme.inputColors
34+
import com.getcode.ui.components.ButtonState
35+
import com.getcode.ui.components.CodeButton
36+
import com.getcode.ui.components.CodeScaffold
37+
import com.getcode.ui.components.TextInput
38+
import com.getcode.ui.components.keyboardAsState
39+
import kotlinx.coroutines.delay
40+
import kotlinx.coroutines.flow.filterIsInstance
41+
import kotlinx.coroutines.flow.launchIn
42+
import kotlinx.coroutines.flow.map
43+
import kotlinx.coroutines.flow.onEach
44+
import kotlinx.coroutines.launch
45+
46+
@OptIn(ExperimentalFoundationApi::class)
47+
@Composable
48+
fun ChatByUsernameScreen(
49+
viewModel: ChatByUsernameViewModel
50+
) {
51+
val state by viewModel.stateFlow.collectAsState()
52+
val navigator = LocalCodeNavigator.current
53+
54+
val keyboardVisible by keyboardAsState()
55+
val keyboardController = LocalSoftwareKeyboardController.current
56+
val composeScope = rememberCoroutineScope()
57+
var isChecking by remember(state.checkingUsername) { mutableStateOf(false) }
58+
59+
val checkUsername = {
60+
composeScope.launch {
61+
isChecking = true
62+
if (keyboardVisible) {
63+
keyboardController?.hide()
64+
delay(500)
65+
}
66+
viewModel.dispatchEvent(ChatByUsernameViewModel.Event.CheckUsername)
67+
}
68+
}
69+
70+
LaunchedEffect(viewModel) {
71+
viewModel.eventFlow
72+
.filterIsInstance<ChatByUsernameViewModel.Event.OnSuccess>()
73+
.map { it.username }
74+
.onEach {
75+
navigator.push(ChatMessageConversationScreen(username = it))
76+
}.launchIn(this)
77+
}
78+
79+
CodeScaffold(
80+
modifier = Modifier.fillMaxSize().imePadding(),
81+
bottomBar = {
82+
Box(modifier = Modifier.fillMaxWidth()) {
83+
CodeButton(
84+
enabled = state.canAdvance,
85+
modifier = Modifier
86+
.fillMaxWidth()
87+
.padding(horizontal = CodeTheme.dimens.inset)
88+
.padding(bottom = CodeTheme.dimens.grid.x2),
89+
buttonState = ButtonState.Filled,
90+
text = stringResource(R.string.action_next),
91+
isLoading = isChecking,
92+
isSuccess = state.isValidUsername
93+
) {
94+
checkUsername()
95+
}
96+
}
97+
}
98+
) { padding ->
99+
val focusRequester = remember { FocusRequester() }
100+
Box(
101+
modifier = Modifier
102+
.fillMaxSize()
103+
.padding(padding)
104+
) {
105+
TextInput(
106+
modifier = Modifier
107+
.fillMaxWidth()
108+
.align(Alignment.Center)
109+
.padding(CodeTheme.dimens.inset)
110+
.focusRequester(focusRequester),
111+
state = state.textFieldState,
112+
colors = inputColors(
113+
backgroundColor = Color.Transparent,
114+
borderColor = Color.Transparent
115+
),
116+
contentPadding = PaddingValues(horizontal = 20.dp),
117+
style = CodeTheme.typography.displayMedium,
118+
placeholderStyle = CodeTheme.typography.displayMedium,
119+
placeholder = stringResource(R.string.subtitle_xUsername),
120+
)
121+
}
122+
123+
LaunchedEffect(focusRequester) {
124+
focusRequester.requestFocus()
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)