Skip to content

Commit 860daec

Browse files
committed
feat(fc): implement room descriptions for hosts
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 905d235 commit 860daec

14 files changed

Lines changed: 643 additions & 44 deletions

File tree

flipchatApp/src/main/kotlin/xyz/flipchat/app/data/RoomInfo.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ data class RoomInfo(
99
val id: ID? = null,
1010
val number: Long = 0,
1111
val title: String = "",
12+
val description: String = "",
1213
val imageUrl: String? = null,
1314
val memberCount: Int = 0,
1415
val hostId: ID? = null,

flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationViewModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,7 @@ class ConversationViewModel @Inject constructor(
989989
roomId = room.id,
990990
roomNumber = room.roomNumber,
991991
roomTitle = room.titleOrFallback(resources),
992+
roomDescription = room.description,
992993
memberCount = members.count(),
993994
ownerId = room.ownerId,
994995
hostName = moderator?.identity?.displayName,
@@ -1690,6 +1691,7 @@ class ConversationViewModel @Inject constructor(
16901691
hostId = host?.id,
16911692
roomInfoArgs = RoomInfoArgs(
16921693
roomId = conversation.id,
1694+
roomDescription = conversation.description,
16931695
roomNumber = conversation.roomNumber,
16941696
ownerId = conversation.ownerId,
16951697
hostName = host?.displayName,
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package xyz.flipchat.app.features.chat.description
2+
3+
import android.os.Parcelable
4+
import androidx.activity.compose.BackHandler
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.PaddingValues
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.imePadding
12+
import androidx.compose.foundation.layout.navigationBarsPadding
13+
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.text.KeyboardOptions
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.runtime.LaunchedEffect
17+
import androidx.compose.runtime.collectAsState
18+
import androidx.compose.runtime.getValue
19+
import androidx.compose.runtime.remember
20+
import androidx.compose.runtime.rememberCoroutineScope
21+
import androidx.compose.ui.Alignment
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.focus.FocusRequester
24+
import androidx.compose.ui.focus.focusRequester
25+
import androidx.compose.ui.graphics.Color
26+
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
27+
import androidx.compose.ui.res.stringResource
28+
import androidx.compose.ui.text.input.KeyboardCapitalization
29+
import androidx.compose.ui.text.input.KeyboardType
30+
import androidx.compose.ui.unit.dp
31+
import cafe.adriel.voyager.core.screen.Screen
32+
import cafe.adriel.voyager.core.screen.ScreenKey
33+
import cafe.adriel.voyager.core.screen.uniqueScreenKey
34+
import cafe.adriel.voyager.hilt.getViewModel
35+
import com.getcode.model.ID
36+
import com.getcode.navigation.core.LocalCodeNavigator
37+
import com.getcode.theme.CodeTheme
38+
import com.getcode.theme.inputColors
39+
import com.getcode.ui.components.AppBarWithTitle
40+
import com.getcode.ui.components.TextInput
41+
import com.getcode.ui.components.keyboardAsState
42+
import com.getcode.ui.core.rememberAnimationScale
43+
import com.getcode.ui.core.scaled
44+
import com.getcode.ui.theme.ButtonState
45+
import com.getcode.ui.theme.CodeButton
46+
import com.getcode.ui.theme.CodeScaffold
47+
import kotlinx.coroutines.delay
48+
import kotlinx.coroutines.flow.filterIsInstance
49+
import kotlinx.coroutines.flow.launchIn
50+
import kotlinx.coroutines.flow.onEach
51+
import kotlinx.coroutines.launch
52+
import kotlinx.parcelize.IgnoredOnParcel
53+
import kotlinx.parcelize.Parcelize
54+
import xyz.flipchat.app.R
55+
import kotlin.time.Duration.Companion.seconds
56+
57+
@Parcelize
58+
data class RoomDescriptionScreen(val roomId: ID, val existingDescription: String) : Screen, Parcelable {
59+
60+
@IgnoredOnParcel
61+
override val key: ScreenKey = uniqueScreenKey
62+
63+
@Composable
64+
override fun Content() {
65+
val navigator = LocalCodeNavigator.current
66+
val keyboardIsVisible by keyboardAsState()
67+
val keyboard = LocalSoftwareKeyboardController.current
68+
val scope = rememberCoroutineScope()
69+
val animationScale by rememberAnimationScale()
70+
val goBack = {
71+
scope.launch {
72+
if (keyboardIsVisible) {
73+
keyboard?.hide()
74+
delay(500.scaled(animationScale))
75+
}
76+
navigator.pop()
77+
}
78+
}
79+
BackHandler {
80+
goBack()
81+
}
82+
83+
Column(
84+
modifier = Modifier
85+
.fillMaxSize()
86+
) {
87+
AppBarWithTitle(
88+
backButton = true,
89+
title = stringResource(R.string.action_changeRoomDescription),
90+
onBackIconClicked = { goBack() },
91+
)
92+
93+
val viewModel = getViewModel<RoomDescriptionScreenViewModel>()
94+
95+
LaunchedEffect(viewModel, roomId, existingDescription) {
96+
viewModel.dispatchEvent(
97+
RoomDescriptionScreenViewModel.Event.OnNewRequest(
98+
roomId,
99+
existingDescription
100+
)
101+
)
102+
}
103+
104+
LaunchedEffect(viewModel) {
105+
viewModel.eventFlow
106+
.filterIsInstance<RoomDescriptionScreenViewModel.Event.OnSuccess>()
107+
.onEach {
108+
delay(2.seconds)
109+
navigator.pop()
110+
}
111+
.launchIn(this)
112+
}
113+
114+
val state by viewModel.stateFlow.collectAsState()
115+
RoomDescriptionScreenContent(
116+
state = state,
117+
dispatch = viewModel::dispatchEvent
118+
)
119+
}
120+
}
121+
}
122+
123+
@Composable
124+
private fun RoomDescriptionScreenContent(
125+
state: RoomDescriptionScreenViewModel.State,
126+
dispatch: (RoomDescriptionScreenViewModel.Event) -> Unit,
127+
) {
128+
val keyboard = LocalSoftwareKeyboardController.current
129+
val focusRequester = remember { FocusRequester() }
130+
131+
CodeScaffold(
132+
modifier = Modifier
133+
.fillMaxSize()
134+
.imePadding(),
135+
bottomBar = {
136+
Box(
137+
modifier = Modifier
138+
.fillMaxWidth()
139+
.navigationBarsPadding()
140+
.padding(bottom = CodeTheme.dimens.grid.x3),
141+
) {
142+
CodeButton(
143+
enabled = state.canCheck || state.previousRoomDescription.isNotEmpty(), // allow resets
144+
modifier = Modifier
145+
.fillMaxWidth()
146+
.padding(horizontal = CodeTheme.dimens.inset),
147+
buttonState = ButtonState.Filled,
148+
text = stringResource(R.string.action_save),
149+
isLoading = state.update.loading,
150+
isSuccess = state.update.success
151+
) {
152+
keyboard?.hide()
153+
dispatch(RoomDescriptionScreenViewModel.Event.UpdateDescription)
154+
focusRequester.freeFocus()
155+
}
156+
}
157+
}
158+
) { padding ->
159+
Box(
160+
modifier = Modifier
161+
.fillMaxSize()
162+
.padding(padding)
163+
) {
164+
Column(
165+
modifier = Modifier
166+
.fillMaxWidth()
167+
.padding(top = CodeTheme.dimens.inset)
168+
.align(Alignment.TopStart)
169+
.padding(horizontal = CodeTheme.dimens.inset),
170+
verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset)
171+
) {
172+
TextInput(
173+
modifier = Modifier
174+
.fillMaxWidth()
175+
.focusRequester(focusRequester),
176+
state = state.textFieldState,
177+
colors = inputColors(backgroundColor = Color.Transparent),
178+
keyboardOptions = KeyboardOptions(
179+
keyboardType = KeyboardType.Text,
180+
capitalization = KeyboardCapitalization.Sentences
181+
),
182+
contentPadding = PaddingValues(CodeTheme.dimens.grid.x3),
183+
textFieldAlignment = Alignment.TopStart,
184+
minHeight = 120.dp,
185+
maxLines = 6,
186+
isError = state.textFieldState.text.length > state.maxLimit,
187+
placeholder = stringResource(R.string.subtitle_roomDescription),
188+
)
189+
}
190+
}
191+
192+
LaunchedEffect(focusRequester) {
193+
focusRequester.requestFocus()
194+
}
195+
}
196+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package xyz.flipchat.app.features.chat.description
2+
3+
import androidx.compose.foundation.text.input.TextFieldState
4+
import androidx.lifecycle.viewModelScope
5+
import com.getcode.manager.TopBarManager
6+
import com.getcode.model.ID
7+
import com.getcode.model.KinAmount
8+
import com.getcode.model.NoId
9+
import com.getcode.services.model.ExtendedMetadata
10+
import com.getcode.services.utils.onSuccessWithDelay
11+
import com.getcode.util.resources.ResourceHelper
12+
import com.getcode.view.BaseViewModel2
13+
import com.getcode.view.LoadingSuccessState
14+
import dagger.hilt.android.lifecycle.HiltViewModel
15+
import kotlinx.coroutines.flow.filterIsInstance
16+
import kotlinx.coroutines.flow.flatMapLatest
17+
import kotlinx.coroutines.flow.launchIn
18+
import kotlinx.coroutines.flow.map
19+
import kotlinx.coroutines.flow.mapNotNull
20+
import kotlinx.coroutines.flow.onEach
21+
import kotlinx.coroutines.flow.take
22+
import xyz.flipchat.app.R
23+
import xyz.flipchat.chat.RoomController
24+
import xyz.flipchat.controllers.ChatsController
25+
import xyz.flipchat.services.PaymentEvent
26+
import xyz.flipchat.services.data.metadata.StartGroupChatPaymentMetadata
27+
import xyz.flipchat.services.data.metadata.erased
28+
import xyz.flipchat.services.data.metadata.typeUrl
29+
import xyz.flipchat.services.internal.network.service.CheckDisplayNameError
30+
import xyz.flipchat.services.internal.network.service.SetRoomDisplayNameError
31+
import xyz.flipchat.services.user.UserManager
32+
import javax.inject.Inject
33+
import kotlin.time.Duration.Companion.seconds
34+
35+
@HiltViewModel
36+
internal class RoomDescriptionScreenViewModel @Inject constructor(
37+
roomController: RoomController,
38+
resources: ResourceHelper,
39+
) : BaseViewModel2<RoomDescriptionScreenViewModel.State, RoomDescriptionScreenViewModel.Event>(
40+
initialState = State(),
41+
updateStateForEvent = updateStateForEvent
42+
) {
43+
data class State(
44+
val roomId: ID = NoId,
45+
val previousRoomDescription: String = "",
46+
val maxLimit: Int = 160,
47+
val update: LoadingSuccessState = LoadingSuccessState(),
48+
val textFieldState: TextFieldState = TextFieldState(""),
49+
) {
50+
val canCheck: Boolean
51+
get() = textFieldState.text.isNotEmpty() && textFieldState.text.length <= 160
52+
}
53+
54+
sealed interface Event {
55+
data class OnNewRequest(val id: ID, val description: String) : Event
56+
data object UpdateDescription : Event
57+
data object OnError : Event
58+
data object OnSuccess : Event
59+
}
60+
61+
init {
62+
eventFlow
63+
.filterIsInstance<Event.UpdateDescription>()
64+
.map { stateFlow.value }
65+
.onEach {
66+
val textFieldState = it.textFieldState
67+
val text = textFieldState.text.toString().trim()
68+
69+
roomController.setDescription(it.roomId, text)
70+
.onFailure {
71+
TopBarManager.showMessage(
72+
TopBarManager.TopBarMessage(
73+
title = resources.getString(R.string.error_title_failedToChangeRoomDescription),
74+
message = resources.getString(R.string.error_description_failedToChangeRoomDescription)
75+
)
76+
)
77+
78+
dispatchEvent(Event.OnError)
79+
}
80+
.onSuccessWithDelay(2.seconds) {
81+
dispatchEvent(Event.OnSuccess)
82+
}
83+
}.launchIn(viewModelScope)
84+
}
85+
86+
internal companion object {
87+
val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
88+
when (event) {
89+
is Event.UpdateDescription -> { state ->
90+
state.copy(
91+
update = LoadingSuccessState(
92+
loading = true,
93+
success = false
94+
)
95+
)
96+
}
97+
98+
is Event.OnError -> { state ->
99+
state.copy(
100+
update = LoadingSuccessState(
101+
loading = false,
102+
success = false
103+
)
104+
)
105+
}
106+
107+
Event.OnSuccess -> { state ->
108+
state.copy(
109+
update = LoadingSuccessState(
110+
loading = false,
111+
success = true
112+
)
113+
)
114+
}
115+
116+
is Event.OnNewRequest -> { state ->
117+
if (state.roomId != event.id) {
118+
state.copy(
119+
update = LoadingSuccessState(),
120+
roomId = event.id,
121+
textFieldState = TextFieldState(initialText = event.description),
122+
previousRoomDescription = event.description,
123+
)
124+
} else {
125+
state
126+
}
127+
}
128+
}
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)