Skip to content

Commit df83b01

Browse files
committed
feat(fc): allow hosts to manage speakers from room info screen
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent a3d8051 commit df83b01

File tree

7 files changed

+196
-15
lines changed

7 files changed

+196
-15
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,12 @@ data class ConversationScreen(
142142

143143
LaunchedEffect(vm) {
144144
vm.eventFlow
145-
.filterIsInstance<ConversationViewModel.Event.OpenJoinConfirmation>()
145+
.filterIsInstance<ConversationViewModel.Event.OpenRoomPreview>()
146146
.map { it.roomInfoArgs }
147147
.onEach {
148148
navigator.push(
149149
ScreenRegistry.get(
150-
NavScreenProvider.Room.Info(args = it, returnToSender = true)
150+
NavScreenProvider.Room.Preview(args = it, returnToSender = true)
151151
)
152152
)
153153
}.launchIn(this)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ class ConversationViewModel @Inject constructor(
240240
data object OnUserTypingStopped : Event
241241

242242
data class LookupRoom(val number: Long) : Event
243-
data class OpenJoinConfirmation(val roomInfoArgs: RoomInfoArgs) : Event
243+
data class OpenRoomPreview(val roomInfoArgs: RoomInfoArgs) : Event
244244
data class OpenRoom(val roomId: ID) : Event
245245

246246
data class Error(
@@ -814,7 +814,7 @@ class ConversationViewModel @Inject constructor(
814814
hostName = moderator?.identity?.displayName,
815815
messagingFeeQuarks = room.messagingFee.quarks,
816816
)
817-
dispatchEvent(Event.OpenJoinConfirmation(roomInfo))
817+
dispatchEvent(Event.OpenRoomPreview(roomInfo))
818818
}
819819
}
820820
}.launchIn(viewModelScope)
@@ -1509,7 +1509,7 @@ class ConversationViewModel @Inject constructor(
15091509
is Event.Resumed,
15101510
is Event.Stopped,
15111511
is Event.LookupRoom,
1512-
is Event.OpenJoinConfirmation,
1512+
is Event.OpenRoomPreview,
15131513
is Event.OpenRoom,
15141514
is Event.OnSendMessage,
15151515
is Event.SendMessage,

flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/ChatInfoViewModel.kt

Lines changed: 164 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import kotlinx.coroutines.flow.mapNotNull
2121
import kotlinx.coroutines.flow.onEach
2222
import xyz.flipchat.app.R
2323
import xyz.flipchat.app.data.RoomInfo
24+
import xyz.flipchat.app.features.chat.conversation.ConversationViewModel
25+
import xyz.flipchat.app.features.chat.conversation.ConversationViewModel.Event
2426
import xyz.flipchat.app.features.login.register.onResult
2527
import xyz.flipchat.app.util.IntentUtils
2628
import xyz.flipchat.chat.RoomController
@@ -47,6 +49,7 @@ class ChatInfoViewModel @Inject constructor(
4749
) {
4850

4951
data class State(
52+
val isPreview: Boolean = false,
5053
val isHost: Boolean = false,
5154
val isMember: Boolean = false,
5255
val paymentDestination: PublicKey? = null,
@@ -64,7 +67,7 @@ class ChatInfoViewModel @Inject constructor(
6467
data class OnHostStatusChanged(val isHost: Boolean) : Event
6568
data class OnRoomOpenStateChanged(val isOpen: Boolean) : Event
6669
data class OnDestinationChanged(val destination: PublicKey) : Event
67-
data class OnInfoChanged(val args: RoomInfoArgs) : Event
70+
data class OnInfoChanged(val args: RoomInfoArgs, val isPreview: Boolean) : Event
6871
data class OnMembersUpdated(val members: List<MinimalMember>) : Event
6972
// endregion state updates
7073

@@ -86,6 +89,13 @@ class ChatInfoViewModel @Inject constructor(
8689
data class OnOpenRoom(val conversationId: ID) : Event
8790
data class OnCloseRoom(val conversationId: ID) : Event
8891

92+
data class PromoteRequested(val member: MinimalMember) : Event
93+
data class PromoteUser(val conversationId: ID, val userId: ID) : Event
94+
data class OnUserPromoted(val id: ID) : Event
95+
data class DemoteRequested(val member: MinimalMember) : Event
96+
data class DemoteUser(val conversationId: ID, val userId: ID) : Event
97+
data class OnUserDemoted(val id: ID) : Event
98+
8999
data object LeaveRoom : Event
90100
data class OnLeavingStateChanged(val leaving: Boolean, val left: Boolean = false) : Event
91101
data object OnLeaveRoomConfirmed : Event
@@ -274,6 +284,60 @@ class ChatInfoViewModel @Inject constructor(
274284
.map { IntentUtils.shareRoom(stateFlow.value.roomInfo.roomNumber) }
275285
.onEach { dispatchEvent(Event.ShareRoom(it)) }
276286
.launchIn(viewModelScope)
287+
288+
eventFlow
289+
.filterIsInstance<Event.PromoteRequested>()
290+
.map { it.member }
291+
.onEach {
292+
confirmUserPromote(
293+
conversationId = stateFlow.value.roomInfo.id.orEmpty(),
294+
userId = it.id.orEmpty(),
295+
user = it.displayName
296+
)
297+
}.launchIn(viewModelScope)
298+
299+
eventFlow
300+
.filterIsInstance<Event.DemoteRequested>()
301+
.map { it.member }
302+
.onEach {
303+
confirmUserDemote(
304+
conversationId = stateFlow.value.roomInfo.id.orEmpty(),
305+
userId = it.id.orEmpty(),
306+
user = it.displayName
307+
)
308+
}.launchIn(viewModelScope)
309+
310+
eventFlow
311+
.filterIsInstance<Event.PromoteUser>()
312+
.onEach { member ->
313+
roomController.promoteUser(member.conversationId, member.userId)
314+
.onFailure {
315+
TopBarManager.showMessage(
316+
TopBarManager.TopBarMessage(
317+
resources.getString(R.string.error_title_failedToPromoteUser),
318+
resources.getString(R.string.error_description_failedToPromoteUser)
319+
)
320+
)
321+
}.onSuccess {
322+
dispatchEvent(Event.OnUserPromoted(member.userId))
323+
}
324+
}.launchIn(viewModelScope)
325+
326+
eventFlow
327+
.filterIsInstance<Event.DemoteUser>()
328+
.onEach { member ->
329+
roomController.demoteUser(member.conversationId, member.userId)
330+
.onFailure {
331+
TopBarManager.showMessage(
332+
TopBarManager.TopBarMessage(
333+
resources.getString(R.string.error_title_failedToDemoteUser),
334+
resources.getString(R.string.error_description_failedToDemoteUser)
335+
)
336+
)
337+
}.onSuccess {
338+
dispatchEvent(Event.OnUserDemoted(member.userId))
339+
}
340+
}.launchIn(viewModelScope)
277341
}
278342

279343
private fun confirmOpenStateChange(conversationId: ID, isRoomOpen: Boolean) {
@@ -302,13 +366,50 @@ class ChatInfoViewModel @Inject constructor(
302366
)
303367
}
304368

369+
private fun confirmUserPromote(conversationId: ID, user: String?, userId: ID) {
370+
BottomBarManager.showMessage(
371+
BottomBarManager.BottomBarMessage(
372+
title = resources.getString(
373+
R.string.title_promoteUserInRoom,
374+
user.orEmpty().ifEmpty { "User" }),
375+
subtitle = resources.getString(R.string.subtitle_promoteUserInRoom),
376+
positiveText = resources.getString(R.string.action_promote),
377+
negativeText = "",
378+
tertiaryText = resources.getString(R.string.action_cancel),
379+
onPositive = { dispatchEvent(Event.PromoteUser(conversationId, userId)) },
380+
onNegative = { },
381+
type = BottomBarManager.BottomBarMessageType.THEMED,
382+
showScrim = true,
383+
)
384+
)
385+
}
386+
387+
private fun confirmUserDemote(conversationId: ID, user: String?, userId: ID) {
388+
BottomBarManager.showMessage(
389+
BottomBarManager.BottomBarMessage(
390+
title = resources.getString(
391+
R.string.title_demoteUserInRoom,
392+
user.orEmpty().ifEmpty { "User" }),
393+
subtitle = resources.getString(R.string.subtitle_demoteUserInRoom),
394+
positiveText = resources.getString(R.string.action_demote),
395+
negativeText = "",
396+
tertiaryText = resources.getString(R.string.action_cancel),
397+
onPositive = { dispatchEvent(Event.DemoteUser(conversationId, userId)) },
398+
onNegative = { },
399+
type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE,
400+
showScrim = true,
401+
)
402+
)
403+
}
404+
305405
companion object {
306406
val updateStateForEvent: (Event) -> ((State) -> State) = { event ->
307-
when (event) {
407+
(when (event) {
308408
Event.LeaveRoom -> { state -> state }
309409
is Event.OnInfoChanged -> { state ->
310410
val args = event.args
311411
state.copy(
412+
isPreview = event.isPreview,
312413
roomInfo = RoomInfo(
313414
id = args.roomId,
314415
number = args.roomNumber,
@@ -322,6 +423,10 @@ class ChatInfoViewModel @Inject constructor(
322423
)
323424
}
324425

426+
is Event.PromoteRequested,
427+
is Event.PromoteUser,
428+
is Event.DemoteRequested,
429+
is Event.DemoteUser,
325430
is Event.OnChangeMessageFee,
326431
Event.OnLeaveRoomConfirmed,
327432
is Event.OnChangeName,
@@ -334,6 +439,52 @@ class ChatInfoViewModel @Inject constructor(
334439
is Event.OnOpenRoom,
335440
Event.OnLeftRoom -> { state -> state }
336441

442+
is Event.OnUserPromoted -> { state ->
443+
val members = state.members.flatMap { it.value }
444+
val updatedMembers = members.map {
445+
if (it.id == event.id) {
446+
it.copy(canSpeak = true)
447+
} else {
448+
it
449+
}
450+
}
451+
452+
val groupedMembers = updatedMembers
453+
.groupBy { it.canSpeak }
454+
.mapKeys {
455+
if (it.key) {
456+
MemberType.Speaker
457+
} else {
458+
MemberType.Listener
459+
}
460+
}.mapValues { it.value.sortedByDescending { it.isHost } }
461+
462+
state.copy(members = groupedMembers)
463+
}
464+
465+
is Event.OnUserDemoted -> { state ->
466+
val members = state.members.flatMap { it.value }
467+
val updatedMembers = members.map {
468+
if (it.id == event.id) {
469+
it.copy(canSpeak = false)
470+
} else {
471+
it
472+
}
473+
}
474+
475+
val groupedMembers = updatedMembers
476+
.groupBy { it.canSpeak }
477+
.mapKeys {
478+
if (it.key) {
479+
MemberType.Speaker
480+
} else {
481+
MemberType.Listener
482+
}
483+
}.mapValues { it.value.sortedByDescending { it.isHost } }
484+
485+
state.copy(members = groupedMembers)
486+
}
487+
337488
is Event.OnHostStatusChanged -> { state -> state.copy(isHost = event.isHost) }
338489
is Event.OnFeeChanged -> { state ->
339490
state.copy(
@@ -371,7 +522,12 @@ class ChatInfoViewModel @Inject constructor(
371522
)
372523
}
373524

374-
is Event.OnRoomNameChangesEnabled -> { state -> state.copy(roomNameChangesEnabled = event.enabled) }
525+
is Event.OnRoomNameChangesEnabled -> { state ->
526+
state.copy(
527+
roomNameChangesEnabled = event.enabled
528+
)
529+
}
530+
375531
is Event.OnDestinationChanged -> { state -> state.copy(paymentDestination = event.destination) }
376532
is Event.OnJoiningStateChanged -> { state ->
377533
state.copy(
@@ -384,12 +540,15 @@ class ChatInfoViewModel @Inject constructor(
384540

385541
is Event.OnLeavingStateChanged -> { state ->
386542
state.copy(
387-
leaving = state.joining.copy(loading = event.leaving, success = event.left)
543+
leaving = state.joining.copy(
544+
loading = event.leaving,
545+
success = event.left
546+
)
388547
)
389548
}
390549

391550
is Event.OnRoomOpenStateChanged -> { state -> state.copy(isOpen = event.isOpen) }
392-
}
551+
})
393552
}
394553
}
395554
}

flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/RoomInfoScreen.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ import com.getcode.ui.components.contextmenu.ContextMenuAction
5555
import com.getcode.ui.theme.ButtonState
5656
import com.getcode.ui.theme.CodeButton
5757
import com.getcode.ui.theme.CodeScaffold
58+
import com.getcode.ui.utils.rememberedLongClickable
5859
import com.getcode.ui.utils.unboundedClickable
5960
import com.getcode.ui.utils.verticalScrollStateGradient
61+
import com.getcode.utils.base58
6062
import kotlinx.coroutines.flow.filterIsInstance
6163
import kotlinx.coroutines.flow.launchIn
6264
import kotlinx.coroutines.flow.map
@@ -69,6 +71,7 @@ import xyz.flipchat.app.features.home.TabbedHomeScreen
6971
@Parcelize
7072
class RoomInfoScreen(
7173
private val info: RoomInfoArgs,
74+
private val isPreview: Boolean,
7275
private val returnToSender: Boolean
7376
) : Screen, Parcelable {
7477

@@ -82,7 +85,7 @@ class RoomInfoScreen(
8285
val context = LocalContext.current
8386

8487
LaunchedEffect(info) {
85-
viewModel.dispatchEvent(ChatInfoViewModel.Event.OnInfoChanged(info))
88+
viewModel.dispatchEvent(ChatInfoViewModel.Event.OnInfoChanged(info, isPreview))
8689
}
8790

8891
LaunchedEffect(viewModel) {
@@ -315,8 +318,13 @@ private fun RoomInfoScreenContent(
315318
)
316319
}
317320

318-
items(speakers) { member ->
321+
items(speakers, key = { it.id?.base58.orEmpty() }) { member ->
319322
Column(
323+
modifier = Modifier.rememberedLongClickable(
324+
enabled = state.isHost && !member.isSelf
325+
) {
326+
dispatch(ChatInfoViewModel.Event.DemoteRequested(member))
327+
}.animateItem(),
320328
horizontalAlignment = Alignment.CenterHorizontally,
321329
verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2)
322330
) {
@@ -355,8 +363,13 @@ private fun RoomInfoScreenContent(
355363
)
356364
}
357365

358-
items(listeners) { member ->
366+
items(listeners, key = { it.id?.base58.orEmpty() }) { member ->
359367
Column(
368+
modifier = Modifier.rememberedLongClickable(
369+
enabled = state.isHost && !member.isSelf
370+
) {
371+
dispatch(ChatInfoViewModel.Event.PromoteRequested(member))
372+
}.animateItem(),
360373
horizontalAlignment = Alignment.CenterHorizontally,
361374
verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2)
362375
) {

flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/navigation/AppScreenContent.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ fun AppScreenContent(content: @Composable () -> Unit) {
9191
}
9292

9393
register<NavScreenProvider.Room.Info> {
94-
RoomInfoScreen(it.args, it.returnToSender)
94+
RoomInfoScreen(it.args, false, it.returnToSender)
95+
}
96+
97+
register<NavScreenProvider.Room.Preview> {
98+
RoomInfoScreen(it.args, true, it.returnToSender)
9599
}
96100

97101
register<NavScreenProvider.Room.ChangeCover> {

flipchatApp/src/main/kotlin/xyz/flipchat/app/util/Router.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class RouterImpl(
125125

126126
screens.add(
127127
ScreenRegistry.get(
128-
NavScreenProvider.Room.Info(args = args, returnToSender = true)
128+
NavScreenProvider.Room.Preview(args = args, returnToSender = true)
129129
)
130130
)
131131
}

ui/navigation/src/main/kotlin/com/getcode/navigation/NavScreenProvider.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ sealed class NavScreenProvider : ScreenProvider {
4545
val returnToSender: Boolean = false
4646
) : NavScreenProvider()
4747

48+
data class Preview(
49+
val args: RoomInfoArgs = RoomInfoArgs(),
50+
val returnToSender: Boolean = false
51+
) : NavScreenProvider()
52+
4853
data class ChangeCover(
4954
val id: ID
5055
) : NavScreenProvider()

0 commit comments

Comments
 (0)