Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reaction emoji search / Sending freeform reactions #2892

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
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 @@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.core.EventId

sealed interface MessagesEvents {
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
data class ToggleReaction(val emoji: String, val eventId: EventId) : MessagesEvents
data class ToggleReaction(val reaction: String, val eventId: EventId) : MessagesEvents
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
data object Dismiss : MessagesEvents
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class MessagesPresenter @AssistedInject constructor(
)
}
is MessagesEvents.ToggleReaction -> {
localCoroutineScope.toggleReaction(event.emoji, event.eventId)
localCoroutineScope.toggleReaction(event.reaction, event.eventId)
}
is MessagesEvents.InviteDialogDismissed -> {
hasDismissedInviteDialog = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemList
import io.element.android.features.messages.impl.timeline.aTimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
import io.element.android.features.messages.impl.timeline.components.customreaction.EmojiPickerState
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents
Expand All @@ -42,6 +43,7 @@ import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMe
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.textcomposer.aRichTextEditorState
import io.element.android.libraries.textcomposer.model.MessageComposerMode
Expand Down Expand Up @@ -163,6 +165,7 @@ fun aCustomReactionState(
target = target,
selectedEmoji = persistentSetOf(),
eventSink = eventSink,
searchState = EmojiPickerState(false, false, "", SearchBarResultState.Initial()) {}
)

fun aReadReceiptBottomSheetState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.timeline.TimelineView
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvents
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheet
Expand All @@ -92,6 +93,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.modifiers.applyIf
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle
Expand Down Expand Up @@ -256,6 +258,9 @@ fun MessagesView(
state = state.customReactionState,
onEmojiSelected = { eventId, emoji ->
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
},
onReactionSelected = { eventId, reaction ->
state.eventSink(MessagesEvents.ToggleReaction(reaction, eventId))
}
)

Expand Down Expand Up @@ -332,7 +337,11 @@ private fun MessagesViewContent(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding()
.imePadding(),
.applyIf(
// Disable imePadding() when reaction picker is open to prevent the chat moving behind the bottom sheet
condition = state.customReactionState.target is CustomReactionState.Target.None,
ifTrue = { imePadding() }
)
) {
AttachmentsBottomSheet(
state = state.composerState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,16 @@ fun MessagesReactionButton(

@Immutable
sealed interface MessagesReactionsButtonContent {
data class Text(val text: String) : MessagesReactionsButtonContent
data class Text(val text: String, val highlight: Boolean = false) : MessagesReactionsButtonContent
data class Icon(@DrawableRes val resourceId: Int) : MessagesReactionsButtonContent

data class Reaction(val reaction: AggregatedReaction) : MessagesReactionsButtonContent

val isHighlighted get() = this is Reaction && reaction.isHighlighted
val isHighlighted get() = when(this) {
is Reaction -> reaction.isHighlighted
is Text -> highlight
else -> false
}
}

internal val REACTION_EMOJI_LINE_HEIGHT = 20.sp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@

package io.element.android.features.messages.impl.timeline.components.customreaction

import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.emojibasebindings.Emoji
import io.element.android.libraries.androidutils.ui.hideKeyboard
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.hide
import io.element.android.libraries.matrix.api.core.EventId
Expand All @@ -32,34 +41,58 @@ import io.element.android.libraries.matrix.api.core.EventId
fun CustomReactionBottomSheet(
state: CustomReactionState,
onEmojiSelected: (EventId, Emoji) -> Unit,
onReactionSelected: (EventId, String) -> Unit,
modifier: Modifier = Modifier,
) {
val sheetState = rememberModalBottomSheetState()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = state.searchState.startActive)
val coroutineScope = rememberCoroutineScope()
val target = state.target as? CustomReactionState.Target.Success
val localView = LocalView.current

fun onDismiss() {
localView.hideKeyboard()
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
}

fun onEmojiSelectedDismiss(emoji: Emoji) {
localView.hideKeyboard()
if (target?.event?.eventId == null) return
sheetState.hide(coroutineScope) {
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
onEmojiSelected(target.event.eventId, emoji)
}
}

fun onReactionSelectedDismiss(reaction: String) {
if (target?.event?.eventId == null) return
sheetState.hide(coroutineScope) {
state.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
onReactionSelected(target.event.eventId, reaction)
}
}

if (target?.emojibaseStore != null && target.event.eventId != null) {
ModalBottomSheet(
onDismissRequest = ::onDismiss,
sheetState = sheetState,
modifier = modifier
.heightIn(min = if (state.searchState.isSearchActive) (LocalConfiguration.current.screenHeightDp).dp else Dp.Unspecified)
.pointerInput(state.searchState.isSearchActive) {
awaitEachGesture {
// For any unconsumed pointer event in this sheet, deactivate the search field and hide the keyboard
awaitFirstDown(requireUnconsumed = true)
if (state.searchState.isSearchActive) {
state.searchState.eventSink(EmojiPickerEvents.OnSearchActiveChanged(false))
}
}
}
) {
EmojiPicker(
onEmojiSelected = ::onEmojiSelectedDismiss,
onReactionSelected = ::onReactionSelectedDismiss,
emojibaseStore = target.emojibaseStore,
selectedEmojis = state.selectedEmoji,
state = state.searchState,
modifier = Modifier.fillMaxSize(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,23 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.androidutils.ui.hideKeyboard
import io.element.android.libraries.architecture.Presenter
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.launch
import javax.inject.Inject

class CustomReactionPresenter @Inject constructor(
private val emojibaseProvider: EmojibaseProvider
private val emojibaseProvider: EmojibaseProvider,
private val emojiPickerStatePresenter: EmojiPickerStatePresenter,
) : Presenter<CustomReactionState> {
@Composable
override fun present(): CustomReactionState {
val target: MutableState<CustomReactionState.Target> = remember {
mutableStateOf(CustomReactionState.Target.None)
}
val searchState = emojiPickerStatePresenter.present()

val localCoroutineScope = rememberCoroutineScope()
fun handleShowCustomReactionSheet(event: TimelineItem.Event) {
Expand All @@ -49,6 +53,7 @@ class CustomReactionPresenter @Inject constructor(

fun handleDismissCustomReactionSheet() {
target.value = CustomReactionState.Target.None
searchState.eventSink(EmojiPickerEvents.Reset)
}

fun handleEvents(event: CustomReactionEvents) {
Expand All @@ -57,6 +62,7 @@ class CustomReactionPresenter @Inject constructor(
is CustomReactionEvents.DismissCustomReactionSheet -> handleDismissCustomReactionSheet()
}
}

val event = (target.value as? CustomReactionState.Target.Success)?.event
val selectedEmoji = event
?.reactionsState
Expand All @@ -67,7 +73,8 @@ class CustomReactionPresenter @Inject constructor(
return CustomReactionState(
target = target.value,
selectedEmoji = selectedEmoji,
eventSink = { handleEvents(it) }
eventSink = ::handleEvents,
searchState = searchState,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ data class CustomReactionState(
val target: Target,
val selectedEmoji: ImmutableSet<String>,
val eventSink: (CustomReactionEvents) -> Unit,
val searchState: EmojiPickerState,
) {
sealed interface Target {
data object None : Target
Expand Down
Loading
Loading