[Message Actions] Add emoji reactions option (#568)

* Add logic to send message reactions

* Add emoji library, create EmojiPicker component

* Fix bottom sheet behaviors

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2023-06-09 16:56:40 +02:00 committed by GitHub
parent cabedb5f7a
commit 9fa261e393
17 changed files with 388 additions and 27 deletions

View file

@ -219,6 +219,8 @@ dependencies {
implementation(libs.network.okhttp.logging)
implementation(libs.serialization.json)
implementation(libs.vanniktech.emoji)
implementation(libs.dagger)
kapt(libs.dagger.compiler)

View file

@ -23,6 +23,7 @@ import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.x.di.DaggerAppComponent
import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.EmojiInitializer
import io.element.android.x.initializer.MatrixInitializer
import io.element.android.x.initializer.TimberInitializer
@ -40,6 +41,7 @@ class ElementXApplication : Application(), DaggerComponentOwner {
initializeComponent(CrashInitializer::class.java)
initializeComponent(TimberInitializer::class.java)
initializeComponent(MatrixInitializer::class.java)
initializeComponent(EmojiInitializer::class.java)
}
logApplicationInfo()
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.x.initializer
import androidx.startup.Initializer
import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.google.GoogleEmojiProvider
class EmojiInitializer : Initializer<Unit> {
override fun create(context: android.content.Context) {
EmojiManager.install(GoogleEmojiProvider())
}
override fun dependencies(): MutableList<Class<out Initializer<*>>> = mutableListOf()
}

1
changelog.d/563.feature Normal file
View file

@ -0,0 +1 @@
Add emoji reactions to the event context menu and allow sending custom reactions.

View file

@ -57,6 +57,7 @@ dependencies {
implementation(libs.accompanist.systemui)
implementation(libs.vanniktech.blurhash)
implementation(libs.telephoto.zoomableimage)
implementation(libs.vanniktech.emoji)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)

View file

@ -18,7 +18,9 @@ package io.element.android.features.messages.impl
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.timeline.model.TimelineItem
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 SendReaction(val emoji: String, val eventId: EventId) : MessagesEvents
}

View file

@ -45,10 +45,12 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
@ -66,6 +68,7 @@ class MessagesPresenter @Inject constructor(
private val networkMonitor: NetworkMonitor,
private val snackbarDispatcher: SnackbarDispatcher,
private val messageSummaryFormatter: MessageSummaryFormatter,
private val dispatchers: CoroutineDispatchers,
) : Presenter<MessagesState> {
@Composable
@ -103,6 +106,7 @@ class MessagesPresenter @Inject constructor(
fun handleEvents(event: MessagesEvents) {
when (event) {
is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.event, composerState)
is MessagesEvents.SendReaction -> localCoroutineScope.sendReaction(event.emoji, event.eventId)
}
}
return MessagesState(
@ -118,7 +122,7 @@ class MessagesPresenter @Inject constructor(
)
}
fun CoroutineScope.handleTimelineAction(
private fun CoroutineScope.handleTimelineAction(
action: TimelineItemAction,
targetEvent: TimelineItem.Event,
composerState: MessageComposerState,
@ -134,6 +138,14 @@ class MessagesPresenter @Inject constructor(
}
}
private fun CoroutineScope.sendReaction(
emoji: String,
eventId: EventId,
) = launch(dispatchers.io) {
room.sendReaction(emoji, eventId)
.onFailure { Timber.e(it) }
}
private fun notImplementedYet() {
Timber.v("NotImplementedYet")
}

View file

@ -34,8 +34,11 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
@ -56,12 +59,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.ActionListView
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
import io.element.android.features.messages.impl.timeline.TimelineView
import io.element.android.features.messages.impl.timeline.components.CustomReactionBottomSheet
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
import io.element.android.libraries.androidutils.ui.hideKeyboard
@ -85,7 +90,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import io.element.android.libraries.ui.strings.R as StringsR
@OptIn(ExperimentalLayoutApi::class)
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@Composable
fun MessagesView(
state: MessagesState,
@ -97,8 +102,13 @@ fun MessagesView(
onItemDebugInfoClicked: (EventId, TimelineItemDebugInfo) -> Unit,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
val actionListViewBottomSheetState = rememberModalBottomSheetState()
val customReactionBottomSheetState = rememberModalBottomSheetState()
LogCompositions(tag = "MessagesScreen", msg = "Root")
var isMessageActionsBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
var isCustomReactionBottomSheetVisible by rememberSaveable { mutableStateOf(false) }
AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments)
@ -121,8 +131,14 @@ fun MessagesView(
isMessageActionsBottomSheetVisible = true
}
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
suspend fun onDismissActionListBottomSheet() {
state.actionListState.eventSink(ActionListEvents.Clear)
actionListViewBottomSheetState.hide()
isMessageActionsBottomSheetVisible = false
}
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
coroutineScope.launch { onDismissActionListBottomSheet() }
when (action) {
is TimelineItemAction.Developer -> if (event.eventId != null && event.debugInfo != null) {
onItemDebugInfoClicked(event.eventId, event.debugInfo)
@ -131,8 +147,10 @@ fun MessagesView(
}
}
fun onDismissActionListBottomSheet() {
isMessageActionsBottomSheetVisible = false
fun onEmojiReactionClicked(emoji: String, event: TimelineItem.Event) {
if (event.eventId == null) return
coroutineScope.launch { onDismissActionListBottomSheet() }
state.eventSink(MessagesEvents.SendReaction(emoji, event.eventId))
}
Scaffold(
@ -168,11 +186,44 @@ fun MessagesView(
},
)
var reactingToEventId: EventId? by remember { mutableStateOf(null) }
ActionListView(
state = state.actionListState,
sheetState = actionListViewBottomSheetState,
isVisible = isMessageActionsBottomSheetVisible,
onDismiss = ::onDismissActionListBottomSheet,
onActionSelected = ::onActionSelected
onDismiss = { coroutineScope.launch { onDismissActionListBottomSheet() } },
onActionSelected = ::onActionSelected,
onCustomReactionClicked = { event ->
reactingToEventId = event.eventId
coroutineScope.launch {
onDismissActionListBottomSheet()
isCustomReactionBottomSheetVisible = true
}
},
onEmojiReactionClicked = ::onEmojiReactionClicked,
)
CustomReactionBottomSheet(
isVisible = isCustomReactionBottomSheetVisible,
sheetState = customReactionBottomSheetState,
onDismiss = {
reactingToEventId = null
coroutineScope.launch {
customReactionBottomSheetState.hide()
isCustomReactionBottomSheetVisible = false
}
},
onEmojiSelected = { emoji ->
val eventId = reactingToEventId
if (eventId != null) {
state.eventSink(MessagesEvents.SendReaction(emoji.unicode, eventId))
reactingToEventId = null
coroutineScope.launch {
customReactionBottomSheetState.hide()
isCustomReactionBottomSheetVisible = false
}
}
}
)
}

View file

@ -17,6 +17,7 @@
package io.element.android.features.messages.impl.actionlist
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -35,10 +36,12 @@ import androidx.compose.material.ListItem
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AddReaction
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -81,29 +84,41 @@ fun ActionListView(
state: ActionListState,
isVisible: Boolean,
onActionSelected: (action: TimelineItemAction, TimelineItem.Event) -> Unit,
onEmojiReactionClicked: (String, TimelineItem.Event) -> Unit,
onCustomReactionClicked: (TimelineItem.Event) -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
sheetState: SheetState = rememberModalBottomSheetState()
) {
LaunchedEffect(isVisible) {
if (!isVisible) {
state.eventSink(ActionListEvents.Clear)
}
}
val targetItem = (state.target as? ActionListState.Target.Success)?.event
fun onItemActionClicked(
itemAction: TimelineItemAction,
targetItem: TimelineItem.Event
itemAction: TimelineItemAction
) {
if (targetItem == null) return
onActionSelected(itemAction, targetItem)
}
fun onEmojiReactionClicked(emoji: String) {
if (targetItem == null) return
onEmojiReactionClicked(emoji, targetItem)
}
fun onCustomReactionClicked() {
if (targetItem == null) return
onCustomReactionClicked(targetItem)
}
if (isVisible) {
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = onDismiss
) {
SheetContent(
state = state,
onActionClicked = ::onItemActionClicked,
onEmojiReactionClicked = ::onEmojiReactionClicked,
onCustomReactionClicked = ::onCustomReactionClicked,
modifier = modifier
.padding(bottom = 32.dp)
// .navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044
@ -117,8 +132,10 @@ fun ActionListView(
@Composable
private fun SheetContent(
state: ActionListState,
onActionClicked: (TimelineItemAction) -> Unit,
onEmojiReactionClicked: (String) -> Unit,
onCustomReactionClicked: () -> Unit,
modifier: Modifier = Modifier,
onActionClicked: (TimelineItemAction, TimelineItem.Event) -> Unit = { _, _ -> },
) {
when (val target = state.target) {
is ActionListState.Target.Loading,
@ -142,7 +159,11 @@ private fun SheetContent(
}
}
item {
EmojiReactionsRow(Modifier.fillMaxWidth())
EmojiReactionsRow(
onEmojiReactionClicked = onEmojiReactionClicked,
onCustomReactionClicked = onCustomReactionClicked,
modifier = Modifier.fillMaxWidth(),
)
Divider()
}
items(
@ -150,7 +171,7 @@ private fun SheetContent(
) { action ->
ListItem(
modifier = Modifier.clickable {
onActionClicked(action, target.event)
onActionClicked(action)
},
text = {
Text(
@ -265,18 +286,26 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
}
}
private val emojiRippleRadius = 24.dp
@Composable
internal fun EmojiReactionsRow(modifier: Modifier = Modifier) {
internal fun EmojiReactionsRow(
onEmojiReactionClicked: (String) -> Unit,
onCustomReactionClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier.padding(horizontal = 28.dp, vertical = 16.dp)
) {
// TODO use real emojis, have real interaction
Text("\uD83D\uDC4D", fontSize = 28.dpToSp())
Text("\uD83D\uDC4E", fontSize = 28.dpToSp())
Text("\uD83D\uDD25", fontSize = 28.dpToSp())
Text("\uFE0F", fontSize = 28.dpToSp())
Text("\uD83D\uDC4F", fontSize = 28.dpToSp())
// TODO use most recently used emojis here when available from the Rust SDK
val defaultEmojis = sequenceOf(
"👍", "👎", "🔥", "❤️", "👏"
)
for (emoji in defaultEmojis) {
EmojiButton(emoji, onEmojiReactionClicked)
}
Icon(
imageVector = Icons.Outlined.AddReaction,
contentDescription = "Emojis",
@ -284,10 +313,34 @@ internal fun EmojiReactionsRow(modifier: Modifier = Modifier) {
modifier = Modifier
.size(24.dp)
.align(Alignment.CenterVertically)
.clickable(
enabled = true,
onClick = onCustomReactionClicked,
indication = rememberRipple(bounded = false, radius = emojiRippleRadius),
interactionSource = remember { MutableInteractionSource() }
)
)
}
}
@Composable
private fun EmojiButton(
emoji: String,
onClicked: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Text(
emoji,
fontSize = 28.dpToSp(),
modifier = modifier.clickable(
enabled = true,
onClick = { onClicked(emoji) },
indication = rememberRipple(bounded = false, radius = emojiRippleRadius),
interactionSource = remember { MutableInteractionSource() }
)
)
}
@Composable
private fun Int.dpToSp(): TextUnit = with(LocalDensity.current) {
return dp.toSp()
@ -305,5 +358,10 @@ fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) st
@Composable
private fun ContentToPreview(state: ActionListState) {
SheetContent(state = state)
SheetContent(
state = state,
onActionClicked = {},
onEmojiReactionClicked = {},
onCustomReactionClicked = {},
)
}

View file

@ -0,0 +1,153 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vanniktech.emoji.Emoji
import com.vanniktech.emoji.google.GoogleEmojiProvider
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.Text
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomReactionBottomSheet(
isVisible: Boolean,
sheetState: SheetState,
onDismiss: () -> Unit,
onEmojiSelected: (Emoji) -> Unit,
modifier: Modifier = Modifier,
) {
if (isVisible) {
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState, modifier = modifier) {
EmojiPicker(onEmojiSelected = onEmojiSelected, modifier = Modifier.fillMaxSize())
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun EmojiPicker(
onEmojiSelected: (Emoji) -> Unit,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
val emojiProvider = remember { GoogleEmojiProvider() }
val categories = remember { emojiProvider.categories }
val pagerState = rememberPagerState()
Column (modifier) {
TabRow(
selectedTabIndex = pagerState.currentPage,
) {
categories.forEachIndexed { index, category ->
Tab(
text = {
Icon(
resourceId = emojiProvider.getIcon(category),
contentDescription = category.categoryNames["en"]
)
},
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch { pagerState.animateScrollToPage(index) }
}
)
}
}
HorizontalPager(
pageCount = categories.size,
state = pagerState,
modifier = Modifier.fillMaxWidth(),
) { index ->
val category = categories[index]
LazyVerticalGrid(
modifier = Modifier.fillMaxSize(),
columns = GridCells.Adaptive(minSize = 40.dp),
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
items(category.emojis, key = { it.unicode }) { item ->
Box(
modifier = Modifier
.size(40.dp)
.clickable(
enabled = true,
onClick = { onEmojiSelected(item) },
indication = rememberRipple(bounded = false, radius = 20.dp),
interactionSource = remember { MutableInteractionSource() }
),
contentAlignment = Alignment.Center
) {
Text(text = item.unicode, fontSize = 20.sp)
}
}
}
}
}
}
@Preview
@Composable
internal fun EmojiPickerLightPreview() {
ElementPreviewLight { ContentToPreview() }
}
@Preview
@Composable
internal fun EmojiPickerDarkPreview() {
ElementPreviewDark { ContentToPreview() }
}
@Composable
private fun ContentToPreview() {
EmojiPicker(
onEmojiSelected = {},
modifier = Modifier.fillMaxWidth()
)
}

View file

@ -50,8 +50,10 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -71,6 +73,25 @@ class MessagesPresenterTest {
}
}
@Test
fun `present - handle sending a reaction`() = runTest {
val room = FakeMatrixRoom()
val presenter = createMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.SendReaction("👍", AN_EVENT_ID))
assertThat(room.sendReactionCount).isEqualTo(1)
// No crashes when sending a reaction failed
room.givenSendReactionResult(Result.failure(IllegalStateException("Failed to send reaction")))
initialState.eventSink.invoke(MessagesEvents.SendReaction("👍", AN_EVENT_ID))
assertThat(room.sendReactionCount).isEqualTo(2)
}
}
@Test
fun `present - handle action forward`() = runTest {
val presenter = createMessagePresenter()
@ -309,6 +330,7 @@ class MessagesPresenterTest {
networkMonitor = FakeNetworkMonitor(),
snackbarDispatcher = SnackbarDispatcher(),
messageSummaryFormatter = FakeMessageSummaryFormatter(),
dispatchers = testCoroutineDispatchers(testScheduler),
)
}
}

View file

@ -149,6 +149,7 @@ unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
gujun_span = "me.gujun.android:span:1.7"
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0"
vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
# Analytics

View file

@ -81,6 +81,8 @@ interface MatrixRoom : Closeable {
suspend fun sendFile(file: File, fileInfo: FileInfo): Result<Unit>
suspend fun sendReaction(emoji: String, eventId: EventId): Result<Unit>
suspend fun leave(): Result<Unit>
suspend fun acceptInvitation(): Result<Unit>

View file

@ -259,6 +259,12 @@ class RustMatrixRoom(
}
}
override suspend fun sendReaction(emoji: String, eventId: EventId): Result<Unit> = withContext(Dispatchers.IO) {
runCatching {
innerRoom.sendReaction(key = emoji, eventId = eventId.value)
}
}
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> =
withContext(Dispatchers.IO) {

View file

@ -70,10 +70,14 @@ class FakeMatrixRoom(
private var setTopicResult = Result.success(Unit)
private var updateAvatarResult = Result.success(Unit)
private var removeAvatarResult = Result.success(Unit)
private var sendReactionResult = Result.success(Unit)
var sendMediaCount = 0
private set
var sendReactionCount = 0
private set
var isInviteAccepted: Boolean = false
private set
@ -124,6 +128,11 @@ class FakeMatrixRoom(
return Result.success(Unit)
}
override suspend fun sendReaction(emoji: String, eventId: EventId): Result<Unit> {
sendReactionCount++
return sendReactionResult
}
var editMessageParameter: String? = null
private set
@ -279,4 +288,8 @@ class FakeMatrixRoom(
fun givenSetTopicResult(result: Result<Unit>) {
setTopicResult = result
}
fun givenSendReactionResult(result: Result<Unit>) {
sendReactionResult = result
}
}

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a70e089f9987f96a1f8bf4b690f38981d3c802f623e4a1420661e8aa10ad1566
size 175771

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b0802887deebf06ca63678751c3553c7bac5765a13cbf85c45f89b76c324173
size 175771