Merge branch 'develop' into feature/fga/better_timeline_scroll
This commit is contained in:
commit
8f01e8133f
156 changed files with 3806 additions and 539 deletions
|
|
@ -25,7 +25,6 @@ import io.element.android.features.messages.impl.timeline.components.customreact
|
|||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -48,7 +47,7 @@ fun aMessagesState() = MessagesState(
|
|||
roomAvatar = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom),
|
||||
userHasPermissionToSendMessage = true,
|
||||
composerState = aMessageComposerState().copy(
|
||||
text = StableCharSequence("Hello"),
|
||||
text = "Hello",
|
||||
isFullScreen = false,
|
||||
mode = MessageComposerMode.Normal("Hello"),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.messagecomposer
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.messages.api.MessageComposerContext
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import javax.inject.Inject
|
||||
|
||||
@SingleIn(RoomScope::class)
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class MessageComposerContextImpl @Inject constructor() : MessageComposerContext {
|
||||
override var composerMode: MessageComposerMode by mutableStateOf(MessageComposerMode.Normal(""))
|
||||
internal set
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ sealed interface MessageComposerEvents {
|
|||
data class SendMessage(val message: String) : MessageComposerEvents
|
||||
object CloseSpecialMode : MessageComposerEvents
|
||||
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
|
||||
data class UpdateText(val text: CharSequence) : MessageComposerEvents
|
||||
data class UpdateText(val text: String) : MessageComposerEvents
|
||||
object AddAttachment : MessageComposerEvents
|
||||
object DismissAttachmentMenu : MessageComposerEvents
|
||||
sealed interface PickAttachmentSource : MessageComposerEvents {
|
||||
|
|
|
|||
|
|
@ -34,8 +34,6 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
|||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
||||
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.core.data.toStableCharSequence
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
|
@ -64,6 +62,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
private val mediaSender: MediaSender,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val messageComposerContext: MessageComposerContextImpl,
|
||||
) : Presenter<MessageComposerState> {
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
|
|
@ -93,18 +92,15 @@ class MessageComposerPresenter @Inject constructor(
|
|||
val hasFocus = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val text: MutableState<StableCharSequence> = remember {
|
||||
mutableStateOf(StableCharSequence(""))
|
||||
}
|
||||
val composerMode: MutableState<MessageComposerMode> = rememberSaveable {
|
||||
mutableStateOf(MessageComposerMode.Normal(""))
|
||||
val text: MutableState<String> = rememberSaveable {
|
||||
mutableStateOf("")
|
||||
}
|
||||
|
||||
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(composerMode.value) {
|
||||
when (val modeValue = composerMode.value) {
|
||||
is MessageComposerMode.Edit -> text.value = modeValue.defaultContent.toStableCharSequence()
|
||||
LaunchedEffect(messageComposerContext.composerMode) {
|
||||
when (val modeValue = messageComposerContext.composerMode) {
|
||||
is MessageComposerMode.Edit -> text.value = modeValue.defaultContent
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -122,20 +118,24 @@ class MessageComposerPresenter @Inject constructor(
|
|||
|
||||
is MessageComposerEvents.FocusChanged -> hasFocus.value = event.hasFocus
|
||||
|
||||
is MessageComposerEvents.UpdateText -> text.value = event.text.toStableCharSequence()
|
||||
is MessageComposerEvents.UpdateText -> text.value = event.text
|
||||
MessageComposerEvents.CloseSpecialMode -> {
|
||||
text.value = "".toStableCharSequence()
|
||||
composerMode.setToNormal()
|
||||
text.value = ""
|
||||
messageComposerContext.composerMode = MessageComposerMode.Normal("")
|
||||
}
|
||||
|
||||
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text)
|
||||
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(
|
||||
text = event.message,
|
||||
updateComposerMode = { messageComposerContext.composerMode = it },
|
||||
textState = text
|
||||
)
|
||||
is MessageComposerEvents.SetMode -> {
|
||||
composerMode.value = event.composerMode
|
||||
messageComposerContext.composerMode = event.composerMode
|
||||
analyticsService.capture(
|
||||
Composer(
|
||||
inThread = false,
|
||||
isEditing = composerMode.value is MessageComposerMode.Edit,
|
||||
isReply = composerMode.value is MessageComposerMode.Reply,
|
||||
inThread = messageComposerContext.composerMode.inThread,
|
||||
isEditing = messageComposerContext.composerMode.isEditing,
|
||||
isReply = messageComposerContext.composerMode.isReply,
|
||||
isLocation = false,
|
||||
)
|
||||
)
|
||||
|
|
@ -171,7 +171,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
text = text.value,
|
||||
isFullScreen = isFullScreen.value,
|
||||
hasFocus = hasFocus.value,
|
||||
mode = composerMode.value,
|
||||
mode = messageComposerContext.composerMode,
|
||||
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
||||
attachmentsState = attachmentsState.value,
|
||||
eventSink = ::handleEvents
|
||||
|
|
@ -184,31 +184,30 @@ class MessageComposerPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MessageComposerMode>.setToNormal() {
|
||||
value = MessageComposerMode.Normal("")
|
||||
}
|
||||
|
||||
private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState<MessageComposerMode>, textState: MutableState<StableCharSequence>) =
|
||||
launch {
|
||||
val capturedMode = composerMode.value
|
||||
// Reset composer right away
|
||||
textState.value = "".toStableCharSequence()
|
||||
composerMode.setToNormal()
|
||||
when (capturedMode) {
|
||||
is MessageComposerMode.Normal -> room.sendMessage(text)
|
||||
is MessageComposerMode.Edit -> {
|
||||
val eventId = capturedMode.eventId
|
||||
val transactionId = capturedMode.transactionId
|
||||
room.editMessage(eventId, transactionId, text)
|
||||
}
|
||||
|
||||
is MessageComposerMode.Quote -> TODO()
|
||||
is MessageComposerMode.Reply -> room.replyMessage(
|
||||
capturedMode.eventId,
|
||||
text
|
||||
)
|
||||
private fun CoroutineScope.sendMessage(
|
||||
text: String,
|
||||
updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit,
|
||||
textState: MutableState<String>
|
||||
) = launch {
|
||||
val capturedMode = messageComposerContext.composerMode
|
||||
// Reset composer right away
|
||||
textState.value = ""
|
||||
updateComposerMode(MessageComposerMode.Normal(""))
|
||||
when (capturedMode) {
|
||||
is MessageComposerMode.Normal -> room.sendMessage(text)
|
||||
is MessageComposerMode.Edit -> {
|
||||
val eventId = capturedMode.eventId
|
||||
val transactionId = capturedMode.transactionId
|
||||
room.editMessage(eventId, transactionId, text)
|
||||
}
|
||||
|
||||
is MessageComposerMode.Quote -> TODO()
|
||||
is MessageComposerMode.Reply -> room.replyMessage(
|
||||
capturedMode.eventId,
|
||||
text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.sendAttachment(
|
||||
attachment: Attachment,
|
||||
|
|
|
|||
|
|
@ -18,13 +18,12 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class MessageComposerState(
|
||||
val text: StableCharSequence?,
|
||||
val text: String?,
|
||||
val isFullScreen: Boolean,
|
||||
val hasFocus: Boolean,
|
||||
val mode: MessageComposerMode,
|
||||
|
|
@ -32,7 +31,7 @@ data class MessageComposerState(
|
|||
val attachmentsState: AttachmentsState,
|
||||
val eventSink: (MessageComposerEvents) -> Unit
|
||||
) {
|
||||
val isSendButtonVisible: Boolean = text?.charSequence.isNullOrEmpty().not()
|
||||
val isSendButtonVisible: Boolean = text.isNullOrEmpty().not()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package io.element.android.features.messages.impl.messagecomposer
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
|
||||
open class MessageComposerStateProvider : PreviewParameterProvider<MessageComposerState> {
|
||||
|
|
@ -28,7 +27,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider<MessageCompos
|
|||
}
|
||||
|
||||
fun aMessageComposerState() = MessageComposerState(
|
||||
text = StableCharSequence(""),
|
||||
text = "",
|
||||
isFullScreen = false,
|
||||
hasFocus = false,
|
||||
mode = MessageComposerMode.Normal(content = ""),
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ fun MessageComposerView(
|
|||
state.eventSink(MessageComposerEvents.CloseSpecialMode)
|
||||
}
|
||||
|
||||
fun onComposerTextChange(text: CharSequence) {
|
||||
fun onComposerTextChange(text: String) {
|
||||
state.eventSink(MessageComposerEvents.UpdateText(text))
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ fun MessageComposerView(
|
|||
onAddAttachment = ::onAddAttachment,
|
||||
onFocusChanged = ::onFocusChanged,
|
||||
composerCanSendMessage = state.isSendButtonVisible,
|
||||
composerText = state.text?.charSequence?.toString(),
|
||||
composerText = state.text,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.aTimelin
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
|
|
@ -103,7 +104,7 @@ fun aTimelineItemDaySeparator(): TimelineItem.Virtual {
|
|||
|
||||
internal fun aTimelineItemEvent(
|
||||
eventId: EventId = EventId("\$" + Random.nextInt().toString()),
|
||||
transactionId: String? = null,
|
||||
transactionId: TransactionId? = null,
|
||||
isMine: Boolean = false,
|
||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ package io.element.android.features.messages.impl.timeline.components
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineEncryptedHistoryBannerView
|
||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
|
||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
||||
|
||||
@Composable
|
||||
|
|
@ -32,5 +33,7 @@ fun TimelineItemVirtualRow(
|
|||
when (virtual.model) {
|
||||
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
||||
TimelineItemReadMarkerModel -> return
|
||||
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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.virtual
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.R
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
@Composable
|
||||
fun TimelineEncryptedHistoryBannerView(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 32.dp)
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.border(1.dp, ElementTheme.colors.borderInfoSubtle, MaterialTheme.shapes.small)
|
||||
.background(ElementTheme.colors.bgInfoSubtle)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = "Info",
|
||||
tint = ElementTheme.colors.iconInfoPrimary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.screen_room_encrypted_history_banner),
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
color = ElementTheme.colors.textInfoPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
internal fun TimelineEncryptedHistoryBannerViewPreview() {
|
||||
ElementTheme {
|
||||
TimelineEncryptedHistoryBannerView()
|
||||
}
|
||||
}
|
||||
|
|
@ -45,7 +45,6 @@ class TimelineItemsFactory @Inject constructor(
|
|||
private val virtualItemFactory: TimelineItemVirtualFactory,
|
||||
private val timelineItemGrouper: TimelineItemGrouper,
|
||||
) {
|
||||
|
||||
private val timelineItems = MutableStateFlow(persistentListOf<TimelineItem>())
|
||||
private val timelineItemsCache = arrayListOf<TimelineItem?>()
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.messages.impl.timeline.factories.virtual
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
|
|
@ -30,8 +31,13 @@ class TimelineItemVirtualFactory @Inject constructor(
|
|||
fun create(
|
||||
virtualTimelineItem: MatrixTimelineItem.Virtual,
|
||||
): TimelineItem.Virtual {
|
||||
val id = if (virtualTimelineItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner) {
|
||||
"encrypted_history_banner"
|
||||
} else {
|
||||
virtualTimelineItem.uniqueId.toString()
|
||||
}
|
||||
return TimelineItem.Virtual(
|
||||
id = virtualTimelineItem.uniqueId.toString(),
|
||||
id = id,
|
||||
model = virtualTimelineItem.computeModel()
|
||||
)
|
||||
}
|
||||
|
|
@ -40,6 +46,7 @@ class TimelineItemVirtualFactory @Inject constructor(
|
|||
return when (val inner = virtual) {
|
||||
is VirtualTimelineItem.DayDivider -> daySeparatorFactory.create(inner)
|
||||
is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel
|
||||
is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||
|
|
@ -54,7 +55,7 @@ sealed interface TimelineItem {
|
|||
data class Event(
|
||||
val id: String,
|
||||
val eventId: EventId? = null,
|
||||
val transactionId: String? = null,
|
||||
val transactionId: TransactionId? = null,
|
||||
val senderId: UserId,
|
||||
val senderDisplayName: String?,
|
||||
val senderAvatar: AvatarData,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.model.virtual
|
||||
|
||||
object TimelineItemEncryptedHistoryBannerVirtualModel : TimelineItemVirtualModel {
|
||||
override val type: String = "TimelineItemEncryptedHistoryBannerVirtualModel"
|
||||
}
|
||||
|
|
@ -4,10 +4,27 @@
|
|||
<item quantity="one">"%1$d changement dans la conversation"</item>
|
||||
<item quantity="other">"%1$d changements dans la conversation"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_more_reactions">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="other">"%1$d de plus"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_attachment_source_camera">"Appareil photo"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Prendre une photo"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"Enregistrer une vidéo"</string>
|
||||
<string name="screen_room_attachment_source_files">"Pièce-jointe"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Gallerie photo et vidéo"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"L’historique des messages n’est pas disponible actuellement dans ce salon"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Impossible de récupérer les détails de l’utilisateur"</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Souhaitez-vous les inviter à revenir ?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"Vous êtes seul dans ce chat"</string>
|
||||
<string name="screen_room_message_copied">"Message copié"</string>
|
||||
<string name="screen_room_no_permission_to_post">"Vous n‘avez pas le droit de poster dans ce salon"</string>
|
||||
<string name="screen_room_reactions_show_less">"Afficher moins"</string>
|
||||
<string name="screen_room_reactions_show_more">"Afficher plus"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Renvoyer"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Votre message n\'a pas pu être envoyé"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Ajouter un emoji"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Montrer moins"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Échec du traitement du média avant son envoi, veuillez réessayer."</string>
|
||||
<string name="screen_room_retry_send_menu_remove_action">"Supprimer"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
<item quantity="few">"%1$d zmeny miestnosti"</item>
|
||||
<item quantity="other">"%1$d zmien miestnosti"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_more_reactions">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="few">"%1$d ďalšie"</item>
|
||||
<item quantity="other">"%1$d ďalších"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_attachment_source_camera">"Kamera"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Odfotiť"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"Nahrať video"</string>
|
||||
|
|
@ -21,6 +26,8 @@
|
|||
<string name="screen_room_reactions_show_more">"Zobraziť viac"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Odoslať znova"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Vašu správu sa nepodarilo odoslať"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Pridať emoji"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Zobraziť menej"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova."</string>
|
||||
<string name="screen_room_retry_send_menu_remove_action">"Odstrániť"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,17 @@
|
|||
<string name="screen_room_invite_again_alert_title">"You are alone in this chat"</string>
|
||||
<string name="screen_room_message_copied">"Message copied"</string>
|
||||
<string name="screen_room_no_permission_to_post">"You do not have permission to post to this room"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Allow custom setting"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Turning this on will override your default setting"</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Notify me in this chat for"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote">"You can change it in your %1$s."</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"global settings"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"Default setting"</string>
|
||||
<string name="screen_room_notification_settings_error_loading_settings">"An error occurred while loading notification settings."</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"Failed restoring the default mode, please try again."</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"Failed setting the mode, please try again."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"All messages"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
|
||||
<string name="screen_room_reactions_show_less">"Show less"</string>
|
||||
<string name="screen_room_reactions_show_more">"Show more"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Send again"</string>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.MessagesPresenter
|
|||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||
|
|
@ -568,6 +569,7 @@ class MessagesPresenterTest {
|
|||
mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom),
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
messageComposerContext = MessageComposerContextImpl(),
|
||||
)
|
||||
val timelinePresenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
|
|
|
|||
|
|
@ -26,17 +26,18 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
|
|
@ -82,7 +83,7 @@ class MessageComposerPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isFullScreen).isFalse()
|
||||
assertThat(initialState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(initialState.text).isEqualTo("")
|
||||
assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal(""))
|
||||
assertThat(initialState.showAttachmentSourcePicker).isFalse()
|
||||
assertThat(initialState.attachmentsState).isEqualTo(AttachmentsState.None)
|
||||
|
|
@ -115,11 +116,11 @@ class MessageComposerPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_MESSAGE))
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE))
|
||||
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(""))
|
||||
val withEmptyMessageState = awaitItem()
|
||||
assertThat(withEmptyMessageState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(withEmptyMessageState.text).isEqualTo("")
|
||||
assertThat(withEmptyMessageState.isSendButtonVisible).isFalse()
|
||||
}
|
||||
}
|
||||
|
|
@ -136,7 +137,7 @@ class MessageComposerPresenterTest {
|
|||
state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
state = awaitItem()
|
||||
assertThat(state.text).isEqualTo(StableCharSequence(A_MESSAGE))
|
||||
assertThat(state.text).isEqualTo(A_MESSAGE)
|
||||
assertThat(state.isSendButtonVisible).isTrue()
|
||||
backToNormalMode(state, skipCount = 1)
|
||||
}
|
||||
|
|
@ -153,7 +154,7 @@ class MessageComposerPresenterTest {
|
|||
state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
assertThat(state.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(state.text).isEqualTo("")
|
||||
assertThat(state.isSendButtonVisible).isFalse()
|
||||
backToNormalMode(state)
|
||||
}
|
||||
|
|
@ -170,7 +171,7 @@ class MessageComposerPresenterTest {
|
|||
state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
assertThat(state.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(state.text).isEqualTo("")
|
||||
assertThat(state.isSendButtonVisible).isFalse()
|
||||
backToNormalMode(state)
|
||||
}
|
||||
|
|
@ -185,11 +186,11 @@ class MessageComposerPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_MESSAGE))
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE))
|
||||
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_MESSAGE))
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(messageSentState.text).isEqualTo("")
|
||||
assertThat(messageSentState.isSendButtonVisible).isFalse()
|
||||
}
|
||||
}
|
||||
|
|
@ -205,21 +206,21 @@ class MessageComposerPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(initialState.text).isEqualTo("")
|
||||
val mode = anEditMode()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
skipItems(1)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.mode).isEqualTo(mode)
|
||||
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE))
|
||||
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE))
|
||||
val withEditedMessageState = awaitItem()
|
||||
assertThat(withEditedMessageState.text).isEqualTo(StableCharSequence(ANOTHER_MESSAGE))
|
||||
assertThat(withEditedMessageState.text).isEqualTo(ANOTHER_MESSAGE)
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE))
|
||||
skipItems(1)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(messageSentState.text).isEqualTo("")
|
||||
assertThat(messageSentState.isSendButtonVisible).isFalse()
|
||||
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE)
|
||||
}
|
||||
|
|
@ -236,21 +237,21 @@ class MessageComposerPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(initialState.text).isEqualTo("")
|
||||
val mode = anEditMode(eventId = null, transactionId = A_TRANSACTION_ID)
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
skipItems(1)
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.mode).isEqualTo(mode)
|
||||
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_MESSAGE))
|
||||
assertThat(withMessageState.text).isEqualTo(A_MESSAGE)
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.UpdateText(ANOTHER_MESSAGE))
|
||||
val withEditedMessageState = awaitItem()
|
||||
assertThat(withEditedMessageState.text).isEqualTo(StableCharSequence(ANOTHER_MESSAGE))
|
||||
assertThat(withEditedMessageState.text).isEqualTo(ANOTHER_MESSAGE)
|
||||
withEditedMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(ANOTHER_MESSAGE))
|
||||
skipItems(1)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(messageSentState.text).isEqualTo("")
|
||||
assertThat(messageSentState.isSendButtonVisible).isFalse()
|
||||
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE)
|
||||
}
|
||||
|
|
@ -267,21 +268,21 @@ class MessageComposerPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(initialState.text).isEqualTo("")
|
||||
val mode = aReplyMode()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.SetMode(mode))
|
||||
val state = awaitItem()
|
||||
assertThat(state.mode).isEqualTo(mode)
|
||||
assertThat(state.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(state.text).isEqualTo("")
|
||||
assertThat(state.isSendButtonVisible).isFalse()
|
||||
initialState.eventSink.invoke(MessageComposerEvents.UpdateText(A_REPLY))
|
||||
val withMessageState = awaitItem()
|
||||
assertThat(withMessageState.text).isEqualTo(StableCharSequence(A_REPLY))
|
||||
assertThat(withMessageState.text).isEqualTo(A_REPLY)
|
||||
assertThat(withMessageState.isSendButtonVisible).isTrue()
|
||||
withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY))
|
||||
skipItems(1)
|
||||
val messageSentState = awaitItem()
|
||||
assertThat(messageSentState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(messageSentState.text).isEqualTo("")
|
||||
assertThat(messageSentState.isSendButtonVisible).isFalse()
|
||||
assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY)
|
||||
}
|
||||
|
|
@ -484,7 +485,7 @@ class MessageComposerPresenterTest {
|
|||
skipItems(skipCount)
|
||||
val normalState = awaitItem()
|
||||
assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal(""))
|
||||
assertThat(normalState.text).isEqualTo(StableCharSequence(""))
|
||||
assertThat(normalState.text).isEqualTo("")
|
||||
assertThat(normalState.isSendButtonVisible).isFalse()
|
||||
}
|
||||
|
||||
|
|
@ -503,14 +504,15 @@ class MessageComposerPresenterTest {
|
|||
localMediaFactory,
|
||||
MediaSender(mediaPreProcessor, room),
|
||||
snackbarDispatcher,
|
||||
FakeAnalyticsService()
|
||||
FakeAnalyticsService(),
|
||||
MessageComposerContextImpl(),
|
||||
)
|
||||
}
|
||||
|
||||
fun anEditMode(
|
||||
eventId: EventId? = AN_EVENT_ID,
|
||||
message: String = A_MESSAGE,
|
||||
transactionId: String? = null,
|
||||
transactionId: TransactionId? = null,
|
||||
) = MessageComposerMode.Edit(eventId, message, transactionId)
|
||||
fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, AN_EVENT_ID, A_MESSAGE)
|
||||
fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue