Merge branch 'develop' of https://github.com/vector-im/element-x-android into dla/feature/room_list_decoration

This commit is contained in:
David Langley
2023-09-15 10:26:24 +01:00
49 changed files with 717 additions and 88 deletions

View File

@@ -81,6 +81,6 @@ jobs:
# https://github.com/codecov/codecov-action
- name: ☂️ Upload coverage reports to codecov
if: always()
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v3
# with:
# files: build/reports/kover/merged/xml/report.xml

View File

@@ -48,6 +48,7 @@ dependencies {
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.mediaupload.api)
implementation(projects.libraries.preferences.api)
implementation(projects.features.networkmonitor.api)
implementation(projects.services.analytics.api)
implementation(libs.coil.compose)
@@ -76,6 +77,7 @@ dependencies {
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.textcomposer.test)
testImplementation(libs.test.mockk)

View File

@@ -57,6 +57,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
@@ -66,8 +67,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
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.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@@ -97,7 +96,7 @@ class MessagesPresenter @AssistedInject constructor(
private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper,
private val analyticsService: AnalyticsService,
private val featureFlagService: FeatureFlagService,
private val preferencesStore: PreferencesStore,
@Assisted private val navigator: MessagesNavigator,
) : Presenter<MessagesState> {
@@ -146,15 +145,17 @@ class MessagesPresenter @AssistedInject constructor(
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
}
var enableTextFormatting by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
enableTextFormatting = featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)
}
val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true)
fun handleEvents(event: MessagesEvents) {
when (event) {
is MessagesEvents.HandleAction -> {
localCoroutineScope.handleTimelineAction(event.action, event.event, composerState)
localCoroutineScope.handleTimelineAction(
action = event.action,
targetEvent = event.event,
composerState = composerState,
enableTextFormatting = enableTextFormatting,
)
}
is MessagesEvents.ToggleReaction -> {
localCoroutineScope.toggleReaction(event.emoji, event.eventId)
@@ -204,14 +205,15 @@ class MessagesPresenter @AssistedInject constructor(
action: TimelineItemAction,
targetEvent: TimelineItem.Event,
composerState: MessageComposerState,
enableTextFormatting: Boolean,
) = launch {
when (action) {
TimelineItemAction.Copy -> handleCopyContents(targetEvent)
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState)
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
TimelineItemAction.Reply,
TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState)
TimelineItemAction.Developer -> handleShowDebugInfoAction(targetEvent)
TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent)
TimelineItemAction.Forward -> handleForwardAction(targetEvent)
TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent)
@@ -260,11 +262,15 @@ class MessagesPresenter @AssistedInject constructor(
}
}
private suspend fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
private suspend fun handleActionEdit(
targetEvent: TimelineItem.Event,
composerState: MessageComposerState,
enableTextFormatting: Boolean,
) {
val composerMode = MessageComposerMode.Edit(
targetEvent.eventId,
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
if (featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)) {
if (enableTextFormatting) {
it.htmlBody ?: it.body
} else {
it.body

View File

@@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.actionlist
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -30,15 +31,15 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
import io.element.android.features.messages.impl.timeline.model.event.canReact
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class ActionListPresenter @Inject constructor(
private val buildMeta: BuildMeta,
private val preferencesStore: PreferencesStore,
) : Presenter<ActionListState> {
@Composable
@@ -49,6 +50,8 @@ class ActionListPresenter @Inject constructor(
mutableStateOf(ActionListState.Target.None)
}
val isDeveloperModeEnabled by preferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
val displayEmojiReactions by remember {
derivedStateOf {
val event = (target.value as? ActionListState.Target.Success)?.event
@@ -63,6 +66,7 @@ class ActionListPresenter @Inject constructor(
timelineItem = event.event,
userCanRedact = event.canRedact,
userCanSendMessage = event.canSendMessage,
isDeveloperModeEnabled = isDeveloperModeEnabled,
target = target,
)
}
@@ -79,14 +83,15 @@ class ActionListPresenter @Inject constructor(
timelineItem: TimelineItem.Event,
userCanRedact: Boolean,
userCanSendMessage: Boolean,
isDeveloperModeEnabled: Boolean,
target: MutableState<ActionListState.Target>
) = launch {
target.value = ActionListState.Target.Loading(timelineItem)
val actions =
when (timelineItem.content) {
is TimelineItemRedactedContent -> {
if (buildMeta.isDebuggable) {
listOf(TimelineItemAction.Developer)
if (isDeveloperModeEnabled) {
listOf(TimelineItemAction.ViewSource)
} else {
emptyList()
}
@@ -94,8 +99,8 @@ class ActionListPresenter @Inject constructor(
is TimelineItemStateContent -> {
buildList {
add(TimelineItemAction.Copy)
if (buildMeta.isDebuggable) {
add(TimelineItemAction.Developer)
if (isDeveloperModeEnabled) {
add(TimelineItemAction.ViewSource)
}
}
}
@@ -115,8 +120,8 @@ class ActionListPresenter @Inject constructor(
if (timelineItem.content.canBeCopied()) {
add(TimelineItemAction.Copy)
}
if (buildMeta.isDebuggable) {
add(TimelineItemAction.Developer)
if (isDeveloperModeEnabled) {
add(TimelineItemAction.ViewSource)
}
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)
@@ -144,8 +149,8 @@ class ActionListPresenter @Inject constructor(
if (timelineItem.content.canBeCopied()) {
add(TimelineItemAction.Copy)
}
if (buildMeta.isDebuggable) {
add(TimelineItemAction.Developer)
if (isDeveloperModeEnabled) {
add(TimelineItemAction.ViewSource)
}
if (!timelineItem.isMine) {
add(TimelineItemAction.ReportContent)

View File

@@ -111,7 +111,7 @@ fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
TimelineItemAction.Edit,
TimelineItemAction.Redact,
TimelineItemAction.ReportContent,
TimelineItemAction.Developer,
TimelineItemAction.ViewSource,
)
}
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
@@ -119,7 +119,7 @@ fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
TimelineItemAction.EndPoll,
TimelineItemAction.Reply,
TimelineItemAction.Copy,
TimelineItemAction.Developer,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
TimelineItemAction.Redact,
)

View File

@@ -34,7 +34,7 @@ sealed class TimelineItemAction(
data object Reply : TimelineItemAction(CommonStrings.action_reply, VectorIcons.Reply)
data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, VectorIcons.Reply)
data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit)
data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode)
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode)
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true)
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.PollEnd)
}

View File

@@ -54,6 +54,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@@ -64,7 +65,6 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.mediapickers.test.FakePickerProvider
@@ -364,7 +364,7 @@ class MessagesPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Developer, aMessageEvent()))
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ViewSource, aMessageEvent()))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
assertThat(navigator.onShowEventDebugInfoClickedCount).isEqualTo(1)
}
@@ -614,7 +614,7 @@ class MessagesPresenterTest {
messageComposerContext = MessageComposerContextImpl(),
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
)
)
val timelinePresenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
room = matrixRoom,
@@ -622,12 +622,11 @@ class MessagesPresenterTest {
appScope = this,
analyticsService = analyticsService,
)
val buildMeta = aBuildMeta()
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true)
val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore)
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
val featureFlagsService = FakeFeatureFlagService(mapOf(FeatureFlags.RichTextEditor.key to true))
return MessagesPresenter(
room = matrixRoom,
composerPresenter = messageComposerPresenter,
@@ -642,7 +641,7 @@ class MessagesPresenterTest {
navigator = navigator,
clipboardHelper = clipboardHelper,
analyticsService = analyticsService,
featureFlagService = featureFlagsService,
preferencesStore = preferencesStore,
dispatchers = coroutineDispatchers,
)
}

View File

@@ -31,8 +31,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest
@@ -46,7 +46,7 @@ class ActionListPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
val presenter = anActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -57,7 +57,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for message from me redacted`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
val presenter = anActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -71,7 +71,7 @@ class ActionListPresenterTest {
ActionListState.Target.Success(
messageEvent,
persistentListOf(
TimelineItemAction.Developer,
TimelineItemAction.ViewSource,
)
)
)
@@ -82,7 +82,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for message from others redacted`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
val presenter = anActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -96,7 +96,7 @@ class ActionListPresenterTest {
ActionListState.Target.Success(
messageEvent,
persistentListOf(
TimelineItemAction.Developer,
TimelineItemAction.ViewSource,
)
)
)
@@ -107,7 +107,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for others message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
val presenter = anActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -127,7 +127,7 @@ class ActionListPresenterTest {
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Copy,
TimelineItemAction.Developer,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
)
)
@@ -139,7 +139,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for others message cannot sent message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
val presenter = anActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -158,7 +158,7 @@ class ActionListPresenterTest {
persistentListOf(
TimelineItemAction.Forward,
TimelineItemAction.Copy,
TimelineItemAction.Developer,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
)
)
@@ -170,7 +170,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for others message and can redact`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
val presenter = anActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -188,7 +188,7 @@ class ActionListPresenterTest {
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Copy,
TimelineItemAction.Developer,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
TimelineItemAction.Redact,
)
@@ -201,7 +201,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for my message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
val presenter = anActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -222,7 +222,7 @@ class ActionListPresenterTest {
TimelineItemAction.Forward,
TimelineItemAction.Edit,
TimelineItemAction.Copy,
TimelineItemAction.Developer,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
)
)
@@ -234,7 +234,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for a media item`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
val presenter = anActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -253,7 +253,7 @@ class ActionListPresenterTest {
persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Developer,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
)
)
@@ -265,7 +265,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for a state item in debug build`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
val presenter = anActionListPresenter(isDeveloperModeEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -283,7 +283,7 @@ class ActionListPresenterTest {
stateEvent,
persistentListOf(
TimelineItemAction.Copy,
TimelineItemAction.Developer,
TimelineItemAction.ViewSource,
)
)
)
@@ -294,7 +294,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for a state item in non-debuggable build`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = false)
val presenter = anActionListPresenter(isDeveloperModeEnabled = false)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -322,7 +322,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute message in non-debuggable build`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = false)
val presenter = anActionListPresenter(isDeveloperModeEnabled = false)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -354,7 +354,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute message with no actions`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = false)
val presenter = anActionListPresenter(isDeveloperModeEnabled = false)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -381,7 +381,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute not sent message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = false)
val presenter = anActionListPresenter(isDeveloperModeEnabled = false)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -410,7 +410,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for poll message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = false)
val presenter = anActionListPresenter(isDeveloperModeEnabled = false)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -436,7 +436,7 @@ class ActionListPresenterTest {
@Test
fun `present - compute for ended poll message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = false)
val presenter = anActionListPresenter(isDeveloperModeEnabled = false)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -460,5 +460,8 @@ class ActionListPresenterTest {
}
}
private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable))
private fun anActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter {
val preferencesStore = InMemoryPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
return ActionListPresenter(preferencesStore = preferencesStore)
}

View File

@@ -41,6 +41,7 @@ dependencies {
implementation(projects.libraries.featureflag.ui)
implementation(projects.libraries.network)
implementation(projects.libraries.pushstore.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
implementation(projects.features.rageshake.api)
@@ -54,6 +55,7 @@ dependencies {
implementation(libs.accompanist.placeholder)
implementation(libs.coil.compose)
implementation(libs.androidx.browser)
implementation(libs.androidx.datastore.preferences)
api(projects.features.preferences.api)
ksp(libs.showkase.processor)
@@ -64,6 +66,7 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl)

View File

@@ -31,11 +31,12 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.preferences.impl.about.AboutNode
import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNode
import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode
import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode
import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
@@ -63,6 +64,9 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize
data object DeveloperSettings : NavTarget
@Parcelize
data object AdvancedSettings : NavTarget
@Parcelize
data object ConfigureTracing : NavTarget
@@ -106,6 +110,10 @@ class PreferencesFlowNode @AssistedInject constructor(
override fun onOpenNotificationSettings() {
backstack.push(NavTarget.NotificationSettings)
}
override fun onOpenAdvancedSettings() {
backstack.push(NavTarget.AdvancedSettings)
}
}
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
}
@@ -138,6 +146,9 @@ class PreferencesFlowNode @AssistedInject constructor(
val input = EditDefaultNotificationSettingNode.Inputs(navTarget.isOneToOne)
createNode<EditDefaultNotificationSettingNode>(buildContext, plugins = listOf(input))
}
NavTarget.AdvancedSettings -> {
createNode<AdvancedSettingsNode>(buildContext)
}
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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.preferences.impl.advanced
sealed interface AdvancedSettingsEvents {
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
}

View File

@@ -0,0 +1,45 @@
/*
* 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.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class AdvancedSettingsNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: AdvancedSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
AdvancedSettingsView(
state = state,
modifier = modifier,
onBackPressed = ::navigateUp
)
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.launch
import javax.inject.Inject
class AdvancedSettingsPresenter @Inject constructor(
private val preferencesStore: PreferencesStore,
) : Presenter<AdvancedSettingsState> {
@Composable
override fun present(): AdvancedSettingsState {
val localCoroutineScope = rememberCoroutineScope()
val isRichTextEditorEnabled by preferencesStore
.isRichTextEditorEnabledFlow()
.collectAsState(initial = false)
val isDeveloperModeEnabled by preferencesStore
.isDeveloperModeEnabledFlow()
.collectAsState(initial = false)
fun handleEvents(event: AdvancedSettingsEvents) {
when (event) {
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
preferencesStore.setRichTextEditorEnabled(event.enabled)
}
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
preferencesStore.setDeveloperModeEnabled(event.enabled)
}
}
}
return AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
eventSink = ::handleEvents
)
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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.preferences.impl.advanced
data class AdvancedSettingsState constructor(
val isRichTextEditorEnabled: Boolean,
val isDeveloperModeEnabled: Boolean,
val eventSink: (AdvancedSettingsEvents) -> Unit
)

View File

@@ -0,0 +1,37 @@
/*
* 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.preferences.impl.advanced
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSettingsState> {
override val values: Sequence<AdvancedSettingsState>
get() = sequenceOf(
aAdvancedSettingsState(),
aAdvancedSettingsState(isRichTextEditorEnabled = true),
aAdvancedSettingsState(isDeveloperModeEnabled = true),
)
}
fun aAdvancedSettingsState(
isRichTextEditorEnabled: Boolean = false,
isDeveloperModeEnabled: Boolean = false,
) = AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
eventSink = {}
)

View File

@@ -0,0 +1,63 @@
/*
* 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.preferences.impl.advanced
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun AdvancedSettingsView(
state: AdvancedSettingsState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferenceView(
modifier = modifier,
onBackPressed = onBackPressed,
title = stringResource(id = CommonStrings.common_advanced_settings)
) {
PreferenceSwitch(
title = stringResource(id = CommonStrings.common_rich_text_editor),
// TODO i18n
subtitle = "Disable the rich text editor to type Markdown manually",
isChecked = state.isRichTextEditorEnabled,
onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetRichTextEditorEnabled(it)) },
)
PreferenceSwitch(
// TODO i18n
title = "Developer mode",
// TODO i18n
subtitle = "The developer mode activates hidden features. For developers only!",
isChecked = state.isDeveloperModeEnabled,
onCheckedChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
)
}
}
@DayNightPreviews
@Composable
internal fun AdvancedSettingsViewPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) =
ElementPreview {
AdvancedSettingsView(state = state, onBackPressed = { })
}

View File

@@ -46,6 +46,7 @@ class PreferencesRootNode @AssistedInject constructor(
fun onOpenAbout()
fun onOpenDeveloperSettings()
fun onOpenNotificationSettings()
fun onOpenAdvancedSettings()
}
private fun onOpenBugReport() {
@@ -60,6 +61,10 @@ class PreferencesRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onOpenDeveloperSettings() }
}
private fun onOpenAdvancedSettings() {
plugins<Callback>().forEach { it.onOpenAdvancedSettings() }
}
private fun onOpenAnalytics() {
plugins<Callback>().forEach { it.onOpenAnalytics() }
}
@@ -100,6 +105,7 @@ class PreferencesRootNode @AssistedInject constructor(
onOpenAbout = this::onOpenAbout,
onVerifyClicked = this::onVerifyClicked,
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
onOpenAdvancedSettings = this::onOpenAdvancedSettings,
onSuccessLogout = { onSuccessLogout(activity, it) },
onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) },
onOpenNotificationSettings = this::onOpenNotificationSettings

View File

@@ -25,6 +25,7 @@ import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.InsertChart
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.VerifiedUser
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -58,6 +59,7 @@ fun PreferencesRootView(
onOpenRageShake: () -> Unit,
onOpenAbout: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onOpenAdvancedSettings: () -> Unit,
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
onOpenNotificationSettings: () -> Unit,
modifier: Modifier = Modifier,
@@ -121,10 +123,15 @@ fun PreferencesRootView(
)
HorizontalDivider()
}
PreferenceText(
title = stringResource(id = CommonStrings.common_advanced_settings),
icon = Icons.Outlined.Settings,
onClick = onOpenAdvancedSettings,
)
if (state.showDeveloperSettings) {
DeveloperPreferencesView(onOpenDeveloperSettings)
HorizontalDivider()
}
HorizontalDivider()
LogoutPreferenceView(
state = state.logoutState,
onSuccessLogout = onSuccessLogout,
@@ -168,6 +175,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) {
onOpenAnalytics = {},
onOpenRageShake = {},
onOpenDeveloperSettings = {},
onOpenAdvancedSettings = {},
onOpenAbout = {},
onVerifyClicked = {},
onSuccessLogout = {},

View File

@@ -0,0 +1,78 @@
/*
* 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.preferences.impl.advanced
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class AdvancedSettingsPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val store = InMemoryPreferencesStore()
val presenter = AdvancedSettingsPresenter(store)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.isDeveloperModeEnabled).isFalse()
assertThat(initialState.isRichTextEditorEnabled).isFalse()
}
}
@Test
fun `present - developer mode on off`() = runTest {
val store = InMemoryPreferencesStore()
val presenter = AdvancedSettingsPresenter(store)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.isDeveloperModeEnabled).isFalse()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetDeveloperModeEnabled(true))
assertThat(awaitItem().isDeveloperModeEnabled).isTrue()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetDeveloperModeEnabled(false))
assertThat(awaitItem().isDeveloperModeEnabled).isFalse()
}
}
@Test
fun `present - rich text editor on off`() = runTest {
val store = InMemoryPreferencesStore()
val presenter = AdvancedSettingsPresenter(store)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.isRichTextEditorEnabled).isFalse()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetRichTextEditorEnabled(true))
assertThat(awaitItem().isRichTextEditorEnabled).isTrue()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetRichTextEditorEnabled(false))
assertThat(awaitItem().isRichTextEditorEnabled).isFalse()
}
}
}

View File

@@ -56,6 +56,3 @@ android.experimental.enableTestFixtures=true
# Create BuildConfig files as bytecode to avoid Java compilation phase
android.enableBuildConfigAsBytecode=true
# This should be removed after upgrading to AGP 8.1.0
android.suppressUnsupportedCompileSdk=34

View File

@@ -6,7 +6,7 @@
android_gradle_plugin = "8.1.1"
kotlin = "1.9.10"
ksp = "1.9.10-1.0.13"
molecule = "1.2.0"
molecule = "1.2.1"
# AndroidX
material = "1.9.0"
@@ -66,7 +66,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref
android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3"
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
# https://firebase.google.com/docs/android/setup#available-libraries
google_firebase_bom = "com.google.firebase:firebase-bom:32.2.3"
google_firebase_bom = "com.google.firebase:firebase-bom:32.3.0"
# AndroidX
androidx_material = { module = "com.google.android.material:material", version.ref = "material" }
@@ -149,7 +149,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.52"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.53"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }

View File

@@ -100,6 +100,7 @@ private fun ContentToPreview() {
"placeholderBackground" to ElementTheme.colors.placeholderBackground,
"messageFromMeBackground" to ElementTheme.colors.messageFromMeBackground,
"messageFromOtherBackground" to ElementTheme.colors.messageFromOtherBackground,
"progressIndicatorTrackColor" to ElementTheme.colors.progressIndicatorTrackColor,
"temporaryColorBgSpecial" to ElementTheme.colors.temporaryColorBgSpecial,
)
)

View File

@@ -43,9 +43,4 @@ enum class FeatureFlags(
title = "Show notification settings",
defaultValue = true,
),
RichTextEditor(
key = "feature.richtexteditor",
title = "Enable rich text editor",
defaultValue = true,
),
}

View File

@@ -35,7 +35,6 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.LocationSharing -> true
FeatureFlags.Polls -> true
FeatureFlags.NotificationSettings -> true
FeatureFlags.RichTextEditor -> true
}
} else {
false

View File

@@ -57,6 +57,8 @@ class RustMatrixAuthenticationService @Inject constructor(
userAgent = userAgentProvider.provide(),
oidcConfiguration = oidcConfiguration,
customSlidingSyncProxy = null,
sessionDelegate = null,
crossProcessRefreshLockId = null,
)
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)

View File

@@ -71,7 +71,7 @@ class RoomSummaryListProcessor(
}
}
private suspend fun MutableList<RoomSummary>.applyUpdate(update: RoomListEntriesUpdate) {
private fun MutableList<RoomSummary>.applyUpdate(update: RoomListEntriesUpdate) {
when (update) {
is RoomListEntriesUpdate.Append -> {
val roomSummaries = update.values.map {
@@ -114,7 +114,7 @@ class RoomSummaryListProcessor(
}
}
private suspend fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary {
private fun buildSummaryForRoomListEntry(entry: RoomListEntry): RoomSummary {
return when (entry) {
RoomListEntry.Empty -> buildEmptyRoomSummary()
is RoomListEntry.Filled -> buildAndCacheRoomSummaryForIdentifier(entry.roomId)
@@ -128,9 +128,9 @@ class RoomSummaryListProcessor(
return RoomSummary.Empty(UUID.randomUUID().toString())
}
private suspend fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
private fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary {
val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem ->
roomListItem.roomInfo().use { roomInfo ->
roomListItem.roomInfoBlocking().use { roomInfo ->
RoomSummary.Filled(
details = roomSummaryDetailsFactory.create(roomInfo)
)

View File

@@ -0,0 +1,27 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.preferences.api"
}
dependencies {
implementation(libs.coroutines.core)
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.preferences.api.store
import kotlinx.coroutines.flow.Flow
interface PreferencesStore {
suspend fun setRichTextEditorEnabled(enabled: Boolean)
fun isRichTextEditorEnabledFlow(): Flow<Boolean>
suspend fun setDeveloperModeEnabled(enabled: Boolean)
fun isDeveloperModeEnabledFlow(): Flow<Boolean>
suspend fun reset()
}

View File

@@ -0,0 +1,36 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.libraries.preferences.impl"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
api(projects.libraries.preferences.api)
implementation(libs.dagger)
implementation(libs.androidx.datastore.preferences)
implementation(projects.libraries.di)
implementation(projects.libraries.core)
}

View File

@@ -0,0 +1,77 @@
/*
* 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.libraries.preferences.impl.store
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.preferences.api.store.PreferencesStore
import io.element.android.libraries.core.bool.orTrue
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_preferences")
private val richTextEditorKey = booleanPreferencesKey("richTextEditor")
private val developerModeKey = booleanPreferencesKey("developerMode")
@ContributesBinding(AppScope::class)
class DefaultPreferencesStore @Inject constructor(
@ApplicationContext context: Context,
private val buildMeta: BuildMeta,
) : PreferencesStore {
private val store = context.dataStore
override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
store.edit { prefs ->
prefs[richTextEditorKey] = enabled
}
}
override fun isRichTextEditorEnabledFlow(): Flow<Boolean> {
return store.data.map { prefs ->
// enabled by default
prefs[richTextEditorKey].orTrue()
}
}
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
store.edit { prefs ->
prefs[developerModeKey] = enabled
}
}
override fun isDeveloperModeEnabledFlow(): Flow<Boolean> {
return store.data.map { prefs ->
// disabled by default on release and nightly, enabled by default on debug
prefs[developerModeKey] ?: (buildMeta.buildType == BuildType.DEBUG)
}
}
override suspend fun reset() {
store.edit { it.clear() }
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.preferences.test"
dependencies {
api(projects.libraries.preferences.api)
implementation(libs.coroutines.core)
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.libraries.featureflag.test
import io.element.android.features.preferences.api.store.PreferencesStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class InMemoryPreferencesStore(
isRichTextEditorEnabled: Boolean = false,
isDeveloperModeEnabled: Boolean = false,
) : PreferencesStore {
private var _isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled)
private var _isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
_isRichTextEditorEnabled.value = enabled
}
override fun isRichTextEditorEnabledFlow(): Flow<Boolean> {
return _isRichTextEditorEnabled
}
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
_isDeveloperModeEnabled.value = enabled
}
override fun isDeveloperModeEnabledFlow(): Flow<Boolean> {
return _isDeveloperModeEnabled
}
override suspend fun reset() {
// No op
}
}

View File

@@ -92,6 +92,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
implementation(project(":libraries:pushproviders:unifiedpush"))
implementation(project(":libraries:featureflag:impl"))
implementation(project(":libraries:pushstore:impl"))
implementation(project(":libraries:preferences:impl"))
implementation(project(":libraries:architecture"))
implementation(project(":libraries:dateformatter:impl"))
implementation(project(":libraries:di"))