Merge pull request #5160 from element-hq/feature/bma/cleanupFeatureFlags

Remove old feature flags
This commit is contained in:
Benoit Marty 2025-08-12 18:21:44 +02:00 committed by GitHub
commit 58ce545d98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 310 additions and 1243 deletions

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.featureflag.api
import io.element.android.appconfig.OnBoardingConfig
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
@ -21,46 +20,6 @@ enum class FeatureFlags(
override val defaultValue: (BuildMeta) -> Boolean,
override val isFinished: Boolean,
) : Feature {
LocationSharing(
key = "feature.locationsharing",
title = "Allow user to share location",
defaultValue = { true },
isFinished = true,
),
Polls(
key = "feature.polls",
title = "Polls",
description = "Create poll and render poll events in the timeline",
defaultValue = { true },
isFinished = true,
),
NotificationSettings(
key = "feature.notificationsettings",
title = "Show notification settings",
defaultValue = { true },
isFinished = true,
),
VoiceMessages(
key = "feature.voicemessages",
title = "Voice messages",
description = "Send and receive voice messages",
defaultValue = { true },
isFinished = true,
),
PinUnlock(
key = "feature.pinunlock",
title = "Pin unlock",
description = "Allow user to lock/unlock the app with a pin code or biometrics",
defaultValue = { true },
isFinished = true,
),
MarkAsUnread(
key = "feature.markAsUnread",
title = "Mark as unread",
description = "Allow user to mark a room as unread",
defaultValue = { true },
isFinished = false,
),
RoomDirectorySearch(
key = "feature.roomdirectorysearch",
title = "Room directory search",
@ -75,27 +34,6 @@ enum class FeatureFlags(
defaultValue = { false },
isFinished = false,
),
QrCodeLogin(
key = "feature.qrCodeLogin",
title = "Enable login using QR code",
description = "Allow the user to login using the QR code flow",
defaultValue = { OnBoardingConfig.CAN_LOGIN_WITH_QR_CODE },
isFinished = false,
),
IncomingShare(
key = "feature.incomingShare",
title = "Incoming Share support",
description = "Allow the application to receive data from other applications",
defaultValue = { true },
isFinished = false,
),
PinnedEvents(
key = "feature.pinnedEvents",
title = "Pinned Events",
description = "Allow user to pin events in a room",
defaultValue = { true },
isFinished = false,
),
SyncOnPush(
key = "feature.syncOnPush",
title = "Sync on push",
@ -137,34 +75,6 @@ enum class FeatureFlags(
defaultValue = { false },
isFinished = false,
),
MediaUploadOnSendQueue(
key = "feature.media_upload_through_send_queue",
title = "Media upload through send queue",
description = "Support for treating media uploads as regular events, with an improved retry and cancellation implementation.",
defaultValue = { true },
isFinished = true,
),
MediaCaptionCreation(
key = "feature.media_caption_creation",
title = "Allow creation of media captions",
description = null,
defaultValue = { true },
isFinished = false,
),
MediaCaptionWarning(
key = "feature.media_caption_creation_warning",
title = "Show a compatibility warning on media captions creation",
description = null,
defaultValue = { true },
isFinished = false,
),
MediaGallery(
key = "feature.media_gallery",
title = "Allow user to open the media gallery",
description = null,
defaultValue = { true },
isFinished = false,
),
PrintLogsToLogcat(
key = "feature.print_logs_to_logcat",
title = "Print logs to logcat",
@ -175,15 +85,6 @@ enum class FeatureFlags(
// False so it's displayed in the developer options screen
isFinished = false,
),
SharePos(
key = "feature.share_pos_v2",
title = "Share pos in sliding sync",
description = "Keep the sliding sync pos to make initial syncs faster. Requires an app restart to take effect." +
"\n\nWARNING: this may cause issues with syncs.",
defaultValue = { true },
// False so it's displayed in the developer options screen
isFinished = false,
),
SelectableMediaQuality(
key = "feature.selectable_media_quality",
title = "Select media quality per upload",

View file

@ -19,8 +19,8 @@ class DefaultFeatureFlagServiceTest {
fun `given service without provider when feature is checked then it returns the default value`() = runTest {
val buildMeta = aBuildMeta()
val featureFlagService = DefaultFeatureFlagService(emptySet(), buildMeta)
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test {
assertThat(awaitItem()).isEqualTo(FeatureFlags.LocationSharing.defaultValue(buildMeta))
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space).test {
assertThat(awaitItem()).isEqualTo(FeatureFlags.Space.defaultValue(buildMeta))
cancelAndIgnoreRemainingEvents()
}
}
@ -28,7 +28,7 @@ class DefaultFeatureFlagServiceTest {
@Test
fun `given service without provider when set enabled feature is called then it returns false`() = runTest {
val featureFlagService = DefaultFeatureFlagService(emptySet(), aBuildMeta())
val result = featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
val result = featureFlagService.setFeatureEnabled(FeatureFlags.Space, true)
assertThat(result).isFalse()
}
@ -37,7 +37,7 @@ class DefaultFeatureFlagServiceTest {
val buildMeta = aBuildMeta()
val featureFlagProvider = FakeMutableFeatureFlagProvider(0, buildMeta)
val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider), buildMeta)
val result = featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
val result = featureFlagService.setFeatureEnabled(FeatureFlags.Space, true)
assertThat(result).isTrue()
}
@ -46,10 +46,10 @@ class DefaultFeatureFlagServiceTest {
val buildMeta = aBuildMeta()
val featureFlagProvider = FakeMutableFeatureFlagProvider(0, buildMeta)
val featureFlagService = DefaultFeatureFlagService(setOf(featureFlagProvider), buildMeta)
featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, true)
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test {
featureFlagService.setFeatureEnabled(FeatureFlags.Space, true)
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space).test {
assertThat(awaitItem()).isTrue()
featureFlagService.setFeatureEnabled(FeatureFlags.LocationSharing, false)
featureFlagService.setFeatureEnabled(FeatureFlags.Space, false)
assertThat(awaitItem()).isFalse()
}
}
@ -60,9 +60,9 @@ class DefaultFeatureFlagServiceTest {
val lowPriorityFeatureFlagProvider = FakeMutableFeatureFlagProvider(LOW_PRIORITY, buildMeta)
val highPriorityFeatureFlagProvider = FakeMutableFeatureFlagProvider(HIGH_PRIORITY, buildMeta)
val featureFlagService = DefaultFeatureFlagService(setOf(lowPriorityFeatureFlagProvider, highPriorityFeatureFlagProvider), buildMeta)
lowPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, false)
highPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.LocationSharing, true)
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LocationSharing).test {
lowPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.Space, false)
highPriorityFeatureFlagProvider.setFeatureEnabled(FeatureFlags.Space, true)
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space).test {
assertThat(awaitItem()).isTrue()
}
}

View file

@ -14,7 +14,6 @@ import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.ProgressCallback
@ -136,7 +135,6 @@ class RustMatrixClient(
baseCacheDirectory: File,
clock: SystemClock,
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
featureFlagService: FeatureFlagService,
) : MatrixClient {
override val sessionId: UserId = UserId(innerClient.userId())
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
@ -205,7 +203,6 @@ class RustMatrixClient(
roomContentForwarder = RoomContentForwarder(innerRoomListService),
roomSyncSubscriber = roomSyncSubscriber,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
featureFlagService = featureFlagService,
roomMembershipObserver = roomMembershipObserver,
roomInfoMapper = roomInfoMapper,
)

View file

@ -78,7 +78,7 @@ class RustMatrixClientFactory @Inject constructor(
client.setUtdDelegate(UtdTracker(analyticsService))
val syncService = client.syncService()
.withSharePos(enable = featureFlagService.isFeatureEnabled(FeatureFlags.SharePos))
.withSharePos(true)
.withOfflineMode()
.finish()
@ -93,7 +93,6 @@ class RustMatrixClientFactory @Inject constructor(
baseCacheDirectory = cacheDirectory,
clock = clock,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
featureFlagService = featureFlagService,
).also {
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
}

View file

@ -11,7 +11,6 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
@ -84,7 +83,6 @@ class JoinedRustRoom(
private val coroutineDispatchers: CoroutineDispatchers,
private val systemClock: SystemClock,
private val roomContentForwarder: RoomContentForwarder,
private val featureFlagService: FeatureFlagService,
) : JoinedRoom, BaseRoom by baseRoom {
// Create a dispatcher for all room methods...
private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32)
@ -478,7 +476,6 @@ class JoinedRustRoom(
dispatcher = roomDispatcher,
roomContentForwarder = roomContentForwarder,
onNewSyncedEvent = onNewSyncedEvent,
featureFlagsService = featureFlagService,
)
}
}

View file

@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl.room
import io.element.android.appconfig.TimelineConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
@ -49,7 +48,6 @@ class RustRoomFactory(
private val innerRoomListService: InnerRoomListService,
private val roomSyncSubscriber: RoomSyncSubscriber,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
private val featureFlagService: FeatureFlagService,
private val roomMembershipObserver: RoomMembershipObserver,
private val roomInfoMapper: RoomInfoMapper,
) {
@ -127,7 +125,6 @@ class RustRoomFactory(
liveInnerTimeline = timeline,
coroutineDispatchers = dispatchers,
systemClock = systemClock,
featureFlagService = featureFlagService,
)
)
} else {

View file

@ -8,8 +8,6 @@
package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
@ -89,7 +87,6 @@ class RustTimeline(
private val coroutineScope: CoroutineScope,
private val dispatcher: CoroutineDispatcher,
private val roomContentForwarder: RoomContentForwarder,
private val featureFlagsService: FeatureFlagService,
onNewSyncedEvent: () -> Unit,
) : Timeline {
private val _timelineItems: MutableSharedFlow<List<MatrixTimelineItem>> =
@ -342,7 +339,6 @@ class RustTimeline(
progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result<MediaUploadHandler> {
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
inner.sendImage(
params = UploadParameters(
@ -351,7 +347,7 @@ class RustTimeline(
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
useSendQueue = true,
mentions = null,
inReplyTo = inReplyToEventId?.value,
),
@ -371,7 +367,6 @@ class RustTimeline(
progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result<MediaUploadHandler> {
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
return sendAttachment(listOfNotNull(file, thumbnailFile)) {
inner.sendVideo(
params = UploadParameters(
@ -380,7 +375,7 @@ class RustTimeline(
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
useSendQueue = true,
mentions = null,
inReplyTo = inReplyToEventId?.value,
),
@ -399,7 +394,6 @@ class RustTimeline(
progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result<MediaUploadHandler> {
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
return sendAttachment(listOf(file)) {
inner.sendAudio(
params = UploadParameters(
@ -408,7 +402,7 @@ class RustTimeline(
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
useSendQueue = true,
mentions = null,
inReplyTo = inReplyToEventId?.value,
),
@ -426,7 +420,6 @@ class RustTimeline(
progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result<MediaUploadHandler> {
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
return sendAttachment(listOf(file)) {
inner.sendFile(
params = UploadParameters(
@ -435,7 +428,7 @@ class RustTimeline(
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
useSendQueue = true,
mentions = null,
inReplyTo = inReplyToEventId?.value,
),
@ -489,7 +482,6 @@ class RustTimeline(
progressCallback: ProgressCallback?,
inReplyToEventId: EventId?,
): Result<MediaUploadHandler> {
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
return sendAttachment(listOf(file)) {
inner.sendVoiceMessage(
params = UploadParameters(
@ -497,7 +489,7 @@ class RustTimeline(
// Maybe allow a caption in the future?
caption = null,
formattedCaption = null,
useSendQueue = useSendQueue,
useSendQueue = true,
mentions = null,
inReplyTo = inReplyToEventId?.value,
),

View file

@ -8,7 +8,6 @@
package io.element.android.libraries.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory
@ -67,6 +66,5 @@ class RustMatrixClientTest {
baseCacheDirectory = File(""),
clock = FakeSystemClock(),
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
featureFlagService = FakeFeatureFlagService(),
)
}

View file

@ -10,8 +10,6 @@ package io.element.android.libraries.matrix.impl.timeline
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
@ -98,7 +96,6 @@ private fun TestScope.createRustTimeline(
coroutineScope: CoroutineScope = backgroundScope,
dispatcher: CoroutineDispatcher = testCoroutineDispatchers().io,
roomContentForwarder: RoomContentForwarder = RoomContentForwarder(FakeFfiRoomListService()),
featureFlagsService: FeatureFlagService = FakeFeatureFlagService(),
onNewSyncedEvent: () -> Unit = {},
): RustTimeline {
return RustTimeline(
@ -109,7 +106,6 @@ private fun TestScope.createRustTimeline(
coroutineScope = coroutineScope,
dispatcher = dispatcher,
roomContentForwarder = roomContentForwarder,
featureFlagsService = featureFlagsService,
onNewSyncedEvent = onNewSyncedEvent,
)
}

View file

@ -91,7 +91,6 @@ import io.element.android.wysiwyg.compose.RichTextEditor
import io.element.android.wysiwyg.display.TextDisplay
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch
import uniffi.wysiwyg_composer.MenuAction
import kotlin.time.Duration.Companion.seconds
@ -101,7 +100,6 @@ fun TextComposer(
state: TextEditorState,
voiceMessageState: VoiceMessageState,
composerMode: MessageComposerMode,
enableVoiceMessages: Boolean,
onRequestFocus: () -> Unit,
onSendMessage: () -> Unit,
onResetComposerMode: () -> Unit,
@ -141,8 +139,8 @@ fun TextComposer(
}
val layoutModifier = modifier
.fillMaxSize()
.height(IntrinsicSize.Min)
.fillMaxSize()
.height(IntrinsicSize.Min)
val composerOptionsButton: @Composable () -> Unit = remember(composerMode) {
@Composable {
@ -171,22 +169,17 @@ fun TextComposer(
} else {
stringResource(id = R.string.rich_text_editor_composer_placeholder)
}
val textInput: @Composable () -> Unit = if ((composerMode as? MessageComposerMode.Attachment)?.allowCaption == false) {
{
// No text input when in attachment mode and caption not allowed.
}
} else {
when (state) {
is TextEditorState.Rich -> {
val coroutineScope = rememberCoroutineScope()
val view = LocalView.current
remember(state.richTextEditorState, composerMode, onResetComposerMode, onError) {
@Composable {
TextInputBox(
modifier = Modifier
val textInput: @Composable () -> Unit = when (state) {
is TextEditorState.Rich -> {
val coroutineScope = rememberCoroutineScope()
val view = LocalView.current
remember(state.richTextEditorState, composerMode, onResetComposerMode, onError) {
@Composable {
TextInputBox(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
coroutineScope.launch {
state.requestFocus()
@ -196,46 +189,45 @@ fun TextComposer(
.semantics {
hideFromAccessibility()
},
composerMode = composerMode,
onResetComposerMode = onResetComposerMode,
isTextEmpty = state.richTextEditorState.messageHtml.isEmpty(),
) {
RichTextEditor(
state = state.richTextEditorState,
placeholder = placeholder,
registerStateUpdates = true,
modifier = Modifier
composerMode = composerMode,
onResetComposerMode = onResetComposerMode,
isTextEmpty = state.richTextEditorState.messageHtml.isEmpty(),
) {
RichTextEditor(
state = state.richTextEditorState,
placeholder = placeholder,
registerStateUpdates = true,
modifier = Modifier
.padding(top = 6.dp, bottom = 6.dp)
.fillMaxWidth(),
style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.richTextEditorState.hasFocus),
resolveMentionDisplay = resolveMentionDisplay,
resolveRoomMentionDisplay = resolveAtRoomMentionDisplay,
onError = onError,
onRichContentSelected = onSelectRichContent,
onTyping = onTyping,
)
}
style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.richTextEditorState.hasFocus),
resolveMentionDisplay = resolveMentionDisplay,
resolveRoomMentionDisplay = resolveAtRoomMentionDisplay,
onError = onError,
onRichContentSelected = onSelectRichContent,
onTyping = onTyping,
)
}
}
}
is TextEditorState.Markdown -> {
@Composable {
val style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus())
TextInputBox(
composerMode = composerMode,
onResetComposerMode = onResetComposerMode,
isTextEmpty = state.state.text.value().isEmpty(),
) {
MarkdownTextInput(
state = state.state,
placeholder = placeholder,
placeholderColor = ElementTheme.colors.textSecondary,
onTyping = onTyping,
onReceiveSuggestion = onReceiveSuggestion,
richTextEditorStyle = style,
onSelectRichContent = onSelectRichContent,
)
}
}
is TextEditorState.Markdown -> {
@Composable {
val style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus())
TextInputBox(
composerMode = composerMode,
onResetComposerMode = onResetComposerMode,
isTextEmpty = state.state.text.value().isEmpty(),
) {
MarkdownTextInput(
state = state.state,
placeholder = placeholder,
placeholderColor = ElementTheme.colors.textSecondary,
onTyping = onTyping,
onReceiveSuggestion = onReceiveSuggestion,
richTextEditorStyle = style,
onSelectRichContent = onSelectRichContent,
)
}
}
}
@ -273,7 +265,7 @@ fun TextComposer(
}
val sendOrRecordButton = when {
enableVoiceMessages && !canSendMessage ->
!canSendMessage ->
when (voiceMessageState) {
VoiceMessageState.Idle,
is VoiceMessageState.Recording -> recordVoiceButton
@ -288,7 +280,6 @@ fun TextComposer(
val endButtonA11y = endButtonA11y(
composerMode = composerMode,
voiceMessageState = voiceMessageState,
enableVoiceMessages = enableVoiceMessages,
canSendMessage = canSendMessage,
)
@ -341,7 +332,6 @@ fun TextComposer(
} else {
StandardLayout(
voiceMessageState = voiceMessageState,
enableVoiceMessages = enableVoiceMessages,
isRoomEncrypted = state.isRoomEncrypted,
modifier = layoutModifier,
composerOptionsButton = composerOptionsButton,
@ -378,12 +368,11 @@ fun TextComposer(
private fun endButtonA11y(
composerMode: MessageComposerMode,
voiceMessageState: VoiceMessageState,
enableVoiceMessages: Boolean,
canSendMessage: Boolean,
): (SemanticsPropertyReceiver) -> Unit {
val a11ySendButtonDescription = stringResource(
id = when {
enableVoiceMessages && !canSendMessage ->
!canSendMessage ->
when (voiceMessageState) {
VoiceMessageState.Idle,
is VoiceMessageState.Recording -> if (voiceMessageState is VoiceMessageState.Recording) {
@ -410,7 +399,6 @@ private fun endButtonA11y(
@Composable
private fun StandardLayout(
voiceMessageState: VoiceMessageState,
enableVoiceMessages: Boolean,
isRoomEncrypted: Boolean?,
textInput: @Composable () -> Unit,
composerOptionsButton: @Composable () -> Unit,
@ -427,12 +415,12 @@ private fun StandardLayout(
Spacer(Modifier.height(4.dp))
}
Row(verticalAlignment = Alignment.Bottom) {
if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) {
if (voiceMessageState !is VoiceMessageState.Idle) {
if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) {
Box(
modifier = Modifier
.padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp)
.size(48.dp),
.padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp)
.size(48.dp),
contentAlignment = Alignment.Center,
) {
voiceDeleteButton()
@ -442,8 +430,8 @@ private fun StandardLayout(
}
Box(
modifier = Modifier
.padding(bottom = 8.dp, top = 8.dp)
.weight(1f)
.padding(bottom = 8.dp, top = 8.dp)
.weight(1f)
) {
voiceRecording()
}
@ -456,17 +444,17 @@ private fun StandardLayout(
}
Box(
modifier = Modifier
.padding(bottom = 8.dp, top = 8.dp)
.weight(1f)
.padding(bottom = 8.dp, top = 8.dp)
.weight(1f)
) {
textInput()
}
}
Box(
Modifier
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
.size(48.dp)
.clearAndSetSemantics(endButtonA11y),
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
.size(48.dp)
.clearAndSetSemantics(endButtonA11y),
contentAlignment = Alignment.Center,
) {
endButton()
@ -517,8 +505,8 @@ private fun TextFormattingLayout(
}
Box(
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp)
.weight(1f)
.padding(horizontal = 12.dp)
) {
textInput()
}
@ -537,11 +525,11 @@ private fun TextFormattingLayout(
}
Box(
modifier = Modifier
.padding(
start = 14.dp,
end = 6.dp,
)
.clearAndSetSemantics(endButtonA11y)
.padding(
start = 14.dp,
end = 6.dp,
)
.clearAndSetSemantics(endButtonA11y)
) {
sendButton()
}
@ -563,12 +551,12 @@ private fun TextInputBox(
Column(
modifier = Modifier
.clip(roundedCorners)
.border(0.5.dp, borderColor, roundedCorners)
.background(color = bgColor)
.requiredHeightIn(min = 42.dp)
.fillMaxSize()
.then(modifier),
.clip(roundedCorners)
.border(0.5.dp, borderColor, roundedCorners)
.background(color = bgColor)
.requiredHeightIn(min = 42.dp)
.fillMaxSize()
.then(modifier),
) {
if (composerMode is MessageComposerMode.Special) {
ComposerModeView(
@ -578,8 +566,8 @@ private fun TextInputBox(
}
Box(
modifier = Modifier
.padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp)
.then(Modifier.testTag(TestTags.textEditor)),
.padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp)
.then(Modifier.testTag(TestTags.textEditor)),
contentAlignment = Alignment.CenterStart,
) {
textInput()
@ -587,9 +575,9 @@ private fun TextInputBox(
var showBottomSheet by remember { mutableStateOf(false) }
Icon(
modifier = Modifier
.clickable { showBottomSheet = true }
.padding(horizontal = 8.dp, vertical = 4.dp)
.align(Alignment.CenterEnd),
.clickable { showBottomSheet = true }
.padding(horizontal = 8.dp, vertical = 4.dp)
.align(Alignment.CenterEnd),
imageVector = CompoundIcons.InfoSolid(),
tint = ElementTheme.colors.iconCriticalPrimary,
contentDescription = null,
@ -631,12 +619,11 @@ private fun aTextEditorStateRichList(isRoomEncrypted: Boolean? = null) = persist
internal fun TextComposerSimplePreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateMarkdownList()
) { _, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
)
}
}
@ -646,12 +633,11 @@ internal fun TextComposerSimplePreview() = ElementPreview {
internal fun TextComposerSimpleNotEncryptedPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateMarkdownList(isRoomEncrypted = false),
) { _, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
)
}
}
@ -661,13 +647,12 @@ internal fun TextComposerSimpleNotEncryptedPreview() = ElementPreview {
internal fun TextComposerFormattingPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList()
) { _, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
showTextFormatting = true,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
)
}
}
@ -677,13 +662,12 @@ internal fun TextComposerFormattingPreview() = ElementPreview {
internal fun TextComposerFormattingNotEncryptedPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList(isRoomEncrypted = false)
) { _, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
showTextFormatting = true,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
)
}
}
@ -693,12 +677,11 @@ internal fun TextComposerFormattingNotEncryptedPreview() = ElementPreview {
internal fun TextComposerEditPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList()
) { _, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = aMessageComposerModeEdit(),
enableVoiceMessages = true,
)
}
}
@ -708,12 +691,11 @@ internal fun TextComposerEditPreview() = ElementPreview {
internal fun TextComposerEditNotEncryptedPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList(isRoomEncrypted = false)
) { _, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = aMessageComposerModeEdit(),
enableVoiceMessages = true,
)
}
}
@ -723,7 +705,7 @@ internal fun TextComposerEditNotEncryptedPreview() = ElementPreview {
internal fun TextComposerEditCaptionPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList()
) { _, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
@ -731,7 +713,6 @@ internal fun TextComposerEditCaptionPreview() = ElementPreview {
// Set an existing caption so that the UI will be in edit caption mode
content = "An existing caption",
),
enableVoiceMessages = false,
)
}
}
@ -741,16 +722,14 @@ internal fun TextComposerEditCaptionPreview() = ElementPreview {
internal fun TextComposerAddCaptionPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList()
) { index, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = aMessageComposerModeEditCaption(
// No caption so that the UI will be in add caption mode
content = "",
showCompatibilityWarning = index == 0,
),
enableVoiceMessages = false,
)
}
}
@ -760,12 +739,11 @@ internal fun TextComposerAddCaptionPreview() = ElementPreview {
internal fun MarkdownTextComposerEditPreview() = ElementPreview {
PreviewColumn(
items = aTextEditorStateMarkdownList()
) { _, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = aMessageComposerModeEdit(),
enableVoiceMessages = true,
)
}
}
@ -775,14 +753,13 @@ internal fun MarkdownTextComposerEditPreview() = ElementPreview {
internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails) = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList()
) { _, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = aMessageComposerModeReply(
replyToDetails = inReplyToDetails,
),
enableVoiceMessages = true,
)
}
}
@ -800,14 +777,13 @@ internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider
internal fun TextComposerReplyNotEncryptedPreview(@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails) = ElementPreview {
PreviewColumn(
items = aTextEditorStateRichList(isRoomEncrypted = false)
) { _, textEditorState ->
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = aMessageComposerModeReply(
replyToDetails = inReplyToDetails,
),
enableVoiceMessages = true,
)
}
}
@ -817,16 +793,12 @@ internal fun TextComposerReplyNotEncryptedPreview(@PreviewParameter(InReplyToDet
internal fun TextComposerCaptionPreview() = ElementPreview {
val list = aTextEditorStateMarkdownList()
PreviewColumn(
items = (list + aTextEditorStateMarkdown(initialText = "NO_CAPTION", initialFocus = true)).toPersistentList()
) { index, textEditorState ->
items = list,
) { textEditorState ->
ATextComposer(
state = textEditorState,
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Attachment(
allowCaption = index < list.size,
showCaptionCompatibilityWarning = index == 0,
),
enableVoiceMessages = false,
composerMode = MessageComposerMode.Attachment,
)
}
}
@ -862,12 +834,11 @@ internal fun TextComposerVoicePreview() = ElementPreview {
playbackProgress = 0.0f
),
)
) { _, voiceMessageState ->
) { voiceMessageState ->
ATextComposer(
state = aTextEditorStateRich(initialFocus = true),
voiceMessageState = voiceMessageState,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
)
}
}
@ -903,12 +874,11 @@ internal fun TextComposerVoiceNotEncryptedPreview() = ElementPreview {
playbackProgress = 0.0f
),
)
) { _, voiceMessageState ->
) { voiceMessageState ->
ATextComposer(
state = aTextEditorStateRich(initialFocus = true, isRoomEncrypted = false),
voiceMessageState = voiceMessageState,
composerMode = MessageComposerMode.Normal,
enableVoiceMessages = true,
)
}
}
@ -916,15 +886,15 @@ internal fun TextComposerVoiceNotEncryptedPreview() = ElementPreview {
@Composable
private fun <T> PreviewColumn(
items: ImmutableList<T>,
view: @Composable (Int, T) -> Unit,
view: @Composable (T) -> Unit,
) {
Column {
items.forEachIndexed { index, item ->
items.forEach { item ->
HorizontalDivider()
Box(
modifier = Modifier.height(IntrinsicSize.Min)
) {
view(index, item)
view(item)
}
}
}
@ -935,7 +905,6 @@ private fun ATextComposer(
state: TextEditorState,
voiceMessageState: VoiceMessageState,
composerMode: MessageComposerMode,
enableVoiceMessages: Boolean,
showTextFormatting: Boolean = false,
) {
TextComposer(
@ -943,7 +912,6 @@ private fun ATextComposer(
showTextFormatting = showTextFormatting,
voiceMessageState = voiceMessageState,
composerMode = composerMode,
enableVoiceMessages = enableVoiceMessages,
onRequestFocus = {},
onSendMessage = {},
onResetComposerMode = {},
@ -973,11 +941,9 @@ fun aMessageComposerModeEdit(
fun aMessageComposerModeEditCaption(
eventOrTransactionId: EventOrTransactionId = EventId("$1234").toEventOrTransactionId(),
content: String,
showCompatibilityWarning: Boolean = false,
) = MessageComposerMode.EditCaption(
eventOrTransactionId = eventOrTransactionId,
content = content,
showCaptionCompatibilityWarning = showCompatibilityWarning,
)
fun aMessageComposerModeReply(

View file

@ -18,10 +18,7 @@ import io.element.android.libraries.matrix.ui.messages.reply.eventId
sealed interface MessageComposerMode {
data object Normal : MessageComposerMode
data class Attachment(
val allowCaption: Boolean,
val showCaptionCompatibilityWarning: Boolean,
) : MessageComposerMode
data object Attachment : MessageComposerMode
sealed interface Special : MessageComposerMode
@ -33,7 +30,6 @@ sealed interface MessageComposerMode {
data class EditCaption(
val eventOrTransactionId: EventOrTransactionId,
val content: String,
val showCaptionCompatibilityWarning: Boolean,
) : Special
data class Reply(
@ -58,8 +54,8 @@ sealed interface MessageComposerMode {
fun MessageComposerMode.showCaptionCompatibilityWarning(): Boolean {
return when (this) {
is MessageComposerMode.Attachment -> showCaptionCompatibilityWarning
is MessageComposerMode.EditCaption -> showCaptionCompatibilityWarning && content.isEmpty()
is MessageComposerMode.Attachment -> true
is MessageComposerMode.EditCaption -> content.isEmpty()
else -> false
}
}