Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,5 @@ sealed interface ChatStep : FlowStep, Parcelable {
*/
@Parcelize
@Serializable
data class Reactors(val messageId: Long) : ChatStep, Sheet, HalfSheet
data class Reactors(val chatId: ChatId, val messageId: Long) : ChatStep, Sheet, HalfSheet
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
Expand All @@ -28,8 +27,10 @@ import com.flipcash.app.messenger.internal.GroupInviteViewModel
import com.flipcash.app.messenger.internal.StartSendCashOnceReady
import com.flipcash.app.messenger.internal.screens.GroupInviteSheet
import com.flipcash.app.messenger.internal.screens.MessengerScreen
import com.flipcash.app.messenger.internal.screens.EmojiPickerViewModel
import com.flipcash.app.messenger.internal.screens.ReactionPickerSheet
import com.flipcash.app.messenger.internal.screens.ReactorsSheet
import com.flipcash.app.messenger.internal.screens.ReactorsViewModel
import com.flipcash.app.messenger.internal.screens.cash.ChatAmountEntryContent
import com.flipcash.app.messenger.internal.screens.cash.ChatInitPaymentSheet
import com.flipcash.app.messenger.internal.screens.profile.ChatProfileScreen
Expand All @@ -39,6 +40,7 @@ import com.flipcash.app.messenger.internal.screens.profile.edit.EditGroupNameScr
import com.flipcash.app.messenger.internal.screens.profile.edit.EditGroupPictureScreen
import com.flipcash.app.messenger.internal.screens.profile.edit.EditGroupScreen
import com.getcode.navigation.annotatedEntry
import com.flipcash.services.models.chat.ChatId
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.flow.FlowHost
import com.getcode.navigation.flow.flowSharedViewModel
Expand All @@ -57,7 +59,6 @@ import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch

@Composable
fun ChatFlowScreen(
Expand Down Expand Up @@ -128,7 +129,7 @@ private fun chatEntryProvider(
FlowReactionPickerScreen(step.messageId)
}
annotatedEntry<ChatStep.Reactors> { step ->
FlowReactorsScreen(step.messageId)
FlowReactorsScreen(step.chatId, step.messageId)
}
}

Expand Down Expand Up @@ -354,16 +355,23 @@ private fun FlowEditGroupPictureScreen() {
EditGroupPictureScreen(flowSharedViewModel<ChatViewModel>())
}

/** The full emoji picker for [messageId] — a tap on any emoji toggles it and dismisses. */
/**
* The full emoji picker for [messageId] — a tap on any emoji toggles it and dismisses. The search
* lives in the entry's own [EmojiPickerViewModel]; only the toggle goes to the chat.
*/
@Composable
private fun FlowReactionPickerScreen(messageId: Long) {
val viewModel = flowSharedViewModel<ChatViewModel>()
val chatViewModel = flowSharedViewModel<ChatViewModel>()
val viewModel = hiltViewModel<EmojiPickerViewModel>()
val dismissSheet = LocalBottomSheetDismissDispatcher.current
val state by viewModel.stateFlow.collectAsStateWithLifecycle()

ReactionPickerSheet(
loadSections = { query -> viewModel.emojiPickerSections(query) },
searchState = state.searchFieldState,
sections = state.sections,
loaded = state.loaded,
onSelected = { emoji ->
viewModel.dispatchEvent(
chatViewModel.dispatchEvent(
ChatViewModel.Event.ToggleReaction(messageId, emoji, clearsSelection = true)
)
dismissSheet()
Expand All @@ -373,50 +381,29 @@ private fun FlowReactionPickerScreen(messageId: Long) {
}

/**
* Who reacted to [messageId], and with what. Tapping a row dismisses this sheet first, then opens
* the reactor's profile — [ChatViewModel.reactorParticipant] resolves a [ChatParticipant] even for
* a reactor absent from `senderProfiles`.
* Who reacted to [messageId], and with what, from the entry's own [ReactorsViewModel]. Tapping a
* named row dismisses this sheet, then opens that person's profile on the chat.
*/
@Composable
private fun FlowReactorsScreen(messageId: Long) {
val viewModel = flowSharedViewModel<ChatViewModel>()
private fun FlowReactorsScreen(chatId: ChatId, messageId: Long) {
val viewModel = hiltViewModel<ReactorsViewModel>()
val navigator = LocalCodeNavigator.current
val dismissSheet = LocalBottomSheetDismissDispatcher.current
val coroutineScope = rememberCoroutineScope()

val pills by remember(viewModel, messageId) { viewModel.reactionPills(messageId) }
.collectAsStateWithLifecycle(initialValue = emptyList())
val rows by remember(viewModel, messageId) { viewModel.reactorsRows(messageId) }
.collectAsStateWithLifecycle(initialValue = emptyList())
val loading by remember(viewModel, messageId) { viewModel.reactorsLoading(messageId) }
.collectAsStateWithLifecycle(initialValue = true)

// OpenReactors already started the fetch; this catches reactions that change while the sheet
// is up. Collected here rather than off `pills` above, whose empty initial value would read as
// "every reaction removed". The first emission is a no-op unless the pills moved since the
// long-press.
LaunchedEffect(viewModel, messageId) {
viewModel.reactionPills(messageId).collect { viewModel.syncReactors(messageId, it) }
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
LaunchedEffect(viewModel, chatId, messageId) {
viewModel.dispatchEvent(ReactorsViewModel.Event.Open(chatId, messageId))
}

ReactorsSheet(
pills = pills,
rows = rows,
loading = loading,
hasMore = { viewModel.reactorsHasMore(messageId) },
resolveDisplay = { userId -> viewModel.reactorDisplay(userId) },
onLoadMore = { viewModel.loadMoreReactors(messageId) },
pills = state.pills,
rows = state.rows,
loading = state.loading,
hasMore = state.hasMore,
onLoadMore = { viewModel.dispatchEvent(ReactorsViewModel.Event.LoadMore) },
onOpenProfile = { userId ->
// Dismiss first (decision: profile opens after the sheet closes), then resolve the
// participant — a row is only tappable once reactorDisplay has already resolved it, so
// this is a re-read of state already on screen rather than a fresh network round trip
// in the common case; reactorParticipant only reaches further (member roster, cached
// profile store) for a reactor absent from senderProfiles.
val profile = state.rows.firstOrNull { it.userId == userId }?.display?.profile
dismissSheet()
coroutineScope.launch {
val participant = viewModel.reactorParticipant(userId) ?: return@launch
navigator.push(ChatStep.Profile(contact = participant))
}
if (profile != null) navigator.push(ChatStep.Profile(contact = ChatParticipant.TipUser(userId, profile)))
},
onDismiss = dismissSheet,
)
Expand Down
Loading
Loading