Merge branch 'develop' into valere/call/fix_join_button_on_several_items
This commit is contained in:
commit
af47e2b405
376 changed files with 5383 additions and 2535 deletions
|
|
@ -586,17 +586,18 @@ class MessagesFlowNode(
|
|||
)
|
||||
}
|
||||
is TimelineItemLocationContent -> {
|
||||
val mode = ShowLocationMode.Static(
|
||||
location = event.content.location,
|
||||
senderName = event.safeSenderName,
|
||||
senderId = event.senderId,
|
||||
senderAvatarUrl = event.senderAvatar.url,
|
||||
timestamp = event.sentTimeMillis,
|
||||
assetType = event.content.assetType,
|
||||
)
|
||||
NavTarget.LocationViewer(
|
||||
mode = mode
|
||||
).takeIf { locationService.isServiceAvailable() }
|
||||
val mode = when (event.content.mode) {
|
||||
is TimelineItemLocationContent.Mode.Live -> ShowLocationMode.Live(event.senderId)
|
||||
is TimelineItemLocationContent.Mode.Static -> ShowLocationMode.Static(
|
||||
location = event.content.mode.location,
|
||||
senderName = event.safeSenderName,
|
||||
senderId = event.senderId,
|
||||
senderAvatarUrl = event.senderAvatar.url,
|
||||
timestamp = event.sentTimeMillis,
|
||||
assetType = event.content.assetType,
|
||||
)
|
||||
}
|
||||
NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() }
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,7 +289,11 @@ private fun MessageSummary(
|
|||
is TimelineItemRedactedContent,
|
||||
is TimelineItemUnknownContent -> content = { ContentForBody(textContent) }
|
||||
is TimelineItemLocationContent -> {
|
||||
content = { ContentForBody(stringResource(CommonStrings.common_shared_location)) }
|
||||
val body = when (event.content.mode) {
|
||||
is TimelineItemLocationContent.Mode.Live -> stringResource(CommonStrings.common_shared_live_location)
|
||||
is TimelineItemLocationContent.Mode.Static -> stringResource(CommonStrings.common_shared_location)
|
||||
}
|
||||
content = { ContentForBody(body) }
|
||||
}
|
||||
is TimelineItemImageContent -> {
|
||||
content = { ContentForBody(event.content.bestDescription) }
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ internal fun MessageComposerView(
|
|||
|
||||
val onSendVoiceMessage = {
|
||||
voiceMessageState.eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
state.eventSink(MessageComposerEvent.CloseSpecialMode)
|
||||
}
|
||||
|
||||
val onDeleteVoiceMessage = {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@
|
|||
package io.element.android.features.messages.impl.threads.list
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory
|
||||
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
|
||||
|
|
@ -97,12 +95,6 @@ class ThreadsListPresenter(
|
|||
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
threadsListService.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleEvent(event: ThreadsListEvents) {
|
||||
when (event) {
|
||||
ThreadsListEvents.Paginate -> if ((paginationStatus as? ThreadListPaginationStatus.Idle)?.hasMoreToLoad == true) {
|
||||
|
|
|
|||
|
|
@ -57,4 +57,6 @@ sealed interface TimelineEvent {
|
|||
data class EditPoll(
|
||||
val pollStartId: EventId,
|
||||
) : TimelineItemPollEvent
|
||||
|
||||
data object StopLiveLocationShare : TimelineItemEvent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ class TimelinePresenter(
|
|||
is TimelineEvent.EditPoll -> {
|
||||
navigator.navigateToEditPoll(event.pollStartId)
|
||||
}
|
||||
is TimelineEvent.StopLiveLocationShare -> Unit
|
||||
is TimelineEvent.FocusOnEvent -> sessionCoroutineScope.launch {
|
||||
focusRequestState.value = FocusRequestState.Requested(event.eventId, event.debounce)
|
||||
delay(event.debounce)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.ensureActiveLiveLocation
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||
import io.element.android.features.messages.impl.timeline.protection.mustBeProtected
|
||||
|
|
@ -677,6 +678,7 @@ private fun MessageEventBubbleContent(
|
|||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
TimestampPosition.Hidden -> Box(modifier) { content {} }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -772,11 +774,17 @@ private fun MessageEventBubbleContent(
|
|||
}
|
||||
}
|
||||
|
||||
val timestampPosition = when (event.content) {
|
||||
is TimelineItemImageContent -> if (event.content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
||||
is TimelineItemVideoContent -> if (event.content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
||||
is TimelineItemStickerContent,
|
||||
is TimelineItemLocationContent -> TimestampPosition.Overlay
|
||||
val timestampPosition = when (val content = event.content) {
|
||||
is TimelineItemImageContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
||||
is TimelineItemVideoContent -> if (content.showCaption) TimestampPosition.Aligned else TimestampPosition.Overlay
|
||||
is TimelineItemStickerContent -> TimestampPosition.Overlay
|
||||
is TimelineItemLocationContent -> {
|
||||
val content = content.ensureActiveLiveLocation()
|
||||
val shouldHide = content.mode is TimelineItemLocationContent.Mode.Live &&
|
||||
content.mode.isActive &&
|
||||
content.mode.canStop
|
||||
if (shouldHide) TimestampPosition.Hidden else TimestampPosition.Overlay
|
||||
}
|
||||
is TimelineItemPollContent -> TimestampPosition.Below
|
||||
else -> TimestampPosition.Default
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ enum class TimestampPosition {
|
|||
/**
|
||||
* Timestamp should always be rendered below the timeline event content (eg. poll).
|
||||
*/
|
||||
Below;
|
||||
Below,
|
||||
|
||||
/**
|
||||
* Timestamp should be hidden.
|
||||
*/
|
||||
Hidden;
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.ensureActiveLiveLocation
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
|
|
@ -71,10 +72,13 @@ fun TimelineItemEventContentView(
|
|||
onContentLayoutChange = onContentLayoutChange,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemLocationContent -> TimelineItemLocationView(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemLocationContent -> {
|
||||
TimelineItemLocationView(
|
||||
content = content.ensureActiveLiveLocation(),
|
||||
onStopLiveLocationClick = { eventSink(TimelineEvent.StopLiveLocationShare) },
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
is TimelineItemImageContent -> TimelineItemImageView(
|
||||
content = content,
|
||||
hideMediaContent = hideMediaContent,
|
||||
|
|
|
|||
|
|
@ -8,33 +8,153 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.location.api.StaticMapView
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContentProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun TimelineItemLocationView(
|
||||
content: TimelineItemLocationContent,
|
||||
onStopLiveLocationClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
StaticMapView(
|
||||
Box(modifier = modifier.fillMaxWidth()) {
|
||||
StaticMapView(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 188.dp),
|
||||
pinVariant = content.pinVariant,
|
||||
location = content.location,
|
||||
zoom = 15.0,
|
||||
contentDescription = content.description
|
||||
)
|
||||
|
||||
if (content.mode is TimelineItemLocationContent.Mode.Live) {
|
||||
LiveLocationOverlay(
|
||||
mode = content.mode,
|
||||
onStopClick = onStopLiveLocationClick,
|
||||
modifier = Modifier.align(Alignment.BottomStart)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LiveLocationOverlay(
|
||||
mode: TimelineItemLocationContent.Mode.Live,
|
||||
onStopClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 188.dp),
|
||||
pinVariant = content.pinVariant,
|
||||
lat = content.location.lat,
|
||||
lon = content.location.lon,
|
||||
zoom = 15.0,
|
||||
contentDescription = content.body
|
||||
)
|
||||
.background(ElementTheme.colors.bgCanvasDefault.copy(alpha = 0.9f))
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val iconShape = RoundedCornerShape(8.dp)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (mode.isActive) ElementTheme.colors.iconQuaternaryAlpha else Color.Transparent,
|
||||
shape = iconShape,
|
||||
)
|
||||
.background(
|
||||
color = if (mode.isActive) {
|
||||
ElementTheme.colors.bgCanvasDefault
|
||||
} else {
|
||||
ElementTheme.colors.bgSubtleSecondary
|
||||
},
|
||||
shape = iconShape
|
||||
)
|
||||
) {
|
||||
if (mode.isLoading) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 2.dp,
|
||||
color = ElementTheme.colors.iconSecondary,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(20.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.LocationPinSolid(),
|
||||
contentDescription = null,
|
||||
tint = if (mode.isActive) {
|
||||
ElementTheme.colors.iconAccentPrimary
|
||||
} else {
|
||||
ElementTheme.colors.iconDisabled
|
||||
},
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = if (mode.isActive) {
|
||||
stringResource(CommonStrings.common_live_location)
|
||||
} else {
|
||||
stringResource(CommonStrings.common_live_location_ended)
|
||||
},
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
if (mode.isActive) {
|
||||
Text(
|
||||
text = mode.endsAt,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (mode.isActive && mode.canStop) {
|
||||
IconButton(
|
||||
onClick = onStopClick,
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
containerColor = ElementTheme.colors.bgCriticalPrimary,
|
||||
contentColor = ElementTheme.colors.iconOnSolidPrimary,
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Stop(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
|
|
@ -43,5 +163,6 @@ internal fun TimelineItemLocationViewPreview(@PreviewParameter(TimelineItemLocat
|
|||
ElementPreview {
|
||||
TimelineItemLocationView(
|
||||
content = content,
|
||||
onStopLiveLocationClick = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -36,6 +38,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StickerConten
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
|
||||
@Inject
|
||||
class TimelineItemContentFactory(
|
||||
|
|
@ -50,6 +54,8 @@ class TimelineItemContentFactory(
|
|||
private val failedToParseMessageFactory: TimelineItemContentFailedToParseMessageFactory,
|
||||
private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory,
|
||||
private val sessionId: SessionId,
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val stringProvider: StringProvider,
|
||||
) {
|
||||
suspend fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
return create(
|
||||
|
|
@ -105,19 +111,24 @@ class TimelineItemContentFactory(
|
|||
val lastKnownLocation = itemContent.locations.mapNotNull { beacon ->
|
||||
Location.fromGeoUri(beacon.geoUri)
|
||||
}.lastOrNull()
|
||||
if (lastKnownLocation != null) {
|
||||
TimelineItemLocationContent(
|
||||
body = itemContent.body.trimEnd(),
|
||||
description = itemContent.description?.trimEnd(),
|
||||
assetType = itemContent.assetType,
|
||||
senderId = sender,
|
||||
senderProfile = senderProfile,
|
||||
location = lastKnownLocation,
|
||||
mode = TimelineItemLocationContent.Mode.Live(isActive = itemContent.isLive)
|
||||
)
|
||||
} else {
|
||||
TimelineItemUnknownContent
|
||||
}
|
||||
|
||||
val endsAt = dateFormatter.format(
|
||||
timestamp = itemContent.endTimestamp,
|
||||
mode = DateFormatterMode.TimeOnly
|
||||
)
|
||||
// Always create content, location can be null for "loading/waiting" state
|
||||
TimelineItemLocationContent(
|
||||
description = itemContent.description?.trimEnd(),
|
||||
assetType = itemContent.assetType,
|
||||
senderId = sender,
|
||||
senderProfile = senderProfile,
|
||||
mode = TimelineItemLocationContent.Mode.Live(
|
||||
lastKnownLocation = lastKnownLocation,
|
||||
isActive = itemContent.isLive,
|
||||
endsAt = stringProvider.getString(CommonStrings.common_ends_at, endsAt),
|
||||
endTimestamp = itemContent.endTimestamp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,13 +150,11 @@ class TimelineItemContentMessageFactory(
|
|||
)
|
||||
} else {
|
||||
TimelineItemLocationContent(
|
||||
body = body,
|
||||
location = location,
|
||||
description = messageType.description,
|
||||
senderId = senderId,
|
||||
senderProfile = senderProfile,
|
||||
assetType = messageType.assetType,
|
||||
mode = TimelineItemLocationContent.Mode.Static
|
||||
mode = TimelineItemLocationContent.Mode.Static(location = location)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
|
|||
aTimelineItemUnknownContent(),
|
||||
aTimelineItemTextContent().copy(isEdited = true),
|
||||
aTimelineItemTextContent(body = AN_EMOJI_ONLY_TEXT),
|
||||
aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(isActive = true)),
|
||||
aTimelineItemLocationContent(
|
||||
mode = TimelineItemLocationContent.Mode.Live(isActive = true, endsAt = "Ends at 12:34", endTimestamp = 0L, lastKnownLocation = null)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.produceState
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.libraries.designsystem.components.PinVariant
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -17,17 +19,25 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
data class TimelineItemLocationContent(
|
||||
val body: String,
|
||||
val senderId: UserId,
|
||||
val senderProfile: ProfileDetails,
|
||||
val location: Location,
|
||||
val description: String? = null,
|
||||
val assetType: AssetType? = null,
|
||||
val mode: Mode,
|
||||
) : TimelineItemEventContent {
|
||||
val pinVariant = when (mode) {
|
||||
val location = when (mode) {
|
||||
is Mode.Live -> mode.lastKnownLocation
|
||||
is Mode.Static -> mode.location
|
||||
}
|
||||
|
||||
/**
|
||||
* The pin variant to display on the map.
|
||||
* Returns a default variant when location is null (map will show loading placeholder anyway).
|
||||
*/
|
||||
val pinVariant: PinVariant = when (mode) {
|
||||
is Mode.Live -> {
|
||||
if (mode.isActive) {
|
||||
PinVariant.UserLocation(avatarData = senderAvatar(), isLive = true)
|
||||
|
|
@ -35,7 +45,7 @@ data class TimelineItemLocationContent(
|
|||
PinVariant.StaleLocation
|
||||
}
|
||||
}
|
||||
Mode.Static -> {
|
||||
is Mode.Static -> {
|
||||
when (assetType) {
|
||||
AssetType.PIN -> PinVariant.PinnedLocation
|
||||
AssetType.SENDER,
|
||||
|
|
@ -53,9 +63,57 @@ data class TimelineItemLocationContent(
|
|||
)
|
||||
|
||||
sealed interface Mode {
|
||||
data object Static : Mode
|
||||
data class Live(val isActive: Boolean) : Mode
|
||||
data class Static(
|
||||
val location: Location,
|
||||
) : Mode
|
||||
|
||||
data class Live(
|
||||
val lastKnownLocation: Location?,
|
||||
val isActive: Boolean,
|
||||
val endsAt: String,
|
||||
val endTimestamp: Long,
|
||||
val canStop: Boolean = false,
|
||||
) : Mode {
|
||||
val isLoading = lastKnownLocation == null && isActive
|
||||
}
|
||||
}
|
||||
|
||||
override val type: String = "TimelineItemLocationContent"
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the isActive value if needed, to make sure endTimestamp is used in absence of stop event.
|
||||
*/
|
||||
@Composable
|
||||
internal fun TimelineItemLocationContent.ensureActiveLiveLocation(
|
||||
currentTimeMillis: () -> Long = System::currentTimeMillis,
|
||||
): TimelineItemLocationContent {
|
||||
return when (mode) {
|
||||
is TimelineItemLocationContent.Mode.Live -> {
|
||||
val isActive = rememberIsLiveLocationActive(mode, currentTimeMillis)
|
||||
copy(mode = mode.copy(isActive = isActive))
|
||||
}
|
||||
is TimelineItemLocationContent.Mode.Static -> this
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberIsLiveLocationActive(
|
||||
mode: TimelineItemLocationContent.Mode.Live,
|
||||
currentTimeMillis: () -> Long,
|
||||
): Boolean {
|
||||
fun TimelineItemLocationContent.Mode.Live.isActive(): Boolean {
|
||||
return isActive && endTimestamp > currentTimeMillis()
|
||||
}
|
||||
return produceState(
|
||||
initialValue = mode.isActive(),
|
||||
key1 = mode.endTimestamp,
|
||||
key2 = mode.isActive,
|
||||
) {
|
||||
if (mode.isActive) {
|
||||
val remainingMillis = mode.endTimestamp - currentTimeMillis()
|
||||
delay(remainingMillis)
|
||||
}
|
||||
value = false
|
||||
}.value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,24 +18,56 @@ open class TimelineItemLocationContentProvider : PreviewParameterProvider<Timeli
|
|||
override val values: Sequence<TimelineItemLocationContent>
|
||||
get() = sequenceOf(
|
||||
aTimelineItemLocationContent(),
|
||||
aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(isActive = true)),
|
||||
aTimelineItemLocationContent(mode = TimelineItemLocationContent.Mode.Live(isActive = false)),
|
||||
aTimelineItemLocationContent(
|
||||
mode = TimelineItemLocationContent.Mode.Live(
|
||||
isActive = true,
|
||||
endsAt = "Ends at 12:34",
|
||||
endTimestamp = 0L,
|
||||
canStop = true,
|
||||
lastKnownLocation = aLocation()
|
||||
),
|
||||
),
|
||||
aTimelineItemLocationContent(
|
||||
mode = TimelineItemLocationContent.Mode.Live(
|
||||
isActive = true,
|
||||
endsAt = "Ends at 12:34",
|
||||
endTimestamp = 0L,
|
||||
lastKnownLocation = aLocation()
|
||||
),
|
||||
),
|
||||
aTimelineItemLocationContent(
|
||||
mode = TimelineItemLocationContent.Mode.Live(
|
||||
isActive = true,
|
||||
endsAt = "Ends at 12:34",
|
||||
endTimestamp = 0L,
|
||||
lastKnownLocation = null
|
||||
),
|
||||
),
|
||||
aTimelineItemLocationContent(
|
||||
mode = TimelineItemLocationContent.Mode.Live(
|
||||
isActive = false,
|
||||
endsAt = "",
|
||||
endTimestamp = 0L,
|
||||
lastKnownLocation = aLocation()
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aTimelineItemLocationContent(
|
||||
body: String = "",
|
||||
senderId: UserId = UserId("@sender:matrix.org"),
|
||||
senderProfile: ProfileDetails = aProfileDetailsReady(),
|
||||
mode: TimelineItemLocationContent.Mode = TimelineItemLocationContent.Mode.Static,
|
||||
description: String? = null,
|
||||
mode: TimelineItemLocationContent.Mode = TimelineItemLocationContent.Mode.Static(aLocation()),
|
||||
) = TimelineItemLocationContent(
|
||||
body = body,
|
||||
location = Location(
|
||||
lat = 52.2445,
|
||||
lon = 0.7186,
|
||||
accuracy = 5000f,
|
||||
),
|
||||
senderId = senderId,
|
||||
senderProfile = senderProfile,
|
||||
mode = mode
|
||||
description = description,
|
||||
mode = mode,
|
||||
)
|
||||
|
||||
fun aLocation() = Location(
|
||||
lat = 52.2445,
|
||||
lon = 0.7186,
|
||||
accuracy = 5000f,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.compose.ui.semantics.Role
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.theme.Theme
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemAspectRatioBox
|
||||
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
|
|
@ -49,7 +50,7 @@ fun ProtectedView(
|
|||
.background(Color(0x99000000)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
ElementTheme(darkTheme = false, applySystemBarsUpdate = false) {
|
||||
ElementTheme(theme = Theme.Light, applySystemBarsUpdate = false) {
|
||||
// Not using a button to be able to have correct size
|
||||
Text(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ class DefaultMessageSummaryFormatter(
|
|||
is TimelineItemTextBasedContent -> content.plainText
|
||||
is TimelineItemProfileChangeContent -> content.body
|
||||
is TimelineItemStateContent -> content.body
|
||||
is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location)
|
||||
is TimelineItemLocationContent -> when (content.mode) {
|
||||
is TimelineItemLocationContent.Mode.Live -> context.getString(CommonStrings.common_shared_live_location)
|
||||
is TimelineItemLocationContent.Mode.Static -> context.getString(CommonStrings.common_shared_location)
|
||||
}
|
||||
is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
|
||||
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
|
||||
is TimelineItemPollContent -> content.question
|
||||
|
|
|
|||
|
|
@ -34,10 +34,12 @@ import io.element.android.libraries.audio.api.AudioFocus
|
|||
import io.element.android.libraries.audio.api.AudioFocusRequester
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
|
||||
import io.element.android.libraries.permissions.api.PermissionsEvent
|
||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
|
||||
import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||
|
|
@ -151,7 +153,7 @@ class DefaultVoiceMessageComposerPresenter(
|
|||
}
|
||||
}
|
||||
|
||||
fun sendVoiceMessage() {
|
||||
fun sendVoiceMessage(inReplyToEventId: EventId?) {
|
||||
val finishedState = recorderState as? VoiceRecorderState.Finished
|
||||
if (finishedState == null) {
|
||||
val exception = VoiceMessageException.FileException("No file to send")
|
||||
|
|
@ -170,6 +172,7 @@ class DefaultVoiceMessageComposerPresenter(
|
|||
file = finishedState.file,
|
||||
mimeType = finishedState.mimeType,
|
||||
waveform = finishedState.waveform,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
if (result.isFailure) {
|
||||
showSendFailureDialog = true
|
||||
|
|
@ -183,8 +186,13 @@ class DefaultVoiceMessageComposerPresenter(
|
|||
when (event) {
|
||||
is VoiceMessageComposerEvent.RecorderEvent -> handleVoiceMessageRecorderEvent(event.recorderEvent)
|
||||
is VoiceMessageComposerEvent.PlayerEvent -> handleVoiceMessagePlayerEvent(event.playerEvent)
|
||||
is VoiceMessageComposerEvent.SendVoiceMessage -> localCoroutineScope.launch {
|
||||
sendVoiceMessage()
|
||||
is VoiceMessageComposerEvent.SendVoiceMessage -> {
|
||||
// Capture reply info eagerly before any coroutine dispatch, since CloseSpecialMode
|
||||
// may reset composerMode before the coroutine runs.
|
||||
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
|
||||
localCoroutineScope.launch {
|
||||
sendVoiceMessage(inReplyToEventId)
|
||||
}
|
||||
}
|
||||
VoiceMessageComposerEvent.DeleteVoiceMessage -> {
|
||||
player.pause()
|
||||
|
|
@ -280,11 +288,13 @@ class DefaultVoiceMessageComposerPresenter(
|
|||
file: File,
|
||||
mimeType: String,
|
||||
waveform: List<Float>,
|
||||
inReplyToEventId: EventId? = null,
|
||||
): Result<Unit> {
|
||||
val result = mediaSender.sendVoiceMessage(
|
||||
uri = file.toUri(),
|
||||
mimeType = mimeType,
|
||||
waveForm = waveform,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
|
||||
if (result.isFailure) {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
<string name="screen_room_attachment_source_camera_video">"Video aufnehmen"</string>
|
||||
<string name="screen_room_attachment_source_files">"Anhang"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Foto- und Videogalerie"</string>
|
||||
<string name="screen_room_attachment_source_location">"Standort"</string>
|
||||
<string name="screen_room_attachment_source_location">"Standort teilen"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Umfrage"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Textformatierung"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Der Nachrichtenverlauf ist derzeit nicht verfügbar"</string>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
<string name="screen_room_attachment_source_camera_video">"Snimi videozapis"</string>
|
||||
<string name="screen_room_attachment_source_files">"Privitak"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Biblioteka fotografija i videozapisa"</string>
|
||||
<string name="screen_room_attachment_source_location">"Lokacija"</string>
|
||||
<string name="screen_room_attachment_source_location">"Dijeli lokaciju"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Anketa"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Oblikovanje teksta"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Povijest poruka trenutačno nije dostupna."</string>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
<string name="emoji_picker_category_objects">"Đồ vật"</string>
|
||||
<string name="emoji_picker_category_people">"Mặt cười & mọi người"</string>
|
||||
<string name="emoji_picker_category_places">"Du lịch và địa danh"</string>
|
||||
<string name="emoji_picker_category_recent">"Biểu tượng cảm xúc gần đây"</string>
|
||||
<string name="emoji_picker_category_symbols">"Biểu tượng"</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Xử lý phương tiện tải lên không thành công, vui lòng thử lại."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Không thể tải lên tệp phương tiện. Vui lòng thử lại."</string>
|
||||
|
|
@ -44,6 +45,12 @@
|
|||
<string name="screen_room_timeline_less_reactions">"Thu gọn"</string>
|
||||
<string name="screen_room_timeline_message_copied">"Đã sao chép tin nhắn"</string>
|
||||
<string name="screen_room_timeline_no_permission_to_post">"Bạn không có quyền gửi tin nhắn trong phòng này"</string>
|
||||
<plurals name="screen_room_timeline_reaction_a11y">
|
||||
<item quantity="other">"%1$d thành viên đã phản ứng với %2$s"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_reaction_including_you_a11y">
|
||||
<item quantity="other">"Bạn và thành viên %1$d đã phản ứng với%2$s"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_timeline_reactions_show_less">"Thu gọn"</string>
|
||||
<string name="screen_room_timeline_reactions_show_more">"Xem thêm"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"Mới"</string>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
<string name="screen_room_attachment_source_camera_video">"錄影"</string>
|
||||
<string name="screen_room_attachment_source_files">"附件"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"照片與影片庫"</string>
|
||||
<string name="screen_room_attachment_source_location">"位置"</string>
|
||||
<string name="screen_room_attachment_source_location">"分享位置"</string>
|
||||
<string name="screen_room_attachment_source_poll">"投票"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"格式化文字"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"目前無法檢視訊息歷史紀錄。"</string>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
|||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
||||
|
|
@ -72,6 +73,8 @@ internal fun aTimelineItemContentFactory(
|
|||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
|
||||
sessionId = matrixClient.sessionId,
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
|
||||
internal fun TestScope.aTimelineItemsFactory(
|
||||
|
|
|
|||
|
|
@ -78,9 +78,7 @@ import org.robolectric.RobolectricTestRunner
|
|||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class TimelineItemContentMessageFactoryTest {
|
||||
@Suppress("LargeClass") @RunWith(RobolectricTestRunner::class) class TimelineItemContentMessageFactoryTest {
|
||||
@Test
|
||||
fun `test create OtherMessageType`() = runTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
|
|
@ -110,11 +108,9 @@ class TimelineItemContentMessageFactoryTest {
|
|||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
val expected = TimelineItemLocationContent(
|
||||
body = "body",
|
||||
location = Location(lat = 1.0, lon = 2.0, accuracy = null),
|
||||
description = "description",
|
||||
assetType = assetType,
|
||||
mode = TimelineItemLocationContent.Mode.Static,
|
||||
mode = TimelineItemLocationContent.Mode.Static(location = Location(lat = 1.0, lon = 2.0, accuracy = null)),
|
||||
senderId = A_USER_ID,
|
||||
senderProfile = aProfileDetails(),
|
||||
)
|
||||
|
|
@ -166,16 +162,11 @@ class TimelineItemContentMessageFactoryTest {
|
|||
senderProfile = aProfileDetails(),
|
||||
eventId = AN_EVENT_ID,
|
||||
) as TimelineItemTextContent
|
||||
val expected = TimelineItemTextContent(
|
||||
body = "https://www.example.org",
|
||||
htmlDocument = null,
|
||||
isEdited = false,
|
||||
formattedBody = buildSpannedString {
|
||||
inSpans(URLSpan("https://www.example.org")) {
|
||||
append("https://www.example.org")
|
||||
}
|
||||
val expected = TimelineItemTextContent(body = "https://www.example.org", htmlDocument = null, isEdited = false, formattedBody = buildSpannedString {
|
||||
inSpans(URLSpan("https://www.example.org")) {
|
||||
append("https://www.example.org")
|
||||
}
|
||||
)
|
||||
})
|
||||
assertThat(result.body).isEqualTo(expected.body)
|
||||
assertThat(result.htmlDocument).isEqualTo(expected.htmlDocument)
|
||||
assertThat(result.plainText).isEqualTo(expected.plainText)
|
||||
|
|
@ -200,9 +191,7 @@ class TimelineItemContentMessageFactoryTest {
|
|||
append("and manually added link")
|
||||
}
|
||||
}.toSpannable()
|
||||
val sut = createTimelineItemContentMessageFactory(
|
||||
domConverterTransform = { expected }
|
||||
)
|
||||
val sut = createTimelineItemContentMessageFactory(domConverterTransform = { expected })
|
||||
val result = sut.create(
|
||||
content = createMessageContent(
|
||||
type = TextMessageType(
|
||||
|
|
@ -219,9 +208,7 @@ class TimelineItemContentMessageFactoryTest {
|
|||
|
||||
@Test
|
||||
fun `test create TextMessageType with unknown formatted body does nothing`() = runTest {
|
||||
val sut = createTimelineItemContentMessageFactory(
|
||||
htmlConverterTransform = { it }
|
||||
)
|
||||
val sut = createTimelineItemContentMessageFactory(htmlConverterTransform = { it })
|
||||
val result = sut.create(
|
||||
content = createMessageContent(
|
||||
type = TextMessageType(
|
||||
|
|
@ -356,10 +343,10 @@ class TimelineItemContentMessageFactoryTest {
|
|||
formattedCaption = null,
|
||||
source = MediaSource("url"),
|
||||
info = AudioInfo(
|
||||
duration = 1.minutes,
|
||||
size = 123L,
|
||||
mimetype = MimeTypes.Mp3,
|
||||
)
|
||||
duration = 1.minutes,
|
||||
size = 123L,
|
||||
mimetype = MimeTypes.Mp3,
|
||||
)
|
||||
),
|
||||
isEdited = true,
|
||||
),
|
||||
|
|
@ -597,16 +584,16 @@ class TimelineItemContentMessageFactoryTest {
|
|||
formattedCaption = null,
|
||||
source = MediaSource("url"),
|
||||
info = FileInfo(
|
||||
mimetype = MimeTypes.Pdf,
|
||||
size = 123L,
|
||||
thumbnailInfo = ThumbnailInfo(
|
||||
height = 10L,
|
||||
width = 5L,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 111L,
|
||||
),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
)
|
||||
mimetype = MimeTypes.Pdf,
|
||||
size = 123L,
|
||||
thumbnailInfo = ThumbnailInfo(
|
||||
height = 10L,
|
||||
width = 5L,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 111L,
|
||||
),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
)
|
||||
),
|
||||
isEdited = true,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import io.element.android.libraries.audio.api.AudioFocusRequester
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
|
|
@ -46,7 +47,9 @@ import io.element.android.libraries.voicerecorder.api.VoiceRecorder
|
|||
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -59,6 +62,7 @@ import java.io.File
|
|||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class DefaultVoiceMessageComposerPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
|
@ -406,6 +410,37 @@ class DefaultVoiceMessageComposerPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - send voice message passes reply event ID only when in reply mode`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
presenter.test {
|
||||
// First send in Normal mode (default composerMode).
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
||||
val idleAfterFirstSend = awaitItem()
|
||||
assertThat(idleAfterFirstSend.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
|
||||
// Switching to reply mode does not trigger recomposition, so reuse the prior eventSink.
|
||||
messageComposerContext.composerMode = aReplyMode()
|
||||
idleAfterFirstSend.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop))
|
||||
awaitItem().eventSink(VoiceMessageComposerEvent.SendVoiceMessage)
|
||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||
|
||||
sendVoiceMessageResult.assertions().isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(any(), any(), any(), value(null)),
|
||||
listOf(any(), any(), any(), value(AN_EVENT_ID)),
|
||||
)
|
||||
|
||||
testPauseAndDestroy(finalState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - send while playing`() = runTest {
|
||||
val presenter = createDefaultVoiceMessageComposerPresenter()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue