Merge branch 'release/25.03.1' into main

This commit is contained in:
Benoit Marty 2025-03-06 16:58:44 +01:00
commit 232f1db645
675 changed files with 2857 additions and 2499 deletions

17
.github/workflows/blocked.yml vendored Normal file
View file

@ -0,0 +1,17 @@
name: Prevent blocked
on:
pull_request:
types: [opened, labeled, unlabeled]
jobs:
prevent-blocked:
name: Prevent blocked
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v7
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
core.setFailed("PR has been labeled with X-Blocked; it cannot be merged.");

View file

@ -29,7 +29,7 @@ jobs:
- name: Run code quality check suite
run: ./tools/check/check_code_quality.sh
checkScreesnhot:
checkScreenshot:
name: Search for invalid screenshot files
runs-on: ubuntu-latest
steps:

View file

@ -1,3 +1,80 @@
Changes in Element X v25.03.0
=============================
<!-- Release notes generated using configuration in .github/release.yml at v25.03.0 -->
## What's Changed
### ✨ Features
* Create `SyncOrchestrator` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4176
* feature(crypto): verification violation handling and block sending by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/4126
* Update Matrix Room API and allow media swipe on pinned event only. by @bmarty in https://github.com/element-hq/element-x-android/pull/4274
* Feature : join room by address by @ganfra in https://github.com/element-hq/element-x-android/pull/4302
### 🙌 Improvements
* Change : Room Preview by @ganfra in https://github.com/element-hq/element-x-android/pull/4250
### 🐛 Bugfixes
* SyncOrchestrator: restore the initial sync step by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4242
* When an emoji is used as the 'initial' for an avatar, use the whole emoji by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4277
* Try avoiding trailing punctuation inside linkified URLs by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4214
* Preload account urls by @bmarty in https://github.com/element-hq/element-x-android/pull/4301
* Fix issues due to multiple ntfy applications with the same name. by @bmarty in https://github.com/element-hq/element-x-android/pull/4312
* Use `Settings.System.DEFAULT_RINGTONE_URI` for ringing notifications by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4310
### 🗣 Translations
* Sync Strings - New translations to turkish by @ElementBot in https://github.com/element-hq/element-x-android/pull/4253
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/4298
### 🧱 Build
* Fix nightly reports by @bmarty in https://github.com/element-hq/element-x-android/pull/4235
* Fix nightly reports - next step by @bmarty in https://github.com/element-hq/element-x-android/pull/4239
* Prepare application for being configurable by @bmarty in https://github.com/element-hq/element-x-android/pull/4285
* runQualityChecks task shouldn't fail fast by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4309
* Get library ComposablePreviewScanner from maven and update to the latest version by @bmarty in https://github.com/element-hq/element-x-android/pull/4327
### Dependency upgrades
* Update dependency com.posthog:posthog-android to v3.11.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4230
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.78 by @renovate in https://github.com/element-hq/element-x-android/pull/4234
* Update dependency org.maplibre.gl:android-sdk to v11.8.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4245
* fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-datetime to v0.6.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4258
* fix(deps): update dependency io.sentry:sentry-android to v8.2.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4262
* fix(deps): update telephoto to v0.15.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4270
* fix(deps): update dependency com.google.firebase:firebase-bom to v33.9.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4249
* chore(deps): update danger/danger-js action to v12.3.4 by @renovate in https://github.com/element-hq/element-x-android/pull/4259
* fix(deps): update android.gradle.plugin to v8.8.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4263
* chore(deps): update plugin dependencycheck to v12.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4272
* fix(deps): update dependency org.matrix.rustcomponents:sdk-android to v25 by @renovate in https://github.com/element-hq/element-x-android/pull/4273
* fix(deps): update dependency androidx.compose:compose-bom to v2025.02.00 by @renovate in https://github.com/element-hq/element-x-android/pull/4261
* fix(deps): update kotlin to v2.1.10-1.0.30 by @renovate in https://github.com/element-hq/element-x-android/pull/4265
* fix(deps): update dependency io.github.zxing-cpp:android to v2.3.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4282
* fix(deps): update firebaseappdistribution to v5.1.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4246
* fix(deps): update dependencyanalysis to v2.8.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4251
* fix(deps): update dependency com.google.accompanist:accompanist-permissions to v0.37.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4283
* fix(deps): update dependency com.google.accompanist:accompanist-permissions to v0.37.2 by @renovate in https://github.com/element-hq/element-x-android/pull/4287
* fix(deps): update dependencyanalysis to v2.10.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4288
* fix(deps): update dependencyanalysis to v2.10.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4295
* Upgrade SDK version to 25.02.26 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/4305
* fix(deps): update kotlinpoet to v2.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4304
* Update compound by @bmarty in https://github.com/element-hq/element-x-android/pull/4319
* fix(deps): update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4324
* fix(deps): update activity to v1.10.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4321
* fix(deps): update dependency androidx.exifinterface:exifinterface to v1.4.0 - autoclosed by @renovate in https://github.com/element-hq/element-x-android/pull/4325
* fix(deps): update dependency androidx.constraintlayout:constraintlayout to v2.2.1 by @renovate in https://github.com/element-hq/element-x-android/pull/4322
* fix(deps): update dependency io.sentry:sentry-android to v8.3.0 by @renovate in https://github.com/element-hq/element-x-android/pull/4316
* fix(deps): update dependency com.posthog:posthog-android to v3.11.3 by @renovate in https://github.com/element-hq/element-x-android/pull/4313
* fix(deps): update dependency com.android.tools:desugar_jdk_libs to v2.1.5 by @renovate in https://github.com/element-hq/element-x-android/pull/4299
* chore(deps): update plugin detekt to v1.23.8 by @renovate in https://github.com/element-hq/element-x-android/pull/4292
### Others
* Update incoming call notification content to "📹 Incoming call" by @bmarty in https://github.com/element-hq/element-x-android/pull/4231
* Display a bottom sheet to let user confirm the DM creation by @bmarty in https://github.com/element-hq/element-x-android/pull/4233
* Open chat links in regular browser tabs by @cbs228 in https://github.com/element-hq/element-x-android/pull/4198
* Theme override by @bmarty in https://github.com/element-hq/element-x-android/pull/4226
* Allow user certificate in production builds. by @bmarty in https://github.com/element-hq/element-x-android/pull/4275
* Replace Material icons with Compound icons wherever it's possible by @bmarty in https://github.com/element-hq/element-x-android/pull/4323
## New Contributors
* @cbs228 made their first contribution in https://github.com/element-hq/element-x-android/pull/4198
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v25.02.0...v25.03.0
Changes in Element X v25.02.0 (2025-02-04)
==========================================
<!-- Release notes generated using configuration in .github/release.yml at v25.02.0 -->
## What's Changed

View file

@ -5,12 +5,15 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(DelicateCoilApi::class)
package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import coil.Coil
import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
@ -69,7 +72,7 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor(
super.onBuilt()
lifecycle.subscribe(
onCreate = {
Coil.setImageLoader(imageLoaderHolder.get(inputs.matrixClient))
SingletonImageLoader.setUnsafe(imageLoaderHolder.get(inputs.matrixClient))
},
)
}

View file

@ -10,34 +10,33 @@ package io.element.android.appnav
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
class LoggedInEventProcessor @Inject constructor(
private val snackbarDispatcher: SnackbarDispatcher,
roomMembershipObserver: RoomMembershipObserver,
private val roomMembershipObserver: RoomMembershipObserver,
) {
private var observingJob: Job? = null
private val displayLeftRoomMessage = roomMembershipObserver.updates
.map { !it.isUserInRoom }
fun observeEvents(coroutineScope: CoroutineScope) {
observingJob = coroutineScope.launch {
displayLeftRoomMessage
.filter { it }
.onEach {
displayMessage(CommonStrings.common_current_user_left_room)
observingJob = roomMembershipObserver.updates
.filter { !it.isUserInRoom }
.onEach {
when (it.change) {
MembershipChange.LEFT -> displayMessage(CommonStrings.common_current_user_left_room)
MembershipChange.INVITATION_REJECTED -> displayMessage(CommonStrings.common_current_user_rejected_invite)
MembershipChange.KNOCK_RETRACTED -> displayMessage(CommonStrings.common_current_user_canceled_knock)
else -> Unit
}
.launchIn(this)
}
}
.launchIn(coroutineScope)
}
fun stopObserving() {

View file

@ -262,10 +262,6 @@ class LoggedInFlowNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onOpenBugReport() }
}
override fun onRoomDirectorySearchClick() {
backstack.push(NavTarget.RoomDirectorySearch)
}
override fun onLogoutForNativeSlidingSyncMigrationNeeded() {
backstack.push(NavTarget.LogoutForNativeSlidingSyncMigrationNeeded)
}
@ -360,6 +356,10 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) {
backstack.replace(NavTarget.Room(roomIdOrAlias = roomIdOrAlias, serverNames = serverNames))
}
override fun onOpenRoomDirectory() {
backstack.push(NavTarget.RoomDirectorySearch)
}
}
createRoomEntryPoint

View file

@ -5,12 +5,15 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(DelicateCoilApi::class)
package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import coil.Coil
import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@ -55,7 +58,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
super.onBuilt()
lifecycle.subscribe(
onCreate = {
Coil.setImageLoader(notLoggedInImageLoaderFactory)
SingletonImageLoader.setUnsafe(notLoggedInImageLoaderFactory.newImageLoader())
},
)
}

View file

@ -0,0 +1,2 @@
Main changes in this version: Event cache / Join room by address.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -33,6 +33,9 @@ data class WidgetMessage(
@SerialName("im.vector.hangup")
HangUp,
@SerialName("io.element.close")
Close,
@SerialName("send_event")
SendEvent,
}

View file

@ -135,7 +135,7 @@ class CallScreenPresenter @AssistedInject constructor(
val parsedMessage = parseMessage(it)
if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) {
if (parsedMessage.action == WidgetMessage.Action.HangUp) {
if (parsedMessage.action == WidgetMessage.Action.Close) {
close(callWidgetDriver.value, navigator)
} else if (parsedMessage.action == WidgetMessage.Action.SendEvent) {
// This event is received when a member joins the call, the first one will be the current one

View file

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Pågående samtale"</string>
<string name="call_foreground_service_message_android">"Trykk for å gå tilbake til samtalen"</string>
<string name="call_foreground_service_title_android">"☎️ Samtale pågår"</string>
<string name="screen_incoming_call_subtitle_android">"Innkommende Element-anrop"</string>
</resources>

View file

@ -9,7 +9,7 @@ package io.element.android.features.call.notifications
import androidx.core.graphics.drawable.IconCompat
import androidx.test.platform.app.InstrumentationRegistry
import coil.ImageLoader
import coil3.ImageLoader
import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
import io.element.android.libraries.matrix.test.AN_EVENT_ID

View file

@ -174,7 +174,7 @@ class CallScreenPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - a received hang up message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
fun `present - a received close message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
val navigator = FakeCallScreenNavigator()
val widgetDriver = FakeMatrixWidgetDriver()
val presenter = createCallScreenPresenter(
@ -191,7 +191,7 @@ class CallScreenPresenterTest {
val initialState = awaitItem()
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
messageInterceptor.givenInterceptedMessage("""{"action":"im.vector.hangup","api":"fromWidget","widgetId":"1","requestId":"1"}""")
messageInterceptor.givenInterceptedMessage("""{"action":"io.element.close","api":"fromWidget","widgetId":"1","requestId":"1"}""")
// Let background coroutines run
runCurrent()

View file

@ -22,5 +22,6 @@ interface CreateRoomEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>)
fun onOpenRoomDirectory()
}
}

View file

@ -21,15 +21,19 @@ interface CreateRoomNavigator : Plugin {
fun onCreateNewRoom()
fun onShowJoinRoomByAddress()
fun onDismissJoinRoomByAddress()
fun onOpenRoomDirectory()
}
class DefaultCreateRoomNavigator(
private val backstack: BackStack<NavTarget>,
private val overlay: Overlay<NavTarget>,
private val openRoom: (RoomIdOrAlias, List<String>) -> Unit,
private val openRoomDirectory: () -> Unit,
) : CreateRoomNavigator {
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) = openRoom(roomIdOrAlias, serverNames)
override fun onOpenRoomDirectory() = openRoomDirectory()
override fun onCreateNewRoom() {
backstack.push(NavTarget.NewRoom)
}

View file

@ -60,6 +60,9 @@ class CreateRoomFlowNode @AssistedInject constructor(
overlay = overlay,
openRoom = { roomIdOrAlias, viaServers ->
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoom(roomIdOrAlias, viaServers) }
},
openRoomDirectory = {
plugins<CreateRoomEntryPoint.Callback>().forEach { it.onOpenRoomDirectory() }
}
)

View file

@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidity
@ -175,6 +176,7 @@ class ConfigureRoomPresenter @Inject constructor(
isEncrypted = config.roomVisibility is RoomVisibilityState.Private,
isDirect = false,
visibility = RoomVisibility.Private,
historyVisibilityOverride = RoomHistoryVisibility.Invited,
preset = RoomPreset.PRIVATE_CHAT,
invite = config.invites.map { it.userId },
avatar = avatarUrl,

View file

@ -55,7 +55,8 @@ class CreateRoomRootNode @AssistedInject constructor(
navigator.onOpenRoom(roomIdOrAlias = it.toRoomIdOrAlias(), serverNames = emptyList())
},
onJoinByAddressClick = navigator::onShowJoinRoomByAddress,
onInviteFriendsClick = { invitePeople(activity) }
onInviteFriendsClick = { invitePeople(activity) },
onRoomDirectorySearchClick = navigator::onOpenRoomDirectory
)
}

View file

@ -9,6 +9,8 @@ package io.element.android.features.createroom.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@ -20,6 +22,8 @@ import io.element.android.features.createroom.impl.userlist.UserListPresenterArg
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.usersearch.api.UserRepository
import kotlinx.coroutines.launch
@ -31,6 +35,7 @@ class CreateRoomRootPresenter @Inject constructor(
userListDataStore: UserListDataStore,
private val startDMAction: StartDMAction,
private val buildMeta: BuildMeta,
private val featureFlagService: FeatureFlagService,
) : Presenter<CreateRoomRootState> {
private val presenter = presenterFactory.create(
UserListPresenterArgs(
@ -47,6 +52,8 @@ class CreateRoomRootPresenter @Inject constructor(
val localCoroutineScope = rememberCoroutineScope()
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val isRoomDirectorySearchEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch).collectAsState(initial = false)
fun handleEvents(event: CreateRoomRootEvents) {
when (event) {
is CreateRoomRootEvents.StartDM -> localCoroutineScope.launch {
@ -64,6 +71,7 @@ class CreateRoomRootPresenter @Inject constructor(
applicationName = buildMeta.applicationName,
userListState = userListState,
startDmAction = startDmActionState.value,
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
eventSink = ::handleEvents,
)
}

View file

@ -15,5 +15,6 @@ data class CreateRoomRootState(
val applicationName: String,
val userListState: UserListState,
val startDmAction: AsyncAction<RoomId>,
val isRoomDirectorySearchEnabled: Boolean,
val eventSink: (CreateRoomRootEvents) -> Unit,
)

View file

@ -53,6 +53,9 @@ open class CreateRoomRootStateProvider : PreviewParameterProvider<CreateRoomRoot
aCreateRoomRootState(
startDmAction = ConfirmingStartDmWithMatrixUser(aMatrixUser()),
),
aCreateRoomRootState(
isRoomDirectorySearchEnabled = true,
),
)
}
@ -60,10 +63,12 @@ fun aCreateRoomRootState(
applicationName: String = "Element X Preview",
userListState: UserListState = aUserListState(),
startDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
isRoomDirectorySearchEnabled: Boolean = false,
eventSink: (CreateRoomRootEvents) -> Unit = {},
) = CreateRoomRootState(
applicationName = applicationName,
userListState = userListState,
startDmAction = startDmAction,
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
eventSink = eventSink,
)

View file

@ -56,6 +56,7 @@ fun CreateRoomRootView(
onOpenDM: (RoomId) -> Unit,
onInviteFriendsClick: () -> Unit,
onJoinByAddressClick: () -> Unit,
onRoomDirectorySearchClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
@ -91,6 +92,7 @@ fun CreateRoomRootView(
onNewRoomClick = onNewRoomClick,
onInvitePeopleClick = onInviteFriendsClick,
onJoinByAddressClick = onJoinByAddressClick,
onRoomDirectorySearchClick = onRoomDirectorySearchClick,
onDmClick = onOpenDM,
)
}
@ -156,6 +158,7 @@ private fun CreateRoomActionButtonsList(
onNewRoomClick: () -> Unit,
onInvitePeopleClick: () -> Unit,
onJoinByAddressClick: () -> Unit,
onRoomDirectorySearchClick: () -> Unit,
onDmClick: (RoomId) -> Unit,
) {
LazyColumn {
@ -166,6 +169,15 @@ private fun CreateRoomActionButtonsList(
onClick = onNewRoomClick,
)
}
if (state.isRoomDirectorySearchEnabled) {
item {
CreateRoomActionButton(
iconRes = CompoundDrawables.ic_compound_list_bulleted,
text = stringResource(id = R.string.screen_room_directory_search_title),
onClick = onRoomDirectorySearchClick,
)
}
}
item {
CreateRoomActionButton(
iconRes = CompoundDrawables.ic_compound_share_android,
@ -242,5 +254,6 @@ internal fun CreateRoomRootViewPreview(@PreviewParameter(CreateRoomRootStateProv
onOpenDM = {},
onJoinByAddressClick = {},
onInviteFriendsClick = {},
onRoomDirectorySearchClick = {},
)
}

View file

@ -20,4 +20,10 @@ To můžete kdykoli změnit v nastavení místnosti."</string>
<string name="screen_create_room_title">"Vytvořit místnost"</string>
<string name="screen_create_room_topic_label">"Téma (nepovinné)"</string>
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
<string name="screen_start_chat_join_room_by_address_action">"Vstoupit do místnosti pomocí adresy"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Neplatná adresa"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Zadejte…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Odpovídající místnost nalezena"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Místnost nebyla nalezena"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"např. #room-name:matrix.org"</string>
</resources>

View file

@ -20,4 +20,10 @@ Sa võid seda jututoa seadistustest alati muuta."</string>
<string name="screen_create_room_title">"Loo jututuba"</string>
<string name="screen_create_room_topic_label">"Teema (kui soovid lisada)"</string>
<string name="screen_start_chat_error_starting_chat">"Vestluse alustamisel tekkis viga"</string>
<string name="screen_start_chat_join_room_by_address_action">"Liitu jututoaga aadressi alusel"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"See pole kehtiv aadress"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Sisene…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Leidsime vastava jututoa"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Jututuba ei leidu"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"nt. #jututoa-nimi:matrix.org"</string>
</resources>

View file

@ -20,4 +20,10 @@ Vous pouvez modifier cela à tout moment dans les paramètres du salon."</string
<string name="screen_create_room_title">"Créer un salon"</string>
<string name="screen_create_room_topic_label">"Sujet (facultatif)"</string>
<string name="screen_start_chat_error_starting_chat">"Une erreur sest produite lors de la tentative de création de la discussion"</string>
<string name="screen_start_chat_join_room_by_address_action">"Saisir une adresse de salon"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Ce nest pas une adresse valide"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Saisir…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Ce salon existe"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Salon non trouvé"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"ex: #nom-du-salon:matrix.org"</string>
</resources>

View file

@ -20,4 +20,10 @@ Môžete to kedykoľvek zmeniť v nastaveniach miestnosti."</string>
<string name="screen_create_room_title">"Vytvoriť miestnosť"</string>
<string name="screen_create_room_topic_label">"Téma (voliteľné)"</string>
<string name="screen_start_chat_error_starting_chat">"Pri pokuse o spustenie konverzácie sa vyskytla chyba"</string>
<string name="screen_start_chat_join_room_by_address_action">"Pripojte sa do miestnosti podľa adresy"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Neplatná adresa"</string>
<string name="screen_start_chat_join_room_by_address_placeholder">"Zadajte…"</string>
<string name="screen_start_chat_join_room_by_address_room_found">"Nájdená zodpovedajúca miestnosť"</string>
<string name="screen_start_chat_join_room_by_address_room_not_found">"Miestnosť sa nenašla"</string>
<string name="screen_start_chat_join_room_by_address_supporting_text">"napr. #nazov-miestnosti:matrix.org"</string>
</resources>

View file

@ -14,6 +14,7 @@ Du kan ändra detta när som helst i rumsinställningarna."</string>
<string name="screen_create_room_room_access_section_knocking_option_description">"Vem som helst kan be om att gå med i rummet men en administratör eller en moderator måste acceptera begäran"</string>
<string name="screen_create_room_room_access_section_knocking_option_title">"Be om att gå med"</string>
<string name="screen_create_room_room_address_section_footer">"För att detta rum ska vara synligt i den allmänna rumskatalogen behöver du en rumsadress."</string>
<string name="screen_create_room_room_address_section_title">"Rumsadress"</string>
<string name="screen_create_room_room_name_label">"Rumsnamn"</string>
<string name="screen_create_room_room_visibility_section_title">"Rumssynlighet"</string>
<string name="screen_create_room_title">"Skapa ett rum"</string>

View file

@ -19,6 +19,7 @@ You can change this anytime in room settings."</string>
<string name="screen_create_room_room_visibility_section_title">"Room visibility"</string>
<string name="screen_create_room_title">"Create a room"</string>
<string name="screen_create_room_topic_label">"Topic (optional)"</string>
<string name="screen_room_directory_search_title">"Room directory"</string>
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
<string name="screen_start_chat_join_room_by_address_action">"Join room by address"</string>
<string name="screen_start_chat_join_room_by_address_invalid_address">"Not a valid address"</string>

View file

@ -15,7 +15,8 @@ class FakeCreateRoomNavigator(
private val createNewRoomLambda: () -> Unit = {},
private val showJoinRoomByAddressLambda: () -> Unit = {},
private val dismissJoinRoomByAddressLambda: () -> Unit = {},
) : CreateRoomNavigator {
private val openRoomDirectoryLambda: () -> Unit = {},
) : CreateRoomNavigator {
override fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>) {
openRoomLambda(roomIdOrAlias, serverNames)
}
@ -31,4 +32,8 @@ class FakeCreateRoomNavigator(
override fun onDismissJoinRoomByAddress() {
dismissJoinRoomByAddressLambda()
}
override fun onOpenRoomDirectory() {
openRoomDirectoryLambda()
}
}

View file

@ -19,6 +19,8 @@ import io.element.android.features.createroom.impl.userlist.FakeUserListPresente
import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.features.createroom.test.FakeStartDMAction
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
@ -163,14 +165,34 @@ class CreateRoomRootPresenterTest {
}
}
@Test
fun `present - room directory search`() = runTest {
val presenter = createCreateRoomRootPresenter(isRoomDirectorySearchEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
awaitItem().let { state ->
assertThat(state.isRoomDirectorySearchEnabled).isTrue()
}
}
}
private fun createCreateRoomRootPresenter(
startDMAction: StartDMAction = FakeStartDMAction(),
isRoomDirectorySearchEnabled: Boolean = false,
): CreateRoomRootPresenter {
val featureFlagService = FakeFeatureFlagService(
initialState = mapOf(
FeatureFlags.RoomDirectorySearch.key to isRoomDirectorySearchEnabled,
),
)
return CreateRoomRootPresenter(
presenterFactory = FakeUserListPresenterFactory(FakeUserListPresenter()),
userRepository = FakeUserRepository(),
userListDataStore = UserListDataStore(),
startDMAction = startDMAction,
featureFlagService = featureFlagService,
buildMeta = aBuildMeta(),
)
}

View file

@ -116,6 +116,21 @@ class CreateRoomRootViewTest {
rule.clickOn(R.string.screen_start_chat_join_room_by_address_action)
}
}
@Test
fun `clicking on room directory invokes the expected callback`() {
val eventsRecorder = EventsRecorder<CreateRoomRootEvents>(expectEvents = false)
ensureCalledOnce {
rule.setCreateRoomRootView(
aCreateRoomRootState(
eventSink = eventsRecorder,
isRoomDirectorySearchEnabled = true
),
onRoomDirectorySearchClick = it
)
rule.clickOn(R.string.screen_room_directory_search_title)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreateRoomRootView(
@ -125,6 +140,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreat
onOpenDM: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onInviteFriendsClick: () -> Unit = EnsureNeverCalled(),
onJoinRoomByAddressClick: () -> Unit = EnsureNeverCalled(),
onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
CreateRoomRootView(
@ -133,7 +149,8 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setCreat
onNewRoomClick = onNewRoomClick,
onOpenDM = onOpenDM,
onInviteFriendsClick = onInviteFriendsClick,
onJoinByAddressClick = onJoinRoomByAddressClick
onJoinByAddressClick = onJoinRoomByAddressClick,
onRoomDirectorySearchClick = onRoomDirectorySearchClick,
)
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_optin_subtitle">"Du kan endre innstillingene dine senere."</string>
<string name="screen_notification_optin_title">"Tillat varslinger og gå aldri glipp av en melding"</string>
<string name="screen_welcome_bullet_1">"Samtaler, avstemninger, søk og mer vil bli lagt til senere i år."</string>
<string name="screen_welcome_bullet_2">"Meldingshistorikk for krypterte rom er ikke tilgjengelig ennå."</string>
<string name="screen_welcome_bullet_3">"Vi vil gjerne høre fra deg, så la oss få vite hva du synes via innstillingssiden."</string>
<string name="screen_welcome_button">"Kom igjen!"</string>
<string name="screen_welcome_subtitle">"Her er hva du trenger å vite:"</string>
<string name="screen_welcome_title">"Velkommen til %1$s!"</string>
</resources>

View file

@ -8,6 +8,6 @@
package io.element.android.features.invite.api.response
interface AcceptDeclineInviteEvents {
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData) : AcceptDeclineInviteEvents
data class AcceptInvite(val invite: InviteData?) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData?, val blockUser: Boolean = false) : AcceptDeclineInviteEvents
}

View file

@ -10,6 +10,7 @@ package io.element.android.features.invite.api.response
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
override val values: Sequence<AcceptDeclineInviteState>
@ -17,12 +18,20 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
anAcceptDeclineInviteState(),
anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite(
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice")
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice", senderId = UserId("@alice:matrix.org")),
blockUser = false,
),
),
anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite(
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room")
InviteData(roomId = RoomId("!room:matrix.org"), isDm = false, roomName = "Some room", senderId = UserId("@alice:matrix.org")),
blockUser = false,
),
),
anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite(
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice", senderId = UserId("@alice:matrix.org")),
blockUser = true,
),
),
anAcceptDeclineInviteState(

View file

@ -11,4 +11,5 @@ import io.element.android.libraries.architecture.AsyncAction
data class ConfirmingDeclineInvite(
val inviteData: InviteData,
val blockUser: Boolean,
) : AsyncAction.Confirming

View file

@ -8,8 +8,10 @@
package io.element.android.features.invite.api.response
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
data class InviteData(
val senderId: UserId,
val roomId: RoomId,
val roomName: String,
val isDm: Boolean,

View file

@ -16,6 +16,7 @@ import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -43,15 +44,34 @@ class AcceptDeclineInvitePresenter @Inject constructor(
fun handleEvents(event: AcceptDeclineInviteEvents) {
when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> {
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
val inviteData = event.invite
if (inviteData == null) {
acceptedAction.value = AsyncAction.Failure(InvalidDataException())
} else {
localCoroutineScope.acceptInvite(inviteData.roomId, acceptedAction)
}
}
is AcceptDeclineInviteEvents.DeclineInvite -> {
declinedAction.value = ConfirmingDeclineInvite(event.invite)
val inviteData = event.invite
if (inviteData == null) {
declinedAction.value = AsyncAction.Failure(InvalidDataException())
} else {
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
}
}
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
localCoroutineScope.declineInvite(event.roomId, declinedAction)
when (val declinedActionValue = declinedAction.value) {
is ConfirmingDeclineInvite -> {
localCoroutineScope.declineInvite(
inviteData = declinedActionValue.inviteData,
declinedAction = declinedAction,
blockUser = declinedActionValue.blockUser,
)
}
else -> Unit
}
}
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
@ -92,13 +112,20 @@ class AcceptDeclineInvitePresenter @Inject constructor(
}
}
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncAction<RoomId>>) = launch {
private fun CoroutineScope.declineInvite(
inviteData: InviteData,
blockUser: Boolean,
declinedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
suspend {
client.getPendingRoom(roomId)?.use {
client.getPendingRoom(inviteData.roomId)?.use {
it.leave().getOrThrow()
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
}
roomId
if (blockUser) {
client.ignoreUser(inviteData.senderId).getOrThrow()
}
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, inviteData.roomId)
inviteData.roomId
}.runCatchingUpdatingState(declinedAction)
}
}

View file

@ -50,8 +50,9 @@ fun AcceptDeclineInviteView(
if (confirming is ConfirmingDeclineInvite) {
DeclineConfirmationDialog(
invite = confirming.inviteData,
blockUser = confirming.blockUser,
onConfirmClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(confirming.inviteData.roomId))
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
},
onDismissClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
@ -66,29 +67,35 @@ fun AcceptDeclineInviteView(
@Composable
private fun DeclineConfirmationDialog(
invite: InviteData,
blockUser: Boolean,
onConfirmClick: () -> Unit,
onDismissClick: () -> Unit,
modifier: Modifier = Modifier
) {
val contentResource = if (invite.isDm) {
R.string.screen_invites_decline_direct_chat_message
} else {
R.string.screen_invites_decline_chat_message
val senderId = invite.senderId.value
val content = when {
blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_message, senderId)
invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_message, invite.roomName)
else -> stringResource(R.string.screen_invites_decline_chat_message, invite.roomName)
}
val titleResource = if (invite.isDm) {
R.string.screen_invites_decline_direct_chat_title
} else {
R.string.screen_invites_decline_chat_title
val title = when {
blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_title)
invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_title)
else -> stringResource(R.string.screen_invites_decline_chat_title)
}
val submitText = if (blockUser) {
stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation)
} else {
stringResource(CommonStrings.action_decline)
}
ConfirmationDialog(
modifier = modifier,
content = stringResource(contentResource, invite.roomName),
title = stringResource(titleResource),
submitText = stringResource(CommonStrings.action_decline),
content = content,
title = title,
submitText = submitText,
cancelText = stringResource(CommonStrings.action_cancel),
onSubmitClick = onConfirmClick,
destructiveSubmit = blockUser,
onDismiss = onDismissClick,
)
}

View file

@ -8,10 +8,9 @@
package io.element.android.features.invite.impl.response
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.libraries.matrix.api.core.RoomId
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
data class ConfirmDeclineInvite(val roomId: RoomId) : InternalAcceptDeclineInviteEvents
data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
data object DismissDeclineError : InternalAcceptDeclineInviteEvents

View file

@ -0,0 +1,10 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
class InvalidDataException : Exception()

View file

@ -6,4 +6,8 @@
<string name="screen_invites_decline_direct_chat_title">"Decline chat"</string>
<string name="screen_invites_empty_list">"No Invites"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Yes, decline &amp; block"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Are you sure you want to decline the invite to join this room? This will also prevent %1$s from contacting you or inviting you to rooms."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Decline invite &amp; block"</string>
<string name="screen_join_room_decline_and_block_button_title">"Decline and block"</string>
</resources>

View file

@ -17,10 +17,12 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeRoomPreview
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
@ -61,7 +63,7 @@ class AcceptDeclineInvitePresenterTest {
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
)
@ -91,9 +93,9 @@ class AcceptDeclineInvitePresenterTest {
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
@ -139,9 +141,9 @@ class AcceptDeclineInvitePresenterTest {
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData))
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId)
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
@ -156,6 +158,80 @@ class AcceptDeclineInvitePresenterTest {
.with(value(A_SESSION_ID), value(A_ROOM_ID))
}
@Test
fun `present - declining invite with block success flow`() = runTest {
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
Result.success(Unit)
}
val fakeNotificationCleaner = FakeNotificationCleaner(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
)
val declineInviteSuccess = lambdaRecorder { -> Result.success(Unit) }
val ignoreUserSuccess = lambdaRecorder { _: UserId -> Result.success(Unit) }
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(FakeRoomPreview(declineInviteResult = declineInviteSuccess))
},
ignoreUserResult = ignoreUserSuccess
)
val presenter = createAcceptDeclineInvitePresenter(
client = client,
notificationCleaner = fakeNotificationCleaner,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, true))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
declineInviteSuccess.assertions().isCalledOnce()
ignoreUserSuccess.assertions().isCalledOnce().with(value(A_USER_ID))
clearMembershipNotificationForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
}
@Test
fun `present - declining invite with block error flow`() = runTest {
val declineInviteFailure = lambdaRecorder { ->
Result.failure<Unit>(RuntimeException("Failed to leave room"))
}
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(FakeRoomPreview(declineInviteResult = declineInviteFailure))
}
)
val presenter = createAcceptDeclineInvitePresenter(client = client)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, true))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
}
}
@Test
fun `present - accepting invite error flow`() = runTest {
val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
@ -237,12 +313,14 @@ class AcceptDeclineInvitePresenterTest {
private fun anInviteData(
roomId: RoomId = A_ROOM_ID,
name: String = A_ROOM_NAME,
isDm: Boolean = false
isDm: Boolean = false,
senderId: UserId = A_USER_ID,
): InviteData {
return InviteData(
roomId = roomId,
roomName = name,
isDm = isDm
isDm = isDm,
senderId = senderId,
)
}

View file

@ -17,5 +17,5 @@ sealed interface JoinRoomEvents {
data class UpdateKnockMessage(val message: String) : JoinRoomEvents
data object ClearActionStates : JoinRoomEvents
data object AcceptInvite : JoinRoomEvents
data object DeclineInvite : JoinRoomEvents
data class DeclineInvite(val blockUser: Boolean) : JoinRoomEvents
}

View file

@ -152,15 +152,15 @@ class JoinRoomPresenter @AssistedInject constructor(
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
JoinRoomEvents.AcceptInvite -> {
val inviteData = contentState.toInviteData() ?: return
val inviteData = contentState.toInviteData()
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
)
}
JoinRoomEvents.DeclineInvite -> {
val inviteData = contentState.toInviteData() ?: return
is JoinRoomEvents.DeclineInvite -> {
val inviteData = contentState.toInviteData()
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
AcceptDeclineInviteEvents.DeclineInvite(invite = inviteData, blockUser = event.blockUser)
)
}
is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
@ -314,12 +314,19 @@ private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus {
@VisibleForTesting
internal fun ContentState.toInviteData(): InviteData? {
return when (this) {
is ContentState.Loaded -> InviteData(
roomId = roomId,
// Note: name should not be null at this point, but use Id just in case...
roomName = name ?: roomId.value,
isDm = isDm
)
is ContentState.Loaded -> {
if (joinAuthorisationStatus is JoinAuthorisationStatus.IsInvited && joinAuthorisationStatus.inviteSender != null) {
InviteData(
roomId = roomId,
// Note: name should not be null at this point, but use Id just in case...
roomName = name ?: roomId.value,
senderId = joinAuthorisationStatus.inviteSender.userId,
isDm = isDm
)
} else {
null
}
}
else -> null
}
}

View file

@ -63,6 +63,7 @@ import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
@ -105,8 +106,8 @@ fun JoinRoomView(
onAcceptInvite = {
state.eventSink(JoinRoomEvents.AcceptInvite)
},
onDeclineInvite = {
state.eventSink(JoinRoomEvents.DeclineInvite)
onDeclineInvite = { blockUser ->
state.eventSink(JoinRoomEvents.DeclineInvite(blockUser))
},
onJoinRoom = {
state.eventSink(JoinRoomEvents.JoinRoom)
@ -183,7 +184,7 @@ fun JoinRoomView(
private fun JoinRoomFooter(
joinAuthorisationStatus: JoinAuthorisationStatus,
onAcceptInvite: () -> Unit,
onDeclineInvite: () -> Unit,
onDeclineInvite: (Boolean) -> Unit,
onJoinRoom: () -> Unit,
onKnockRoom: () -> Unit,
onCancelKnock: () -> Unit,
@ -193,23 +194,32 @@ private fun JoinRoomFooter(
) {
Box(
modifier = modifier
.fillMaxWidth()
.padding(top = 8.dp)
.fillMaxWidth()
.padding(top = 8.dp)
) {
when (joinAuthorisationStatus) {
is JoinAuthorisationStatus.IsInvited -> {
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
OutlinedButton(
text = stringResource(CommonStrings.action_decline),
onClick = onDeclineInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = onAcceptInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
Column {
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
OutlinedButton(
text = stringResource(CommonStrings.action_decline),
onClick = { onDeclineInvite(false) },
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = onAcceptInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
)
}
Spacer(modifier = Modifier.height(24.dp))
TextButton(
text = stringResource(R.string.screen_join_room_decline_and_block_button_title),
onClick = { onDeclineInvite(true) },
modifier = Modifier.fillMaxWidth(),
destructive = true
)
}
}
@ -372,12 +382,19 @@ private fun JoinRoomContent(
IsKnockedLoadedContent()
}
else -> {
DefaultLoadedContent(
modifier = Modifier.verticalScroll(rememberScrollState()),
contentState = contentState,
knockMessage = knockMessage,
onKnockMessageUpdate = onKnockMessageUpdate
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
if (inviteSender != null) {
InviteSenderView(inviteSender = inviteSender)
Spacer(modifier = Modifier.height(32.dp))
}
DefaultLoadedContent(
modifier = Modifier.verticalScroll(rememberScrollState()),
contentState = contentState,
knockMessage = knockMessage,
onKnockMessageUpdate = onKnockMessageUpdate
)
}
}
}
}
@ -440,8 +457,8 @@ private fun IncompleteContent(
private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
BoxWithConstraints(
modifier = modifier
.fillMaxHeight()
.padding(horizontal = 16.dp),
.fillMaxHeight()
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
IconTitleSubtitleMolecule(
@ -487,10 +504,6 @@ private fun DefaultLoadedContent(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
if (inviteSender != null) {
InviteSenderView(inviteSender = inviteSender)
}
RoomPreviewDescriptionAtom(contentState.topic ?: "")
if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
Spacer(modifier = Modifier.height(24.dp))

View file

@ -4,6 +4,8 @@
<string name="screen_join_room_cancel_knock_alert_confirmation">"Ja, avbryt"</string>
<string name="screen_join_room_cancel_knock_alert_description">"Är du säker på att du vill avbryta din begäran om att gå med i det här rummet?"</string>
<string name="screen_join_room_cancel_knock_alert_title">"Avbryt begäran om att gå med"</string>
<string name="screen_join_room_forget_action">"Glöm det här rummet"</string>
<string name="screen_join_room_invite_required_message">"Du behöver en inbjudan för att gå med i detta rum"</string>
<string name="screen_join_room_join_action">"Gå med i rummet"</string>
<string name="screen_join_room_knock_action">"Knacka för att gå med"</string>
<string name="screen_join_room_knock_message_description">"Meddelande (valfritt)"</string>

View file

@ -167,9 +167,9 @@ class JoinRoomPresenterTest {
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.AcceptInvite)
state.eventSink(JoinRoomEvents.DeclineInvite)
state.eventSink(JoinRoomEvents.DeclineInvite(false))
val inviteData = state.contentState.toInviteData()!!
val inviteData = state.contentState.toInviteData()
assert(eventSinkRecorder)
.isCalledExactly(2)

View file

@ -139,7 +139,7 @@ class JoinRoomViewTest {
}
@Test
fun `clicking on Accept invitation IsInvited room emits the expected Event`() {
fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
@ -152,7 +152,7 @@ class JoinRoomViewTest {
}
@Test
fun `clicking on Decline invitation on IsInvited room emits the expected Event`() {
fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
@ -161,7 +161,20 @@ class JoinRoomViewTest {
),
)
rule.clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(false))
}
@Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_join_room_decline_and_block_button_title)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(true))
}
@Test

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
@ -22,9 +23,10 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil3.Extras
import coil3.compose.AsyncImagePainter
import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.location.api.internal.StaticMapPlaceholder
import io.element.android.features.location.api.internal.StaticMapUrlBuilder
@ -33,7 +35,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.utils.CommonDrawables
import timber.log.Timber
/**
* Shows a static map image downloaded via a third party service's static maps API.
@ -75,14 +76,15 @@ fun StaticMapView(
)
)
.size(width = constraints.maxWidth, height = constraints.maxHeight)
.setParameter("retry_hash", retryHash, memoryCacheKey = null)
.apply {
extras.set(Extras.Key("retry_hash"), retryHash).build()
}
.build()
}.apply {
Timber.d("Static map image request: ${this?.data}")
}
)
if (painter.state is AsyncImagePainter.State.Success) {
val collectedState = painter.state.collectAsState()
if (collectedState.value is AsyncImagePainter.State.Success) {
Image(
painter = painter,
contentDescription = contentDescription,
@ -100,7 +102,7 @@ fun StaticMapView(
)
} else {
StaticMapPlaceholder(
showProgress = painter.state is AsyncImagePainter.State.Loading,
showProgress = collectedState.value.isLoading(),
contentDescription = contentDescription,
width = maxWidth,
height = maxHeight,
@ -110,6 +112,11 @@ fun StaticMapView(
}
}
private fun AsyncImagePainter.State.isLoading(): Boolean {
return this is AsyncImagePainter.State.Empty ||
this is AsyncImagePainter.State.Loading
}
@PreviewsDayNight
@Composable
internal fun StaticMapViewPreview() = ElementPreview {

View file

@ -11,16 +11,17 @@ 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.compound.theme.ElementTheme
import io.element.android.features.lockscreen.impl.R
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun LockScreenSettingsView(
@ -35,15 +36,19 @@ fun LockScreenSettingsView(
modifier = modifier
) {
PreferenceCategory(showTopDivider = false) {
PreferenceText(
title = stringResource(id = R.string.screen_app_lock_settings_change_pin),
onClick = onChangePinClick
ListItem(
headlineContent = {
Text(stringResource(id = R.string.screen_app_lock_settings_change_pin))
},
onClick = onChangePinClick,
)
PreferenceDivider()
if (state.showRemovePinOption) {
PreferenceText(
title = stringResource(id = R.string.screen_app_lock_settings_remove_pin),
tintColor = ElementTheme.colors.textCriticalPrimary,
ListItem(
headlineContent = {
Text(stringResource(id = R.string.screen_app_lock_settings_remove_pin))
},
style = ListItemStyle.Destructive,
onClick = {
state.eventSink(LockScreenSettingsEvents.OnRemovePin)
}

View file

@ -2,12 +2,14 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_account_provider_change">"Bytt kontotilbyder"</string>
<string name="screen_account_provider_form_hint">"Hjemmeserveradresse"</string>
<string name="screen_account_provider_form_notice">"Skriv inn et søkeord eller en domeneadresse."</string>
<string name="screen_account_provider_form_subtitle">"Søk etter et selskap, fellesskap eller privat server."</string>
<string name="screen_account_provider_form_title">"Finn en kontoleverandør"</string>
<string name="screen_account_provider_signin_subtitle">"Det er her samtalene dine vil ligge - akkurat som du ville brukt en e-postleverandør til å oppbevare e-postene dine."</string>
<string name="screen_account_provider_signin_title">"Du er i ferd med å logge inn på %s"</string>
<string name="screen_account_provider_signup_subtitle">"Det er her samtalene dine vil ligge - akkurat som du ville brukt en e-postleverandør til å oppbevare e-postene dine."</string>
<string name="screen_account_provider_signup_title">"Du er i ferd med å opprette en konto på %s"</string>
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org er en stor, gratis server på det offentlige Matrix-nettverket for sikker, desentralisert kommunikasjon, drevet av Matrix.org Foundation."</string>
<string name="screen_change_account_provider_other">"Annet"</string>
<string name="screen_change_account_provider_subtitle">"Bruk en annen kontotilbyder, for eksempel din egen private server eller en arbeidskonto."</string>
<string name="screen_change_account_provider_title">"Bytt kontotilbyder"</string>
@ -16,6 +18,7 @@
<string name="screen_change_server_form_header">"URL til hjemmeserver"</string>
<string name="screen_change_server_form_notice">"Du kan bare koble til en eksisterende server som støtter sliding sync. Administrator for din hjemmeserver må konfigurere det. %1$s"</string>
<string name="screen_change_server_subtitle">"Hva er adressen til serveren din?"</string>
<string name="screen_change_server_title">"Velg din server"</string>
<string name="screen_create_account_title">"Opprett konto"</string>
<string name="screen_login_error_deactivated_account">"Denne kontoen er deaktivert."</string>
<string name="screen_login_error_invalid_credentials">"Feil brukernavn og/eller passord"</string>

View file

@ -33,7 +33,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil3.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.R
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction

View file

@ -37,8 +37,8 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
import io.element.android.features.messages.impl.timeline.components.ATimelineItemEventRow

View file

@ -25,8 +25,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider
import io.element.android.features.messages.impl.timeline.protection.ProtectedView

View file

@ -40,8 +40,8 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent

View file

@ -50,7 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil3.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.components.REACTION_IMAGE_ASPECT_RATIO
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction

View file

@ -17,14 +17,24 @@
<string name="screen_room_attachment_source_camera_video">"Ta opp video"</string>
<string name="screen_room_attachment_source_files">"Vedlegg"</string>
<string name="screen_room_attachment_source_gallery">"Foto- og videobibliotek"</string>
<string name="screen_room_attachment_source_location">"Lokasjon"</string>
<string name="screen_room_attachment_source_poll">"Avstemning"</string>
<string name="screen_room_attachment_text_formatting">"Tekstformatering"</string>
<string name="screen_room_encrypted_history_banner">"Meldingshistorikken er for øyeblikket ikke tilgjengelig."</string>
<string name="screen_room_encrypted_history_banner_unverified">"Meldingshistorikk er ikke tilgjengelig i dette rommet. Bekreft denne enheten for å se meldingshistorikken din."</string>
<string name="screen_room_invite_again_alert_message">"Vil du invitere dem tilbake?"</string>
<string name="screen_room_invite_again_alert_title">"Du er alene i denne chatten"</string>
<string name="screen_room_mentions_at_room_title">"Alle"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Send igjen"</string>
<string name="screen_room_retry_send_menu_title">"Meldingen din ble ikke sendt"</string>
<string name="screen_room_timeline_add_reaction">"Legg til emoji"</string>
<string name="screen_room_timeline_beginning_of_room">"Dette er begynnelsen på %1$s."</string>
<string name="screen_room_timeline_beginning_of_room_no_name">"Dette er begynnelsen på denne samtalen."</string>
<string name="screen_room_timeline_less_reactions">"Vis mindre"</string>
<string name="screen_room_timeline_message_copied">"Melding kopiert"</string>
<string name="screen_room_timeline_no_permission_to_post">"Du har ikke tillatelse til å legge ut innlegg i dette rommet"</string>
<string name="screen_room_timeline_reactions_show_less">"Vis mindre"</string>
<string name="screen_room_timeline_reactions_show_more">"Vis mer"</string>
<string name="screen_room_timeline_read_marker_title">"Ny"</string>
<plurals name="screen_room_timeline_state_changes">
<item quantity="one">"%1$d romendring"</item>

View file

@ -3,6 +3,7 @@
<string name="screen_onboarding_sign_in_manually">"Logg på manuelt"</string>
<string name="screen_onboarding_sign_in_with_qr_code">"Logg inn med QR-kode"</string>
<string name="screen_onboarding_sign_up">"Opprett konto"</string>
<string name="screen_onboarding_welcome_message">"Velkommen til den raskeste %1$s noensinne. Superladet for hastighet og enkelhet."</string>
<string name="screen_onboarding_welcome_subtitle">"Velkommen til %1$s. Supercharged, for hastighet og enkelhet."</string>
<string name="screen_onboarding_welcome_title">"Vær i ditt rette element"</string>
</resources>

View file

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Legg til alternativ"</string>
<string name="screen_create_poll_anonymous_desc">"Vis resultater bare etter at avstemningen er avsluttet"</string>
<string name="screen_create_poll_anonymous_headline">"Skjul stemmer"</string>
<string name="screen_create_poll_answer_hint">"Alternativ %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Endringene dine er ikke lagret. Er du sikker på at du vil gå tilbake?"</string>
<string name="screen_create_poll_question_desc">"Spørsmål eller emne"</string>
<string name="screen_create_poll_question_hint">"Hva handler avstemningen om?"</string>
<string name="screen_create_poll_title">"Opprett avstemning"</string>
<string name="screen_edit_poll_delete_confirmation">"Er du sikker på at du vil slette denne avstemningen?"</string>

View file

@ -63,6 +63,9 @@ dependencies {
implementation(libs.androidx.datastore.preferences)
api(projects.features.preferences.api)
implementation(platform(libs.network.okhttp.bom))
implementation(libs.network.okhttp)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)

View file

@ -12,9 +12,10 @@ 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.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
@ -31,13 +32,17 @@ fun AboutView(
title = stringResource(id = CommonStrings.common_about)
) {
state.elementLegals.forEach { elementLegal ->
PreferenceText(
title = stringResource(id = elementLegal.titleRes),
ListItem(
headlineContent = {
Text(stringResource(id = elementLegal.titleRes))
},
onClick = { onElementLegalClick(elementLegal) }
)
}
PreferenceText(
title = stringResource(id = CommonStrings.common_open_source_licenses),
ListItem(
headlineContent = {
Text(stringResource(id = CommonStrings.common_open_source_licenses))
},
onClick = onOpenSourceLicensesClick,
)
}

View file

@ -7,23 +7,29 @@
package io.element.android.features.preferences.impl.developer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.preferences.impl.R
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.components.preferences.PreferenceTextField
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.featureflag.ui.FeatureListView
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.ui.strings.CommonStrings
@ -57,13 +63,15 @@ fun DeveloperSettingsView(
selectedOption = state.tracingLogLevel.dataOrNull(),
options = LogLevelItem.entries.toPersistentList(),
onSelectOption = { logLevel ->
state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(logLevel))
state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(logLevel))
}
)
}
PreferenceCategory(title = "Showkase") {
PreferenceText(
title = "Open Showkase browser",
ListItem(
headlineContent = {
Text("Open Showkase browser")
},
onClick = onOpenShowkase
)
}
@ -71,17 +79,31 @@ fun DeveloperSettingsView(
state = state.rageshakeState,
)
PreferenceCategory(title = "Crash", showTopDivider = false) {
PreferenceText(
title = "Crash the app 💥",
ListItem(
headlineContent = {
Text("Crash the app 💥")
},
onClick = { error("This crash is a test.") }
)
}
val cache = state.cacheSize
PreferenceCategory(title = "Cache", showTopDivider = false) {
PreferenceText(
title = "Clear cache",
currentValue = cache.dataOrNull(),
loadingCurrentValue = state.cacheSize.isLoading() || state.clearCacheAction.isLoading(),
ListItem(
headlineContent = {
Text("Clear cache")
},
trailingContent = if (state.cacheSize.isLoading() || state.clearCacheAction.isLoading()) {
ListItemContent.Custom {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(20.dp),
strokeWidth = 2.dp
)
}
} else {
ListItemContent.Text(cache.dataOrNull().orEmpty())
},
onClick = {
if (state.clearCacheAction.isLoading().not()) {
state.eventSink(DeveloperSettingsEvents.ClearCache)

View file

@ -31,10 +31,10 @@ import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
@ -109,13 +109,19 @@ private fun NotificationSettingsContentView(
val context = LocalContext.current
val systemSettings: NotificationSettingsState.AppSettings = state.appSettings
if (systemSettings.appNotificationsEnabled && !systemSettings.systemNotificationsEnabled) {
PreferenceText(
icon = CompoundIcons.NotificationsOffSolid(),
title = stringResource(id = R.string.screen_notification_settings_system_notifications_turned_off),
subtitle = stringResource(
id = R.string.screen_notification_settings_system_notifications_action_required,
stringResource(id = R.string.screen_notification_settings_system_notifications_action_required_content_link)
),
ListItem(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.NotificationsOffSolid())),
headlineContent = {
Text(stringResource(id = R.string.screen_notification_settings_system_notifications_turned_off))
},
supportingContent = {
Text(
stringResource(
id = R.string.screen_notification_settings_system_notifications_action_required,
stringResource(id = R.string.screen_notification_settings_system_notifications_action_required_content_link)
)
)
},
onClick = {
context.startNotificationSettingsIntent()
}
@ -131,10 +137,14 @@ private fun NotificationSettingsContentView(
if (systemSettings.appNotificationsEnabled) {
if (!state.fullScreenIntentPermissionsState.permissionGranted) {
PreferenceCategory {
PreferenceText(
icon = CompoundIcons.VoiceCallSolid(),
title = stringResource(id = R.string.full_screen_intent_banner_title),
subtitle = stringResource(R.string.full_screen_intent_banner_message),
ListItem(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.VoiceCallSolid())),
headlineContent = {
Text(stringResource(id = R.string.full_screen_intent_banner_title))
},
supportingContent = {
Text(stringResource(R.string.full_screen_intent_banner_message))
},
onClick = {
state.fullScreenIntentPermissionsState.openFullScreenIntentSettings()
}
@ -142,15 +152,22 @@ private fun NotificationSettingsContentView(
}
}
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_notification_section_title)) {
PreferenceText(
title = stringResource(id = R.string.screen_notification_settings_group_chats),
subtitle = getTitleForRoomNotificationMode(mode = matrixSettings.defaultGroupNotificationMode),
ListItem(
headlineContent = {
Text(stringResource(id = R.string.screen_notification_settings_group_chats))
},
supportingContent = {
Text(getTitleForRoomNotificationMode(mode = matrixSettings.defaultGroupNotificationMode))
},
onClick = onGroupChatsClick
)
PreferenceText(
title = stringResource(id = R.string.screen_notification_settings_direct_chats),
subtitle = getTitleForRoomNotificationMode(mode = matrixSettings.defaultOneToOneNotificationMode),
ListItem(
headlineContent = {
Text(stringResource(id = R.string.screen_notification_settings_direct_chats))
},
supportingContent = {
Text(getTitleForRoomNotificationMode(mode = matrixSettings.defaultOneToOneNotificationMode))
},
onClick = onDirectChatsClick
)
}
@ -180,9 +197,10 @@ private fun NotificationSettingsContentView(
)
}
PreferenceCategory(title = stringResource(id = R.string.troubleshoot_notifications_entry_point_section)) {
PreferenceText(
modifier = Modifier,
title = stringResource(id = R.string.troubleshoot_notifications_entry_point_title),
ListItem(
headlineContent = {
Text(stringResource(id = R.string.troubleshoot_notifications_entry_point_title))
},
onClick = onTroubleshootNotificationsClick
)
}

View file

@ -5,13 +5,10 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoilApi::class)
package io.element.android.features.preferences.impl.tasks
import android.content.Context
import coil.Coil
import coil.annotation.ExperimentalCoilApi
import coil3.SingletonImageLoader
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.preferences.impl.DefaultCacheService
@ -43,7 +40,7 @@ class DefaultClearCacheUseCase @Inject constructor(
// Clear Matrix cache
matrixClient.clearCache()
// Clear Coil cache
Coil.imageLoader(context).let {
SingletonImageLoader.get(context).let {
it.diskCache?.clear()
it.memoryCache?.clear()
}

View file

@ -2,12 +2,38 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Velg hvordan du vil motta varsler"</string>
<string name="screen_advanced_settings_developer_mode">"Utviklermodus"</string>
<string name="screen_advanced_settings_developer_mode_description">"Aktiver for å få tilgang til funksjoner og funksjonalitet for utviklere."</string>
<string name="screen_advanced_settings_rich_text_editor_description">"Deaktiver rik tekstredigering for å skrive Markdown manuelt."</string>
<string name="screen_blocked_users_unblock_alert_action">"Fjern blokkering"</string>
<string name="screen_blocked_users_unblock_alert_description">"Du vil kunne se alle meldingene fra dem igjen."</string>
<string name="screen_blocked_users_unblock_alert_title">"Fjern blokkering av bruker"</string>
<string name="screen_edit_profile_display_name">"Visningsnavn"</string>
<string name="screen_edit_profile_display_name_placeholder">"Ditt visningsnavn"</string>
<string name="screen_edit_profile_error">"Det oppstod en ukjent feil, og informasjonen kunne ikke endres."</string>
<string name="screen_edit_profile_error_title">"Kan ikke oppdatere profilen"</string>
<string name="screen_edit_profile_title">"Rediger profil"</string>
<string name="screen_edit_profile_updating_details">"Oppdaterer profilen…"</string>
<string name="screen_notification_settings_additional_settings_section_title">"Ytterligere innstillinger"</string>
<string name="screen_notification_settings_calls_label">"Lyd- og videosamtaler"</string>
<string name="screen_notification_settings_configuration_mismatch">"Uoverensstemmelse i konfigurasjonen"</string>
<string name="screen_notification_settings_configuration_mismatch_description">"Vi har forenklet varslingsinnstillingene for å gjøre det lettere å finne alternativene. Noen av de egendefinerte innstillingene du har valgt tidligere, vises ikke her, men de er fortsatt aktive.
Hvis du fortsetter, kan noen av innstillingene dine endres."</string>
<string name="screen_notification_settings_direct_chats">"Direkte chatter"</string>
<string name="screen_notification_settings_edit_custom_settings_section_title">"Egendefinert innstilling per chat"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Det oppstod en feil under oppdatering av varslingsinnstillingen."</string>
<string name="screen_notification_settings_edit_mode_all_messages">"Alle meldinger"</string>
<string name="screen_notification_settings_edit_mode_mentions_and_keywords">"Bare omtaler og nøkkelord"</string>
<string name="screen_notification_settings_edit_screen_direct_section_header">"På direkte chatter, varsle meg for"</string>
<string name="screen_notification_settings_edit_screen_group_section_header">"I gruppechatter, varsle meg om"</string>
<string name="screen_notification_settings_enable_notifications">"Aktiver varsler på denne enheten"</string>
<string name="screen_notification_settings_failed_fixing_configuration">"Konfigurasjonen er ikke korrigert, prøv igjen."</string>
<string name="screen_notification_settings_group_chats">"Gruppechatter"</string>
<string name="screen_notification_settings_mode_all">"Alle"</string>
<string name="screen_notification_settings_notification_section_title">"Varsle meg om"</string>
<string name="screen_notification_settings_room_mention_label">"Gi meg varsel på @room"</string>
<string name="screen_notification_settings_system_notifications_action_required">"For å motta varsler, vennligst endre %1$s."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"systeminnstillinger"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"Systemvarsler er slått av"</string>
<string name="screen_notification_settings_title">"Varslinger"</string>
</resources>

View file

@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -40,9 +41,9 @@ class BlockedUsersPresenterTest {
@Test
fun `present - initial state with blocked users`() = runTest {
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
}
val matrixClient = FakeMatrixClient(
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -56,9 +57,10 @@ class BlockedUsersPresenterTest {
@Test
fun `present - blocked users list updates with new emissions`() = runTest {
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
}
val ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
val matrixClient = FakeMatrixClient(
ignoredUsersFlow = ignoredUsersFlow
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -66,7 +68,7 @@ class BlockedUsersPresenterTest {
with(awaitItem()) {
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID)))
}
matrixClient.ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
skipItems(1)
with(awaitItem()) {
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID), MatrixUser(A_USER_ID_2)))
@ -77,8 +79,9 @@ class BlockedUsersPresenterTest {
@Test
fun `present - blocked users list with data`() = runTest {
val alice = MatrixUser(A_USER_ID, displayName = "Alice", avatarUrl = "aliceAvatar")
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
val matrixClient = FakeMatrixClient(
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2))
).apply {
givenGetProfileResult(A_USER_ID, Result.success(alice))
givenGetProfileResult(A_USER_ID_2, Result.failure(AN_EXCEPTION))
}
@ -103,9 +106,9 @@ class BlockedUsersPresenterTest {
@Test
fun `present - unblock user`() = runTest {
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
}
val matrixClient = FakeMatrixClient(
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -125,10 +128,10 @@ class BlockedUsersPresenterTest {
@Test
fun `present - unblock user handles failure`() = runTest {
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned")))
}
val matrixClient = FakeMatrixClient(
unIgnoreUserResult = { Result.failure(IllegalStateException("User not banned")) },
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -147,10 +150,10 @@ class BlockedUsersPresenterTest {
@Test
fun `present - unblock user then cancel`() = runTest {
val matrixClient = FakeMatrixClient().apply {
ignoredUsersFlow.value = persistentListOf(A_USER_ID)
givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned")))
}
val matrixClient = FakeMatrixClient(
unIgnoreUserResult = { Result.failure(IllegalStateException("User not banned")) },
ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()

View file

@ -16,9 +16,10 @@ import io.element.android.features.rageshake.api.R
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceSlide
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
@ -52,7 +53,11 @@ fun RageshakePreferencesView(
onValueChange = ::onSensitivityChanged
)
} else {
PreferenceText(title = "Rageshaking is not supported by your device")
ListItem(
headlineContent = {
Text("Rageshaking is not supported by your device")
},
)
}
}
}

View file

@ -28,8 +28,8 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import io.element.android.features.rageshake.impl.R
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncActionView
@ -38,12 +38,13 @@ import io.element.android.libraries.designsystem.components.preferences.Preferen
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceRow
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
import io.element.android.libraries.ui.strings.CommonStrings
@ -96,8 +97,10 @@ fun BugReportView(
}
Spacer(modifier = Modifier.height(16.dp))
PreferenceDivider()
PreferenceText(
title = stringResource(id = R.string.screen_bug_report_view_logs),
ListItem(
headlineContent = {
Text(stringResource(id = R.string.screen_bug_report_view_logs))
},
enabled = isFormEnabled,
onClick = onViewLogs,
)

View file

@ -2,6 +2,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_bug_report_attach_screenshot">"Legg ved skjermbilde"</string>
<string name="screen_bug_report_contact_me">"Du kan kontakte meg hvis du har noen oppfølgingsspørsmål."</string>
<string name="screen_bug_report_contact_me_title">"Kontakt meg"</string>
<string name="screen_bug_report_edit_screenshot">"Rediger skjermbilde"</string>
<string name="screen_bug_report_editor_description">"Vennligst beskriv problemet. Hva har du gjort? Hva forventet du skulle skje? Hva som faktisk skjedde. Vær så detaljert som mulig."</string>
<string name="screen_bug_report_editor_placeholder">"Beskriv problemet…"</string>

View file

@ -54,7 +54,6 @@ import io.element.android.libraries.designsystem.components.button.MainActionBut
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
@ -481,10 +480,14 @@ private fun TopicSection(
showTopDivider = false,
) {
if (roomTopic is RoomTopicState.CanAddTopic) {
PreferenceText(
title = stringResource(R.string.screen_room_details_add_topic_title),
icon = CompoundIcons.Plus(),
onClick = { onActionClick(RoomDetailsAction.AddTopic) },
ListItem(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Plus())),
headlineContent = {
Text(stringResource(id = R.string.screen_room_details_add_topic_title))
},
onClick = {
onActionClick(RoomDetailsAction.AddTopic)
},
)
} else if (roomTopic is RoomTopicState.ExistingTopic) {
ClickableLinkText(

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Det oppstod en feil under oppdatering av varslingsinnstillingen."</string>
<string name="screen_room_change_permissions_everyone">"Alle"</string>
<string name="screen_room_change_role_section_administrators">"Administratorer"</string>
<string name="screen_room_change_role_section_users">"Medlemmer"</string>
<string name="screen_room_change_role_unsaved_changes_description">"Du har endringer som ikke er lagret."</string>
@ -15,11 +17,15 @@
<string name="screen_room_details_edition_error_title">"Kan ikke oppdatere rommet"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Meldingene er krypterte. Det er bare du og mottakerne som har de unike nøklene til å låse dem opp."</string>
<string name="screen_room_details_encryption_enabled_title">"Meldingskryptering aktivert"</string>
<string name="screen_room_details_error_loading_notification_settings">"Det oppstod en feil ved lasting av varslingsinnstillinger."</string>
<string name="screen_room_details_error_muting">"Mislyktes i å dempe dette rommet, prøv igjen."</string>
<string name="screen_room_details_error_unmuting">"Mislyktes i å oppheve dempingen av dette rommet, prøv igjen."</string>
<string name="screen_room_details_invite_people_title">"Inviter folk"</string>
<string name="screen_room_details_leave_conversation_title">"Forlat samtalen"</string>
<string name="screen_room_details_leave_room_title">"Forlat rommet"</string>
<string name="screen_room_details_notification_mode_custom">"Tilpasset"</string>
<string name="screen_room_details_notification_mode_default">"Standard"</string>
<string name="screen_room_details_notification_title">"Varslinger"</string>
<string name="screen_room_details_room_name_label">"Romnavn"</string>
<string name="screen_room_details_security_title">"Sikkerhet"</string>
<string name="screen_room_details_share_room_title">"Del rom"</string>
@ -59,6 +65,11 @@
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"globale innstillinger"</string>
<string name="screen_room_notification_settings_default_setting_title">"Standard innstilling"</string>
<string name="screen_room_notification_settings_edit_remove_setting">"Fjern egendefinert innstilling"</string>
<string name="screen_room_notification_settings_error_loading_settings">"Det oppstod en feil ved innlasting av varslingsinnstillinger."</string>
<string name="screen_room_notification_settings_error_restoring_default">"Gjenoppretting av standardmodus mislyktes, prøv igjen."</string>
<string name="screen_room_notification_settings_error_setting_mode">"Innstilling av modus mislyktes, prøv igjen."</string>
<string name="screen_room_notification_settings_mode_all_messages">"Alle meldinger"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Bare omtaler og nøkkelord"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"I dette rommet, varsle meg om"</string>
<string name="screen_room_roles_and_permissions_admins">"Administratorer"</string>
<string name="screen_room_roles_and_permissions_change_my_role">"Endre rollen min"</string>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_edit_room_address_room_address_section_footer">"Du behöver en rumsadress för att göra den synlig i katalogen."</string>
<string name="screen_edit_room_address_title">"Rumsadress"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Ett fel uppstod vid uppdatering av aviseringsinställningen."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Din hemserver stöder inte det här alternativet i krypterade rum, du kanske inte aviseras i vissa rum."</string>
<string name="screen_polls_history_title">"Omröstningar"</string>
@ -132,10 +134,18 @@ Vi rekommenderar inte att aktivera kryptering för rum som vem som helst kan hit
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Användare kan bara gå med om de är inbjudna"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Endast inbjudan"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Tillgång till rum"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_description">"Utrymmen stöds för närvarande inte"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Utrymmesmedlemmar"</string>
<string name="screen_security_and_privacy_room_address_section_header">"Rumsadress"</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Tillåt att detta rum hittas genom att söka i den offentliga rumskatalogen på %1$s"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Vem som helst"</string>
<string name="screen_security_and_privacy_room_history_section_header">"Vem kan läsa historik"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Endast medlemmar sedan de bjöds in"</string>
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Endast medlemmar sedan det här alternativet har valts"</string>
<string name="screen_security_and_privacy_room_publishing_section_footer">"Rumsadresser är sätt att hitta och komma åt rum. Detta säkerställer också att du enkelt kan dela ditt rum med andra.
Du kan välja att publicera ditt rum i din hemservers offentliga rumskatalog."</string>
<string name="screen_security_and_privacy_room_visibility_section_footer">"Rumsadresser är sätt att hitta och komma åt rum. Detta säkerställer också att du enkelt kan dela ditt rum med andra.
Adressen krävs också för att rummet ska synas i den allmänna rumskatalogen på %1$s."</string>
<string name="screen_security_and_privacy_room_visibility_section_header">"Rumssynlighet"</string>
<string name="screen_security_and_privacy_title">"Säkerhet och sekretess"</string>
</resources>

View file

@ -28,7 +28,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
fun onSessionConfirmRecoveryKeyClick()
fun onRoomSettingsClick(roomId: RoomId)
fun onReportBugClick()
fun onRoomDirectorySearchClick()
fun onLogoutForNativeSlidingSyncMigrationNeeded()
}
}

View file

@ -82,10 +82,6 @@ class RoomListNode @AssistedInject constructor(
}
}
private fun onRoomDirectorySearchClick() {
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomDirectorySearchClick() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -100,7 +96,6 @@ class RoomListNode @AssistedInject constructor(
onConfirmRecoveryKeyClick = this::onSessionConfirmRecoveryKeyClick,
onRoomSettingsClick = this::onRoomSettingsClick,
onMenuActionClick = { onMenuActionClick(activity, it) },
onRoomDirectorySearchClick = this::onRoomDirectorySearchClick,
modifier = modifier,
) {
acceptDeclineInviteView.Render(

View file

@ -51,6 +51,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.isOnline
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.services.analytics.api.AnalyticsService
@ -89,6 +90,7 @@ class RoomListPresenter @Inject constructor(
private val fullScreenIntentPermissionsPresenter: Presenter<FullScreenIntentPermissionsState>,
private val notificationCleaner: NotificationCleaner,
private val logoutPresenter: Presenter<DirectLogoutState>,
private val appPreferencesStore: AppPreferencesStore,
) : Presenter<RoomListState> {
private val encryptionService: EncryptionService = client.encryptionService()
@ -245,7 +247,8 @@ class RoomListPresenter @Inject constructor(
isFavorite = event.roomListRoomSummary.isFavorite,
markAsUnreadFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.MarkAsUnread),
hasNewContent = event.roomListRoomSummary.hasNewContent,
eventCacheFeatureFlagEnabled = featureFlagService.isFeatureEnabled(FeatureFlags.EventCache),
eventCacheFeatureFlagEnabled = appPreferencesStore.isDeveloperModeEnabledFlow().first() &&
featureFlagService.isFeatureEnabled(FeatureFlags.EventCache),
)
contextMenuState.value = initialState
@ -329,9 +332,12 @@ class RoomListPresenter @Inject constructor(
}
@VisibleForTesting
internal fun RoomListRoomSummary.toInviteData() = InviteData(
roomId = roomId,
// Note: `name` should not be null at this point, but just in case, fallback to the roomId
roomName = name ?: roomId.value,
isDm = isDm,
)
internal fun RoomListRoomSummary.toInviteData(): InviteData? {
if (inviteSender == null) return null
return InviteData(
roomId = roomId,
roomName = name ?: roomId.value,
isDm = isDm,
senderId = inviteSender.userId,
)
}

View file

@ -49,7 +49,6 @@ fun RoomListView(
onCreateRoomClick: () -> Unit,
onRoomSettingsClick: (roomId: RoomId) -> Unit,
onMenuActionClick: (RoomListMenuAction) -> Unit,
onRoomDirectorySearchClick: () -> Unit,
modifier: Modifier = Modifier,
acceptDeclineInviteView: @Composable () -> Unit,
) {
@ -83,7 +82,6 @@ fun RoomListView(
state = state.searchState,
eventSink = state.eventSink,
onRoomClick = onRoomClick,
onRoomDirectorySearchClick = onRoomDirectorySearchClick,
modifier = Modifier
.statusBarsPadding()
.padding(top = topPadding)
@ -177,7 +175,6 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
onCreateRoomClick = {},
onRoomSettingsClick = {},
onMenuActionClick = {},
onRoomDirectorySearchClick = {},
acceptDeclineInviteView = {},
)
}

View file

@ -15,14 +15,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import kotlinx.collections.immutable.persistentListOf
import javax.inject.Inject
class RoomListSearchPresenter @Inject constructor(
private val dataSource: RoomListSearchDataSource,
private val featureFlagService: FeatureFlagService,
) : Presenter<RoomListSearchState> {
@Composable
override fun present(): RoomListSearchState {
@ -57,14 +54,12 @@ class RoomListSearchPresenter @Inject constructor(
}
}
val isRoomDirectorySearchEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch).collectAsState(initial = false)
val searchResults by dataSource.roomSummaries.collectAsState(initial = persistentListOf())
return RoomListSearchState(
isSearchActive = isSearchActive,
query = searchQuery,
results = searchResults,
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
eventSink = ::handleEvents
)
}

View file

@ -14,8 +14,5 @@ data class RoomListSearchState(
val isSearchActive: Boolean,
val query: String,
val results: ImmutableList<RoomListRoomSummary>,
val isRoomDirectorySearchEnabled: Boolean,
val eventSink: (RoomListSearchEvents) -> Unit
) {
val displayRoomDirectorySearch = query.isEmpty() && isRoomDirectorySearchEnabled
}
)

View file

@ -17,7 +17,6 @@ class RoomListSearchStateProvider : PreviewParameterProvider<RoomListSearchState
override val values: Sequence<RoomListSearchState>
get() = sequenceOf(
aRoomListSearchState(),
aRoomListSearchState(isRoomDirectorySearchEnabled = true),
aRoomListSearchState(
isSearchActive = true,
query = "Test",
@ -30,12 +29,10 @@ fun aRoomListSearchState(
isSearchActive: Boolean = false,
query: String = "",
results: ImmutableList<RoomListRoomSummary> = persistentListOf(),
isRoomDirectorySearchEnabled: Boolean = false,
eventSink: (RoomListSearchEvents) -> Unit = { },
) = RoomListSearchState(
isSearchActive = isSearchActive,
query = query,
results = results,
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
eventSink = eventSink,
)

View file

@ -12,12 +12,9 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
@ -26,7 +23,6 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
@ -38,22 +34,18 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomlist.impl.R
import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
import io.element.android.features.roomlist.impl.contentType
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.SuperButton
import io.element.android.libraries.designsystem.modifiers.applyIf
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.FilledTextField
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.copy
import io.element.android.libraries.matrix.api.core.RoomId
@ -64,7 +56,6 @@ internal fun RoomListSearchView(
state: RoomListSearchState,
eventSink: (RoomListEvents) -> Unit,
onRoomClick: (RoomId) -> Unit,
onRoomDirectorySearchClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(enabled = state.isSearchActive) {
@ -91,7 +82,6 @@ internal fun RoomListSearchView(
state = state,
onRoomClick = onRoomClick,
eventSink = eventSink,
onRoomDirectorySearchClick = onRoomDirectorySearchClick,
)
}
}
@ -104,7 +94,6 @@ private fun RoomListSearchContent(
state: RoomListSearchState,
eventSink: (RoomListEvents) -> Unit,
onRoomClick: (RoomId) -> Unit,
onRoomDirectorySearchClick: () -> Unit,
) {
val borderColor = MaterialTheme.colorScheme.tertiary
val strokeWidth = 1.dp
@ -175,14 +164,6 @@ private fun RoomListSearchContent(
.padding(padding)
.consumeWindowInsets(padding)
) {
if (state.displayRoomDirectorySearch) {
RoomDirectorySearchButton(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp, horizontal = 16.dp),
onClick = onRoomDirectorySearchClick
)
}
LazyColumn(
modifier = Modifier.weight(1f),
) {
@ -201,31 +182,6 @@ private fun RoomListSearchContent(
}
}
@Composable
private fun RoomDirectorySearchButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
SuperButton(
onClick = onClick,
modifier = modifier,
buttonSize = ButtonSize.Large,
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = CompoundIcons.ListBulleted(),
contentDescription = null,
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.screen_roomlist_room_directory_button_title),
)
}
}
}
@PreviewsDayNight
@Composable
internal fun RoomListSearchContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview {
@ -233,6 +189,5 @@ internal fun RoomListSearchContentPreview(@PreviewParameter(RoomListSearchStateP
state = state,
onRoomClick = {},
eventSink = {},
onRoomDirectorySearchClick = {},
)
}

View file

@ -8,6 +8,8 @@
<string name="screen_invites_decline_direct_chat_title">"Avslå chat"</string>
<string name="screen_invites_empty_list">"Ingen invitasjoner"</string>
<string name="screen_invites_invited_you">"%1$s(%2$s) inviterte deg"</string>
<string name="screen_migration_message">"Dette er en engangsprosess, takk for at du venter."</string>
<string name="screen_migration_title">"Setter opp kontoen din."</string>
<string name="screen_roomlist_a11y_create_message">"Opprett en ny samtale eller et nytt rom"</string>
<string name="screen_roomlist_empty_message">"Kom i gang med å sende meldinger til noen."</string>
<string name="screen_roomlist_empty_title">"Ingen chatter ennå."</string>

View file

@ -67,7 +67,9 @@ import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
@ -312,6 +314,35 @@ class RoomListPresenterTest {
}
}
@Test
fun `present - show context menu with view source on`() = runTest {
val presenter = createRoomListPresenter(
appPreferencesStore = InMemoryAppPreferencesStore(
isDeveloperModeEnabled = true,
)
)
presenter.test {
val initialState = awaitItem()
val summary = createRoomListRoomSummary()
initialState.eventSink(RoomListEvents.ShowContextMenu(summary))
awaitItem().also { state ->
assertThat(state.contextMenu)
.isEqualTo(
RoomListState.ContextMenu.Shown(
roomId = summary.roomId,
roomName = summary.name,
isDm = false,
isFavorite = false,
markAsUnreadFeatureFlagEnabled = true,
// true here.
eventCacheFeatureFlagEnabled = true,
hasNewContent = false,
)
)
}
}
}
@Test
fun `present - hide context menu`() = runTest {
val room = FakeMatrixRoom()
@ -643,6 +674,7 @@ class RoomListPresenterTest {
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
) = RoomListPresenter(
client = client,
syncService = syncService,
@ -672,6 +704,7 @@ class RoomListPresenterTest {
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
notificationCleaner = notificationCleaner,
logoutPresenter = { aDirectLogoutState() },
appPreferencesStore = appPreferencesStore,
)
}

View file

@ -240,7 +240,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onCreateRoomClick: () -> Unit = EnsureNeverCalled(),
onRoomSettingsClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onMenuActionClick: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
RoomListView(
@ -252,7 +251,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onCreateRoomClick = onCreateRoomClick,
onRoomSettingsClick = onRoomSettingsClick,
onMenuActionClick = onMenuActionClick,
onRoomDirectorySearchClick = onRoomDirectorySearchClick,
acceptDeclineInviteView = { },
)
}

View file

@ -14,9 +14,6 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
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.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.room.aRoomSummary
@ -118,26 +115,10 @@ class RoomListSearchPresenterTest {
}
}
}
@Test
fun `present - room directory search`() = runTest {
val featureFlagService = FakeFeatureFlagService()
featureFlagService.setFeatureEnabled(FeatureFlags.RoomDirectorySearch, true)
val presenter = createRoomListSearchPresenter(featureFlagService = featureFlagService)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
awaitItem().let { state ->
assertThat(state.isRoomDirectorySearchEnabled).isTrue()
}
}
}
}
fun TestScope.createRoomListSearchPresenter(
roomListService: RoomListService = FakeRoomListService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
): RoomListSearchPresenter {
return RoomListSearchPresenter(
dataSource = RoomListSearchDataSource(
@ -148,6 +129,5 @@ fun TestScope.createRoomListSearchPresenter(
),
coroutineDispatchers = testCoroutineDispatchers(),
),
featureFlagService = featureFlagService,
)
}

View file

@ -1,62 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomlist.impl.search
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomlist.impl.R
import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RoomListSearchViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on 'Browse all rooms' invokes the expected callback`() {
val eventsRecorder = EventsRecorder<RoomListSearchEvents>(expectEvents = false)
ensureCalledOnce {
rule.setRoomListSearchView(
aRoomListSearchState(
isSearchActive = true,
isRoomDirectorySearchEnabled = true,
eventSink = eventsRecorder,
),
onRoomDirectorySearchClick = it,
)
rule.clickOn(R.string.screen_roomlist_room_directory_button_title)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomListSearchView(
state: RoomListSearchState,
eventSink: (RoomListEvents) -> Unit = EventsRecorder(expectEvents = false),
onRoomClick: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onRoomDirectorySearchClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
RoomListSearchView(
state = state,
eventSink = eventSink,
onRoomClick = onRoomClick,
onRoomDirectorySearchClick = onRoomDirectorySearchClick,
)
}
}

View file

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_chat_backup_key_backup_action_enable">"Slå på sikkerhetskopiering"</string>
<string name="screen_chat_backup_key_backup_description">"Lagre din kryptografiske identitet og meldingsnøkler sikkert på serveren. Dette gjør at du kan se meldingshistorikken din på alle nye enheter. %1$s."</string>
<string name="screen_chat_backup_recovery_action_change">"Endre gjenopprettingsnøkkel"</string>
<string name="screen_chat_backup_recovery_action_confirm">"Skriv inn gjenopprettingsnøkkel"</string>
<string name="screen_chat_backup_recovery_action_setup_description">"Få tilgang til de krypterte meldingene dine hvis du mister alle enhetene dine eller blir logget ut av %1$s overalt."</string>
<string name="screen_encryption_reset_action_continue_reset">"Fortsett tilbakestillingen"</string>
<string name="screen_encryption_reset_bullet_1">"Dine kontodetaljer, kontakter, innstillinger og chatteliste vil bli beholdt"</string>
<string name="screen_encryption_reset_bullet_2">"Du mister all meldingshistorikk som bare er lagret på serveren"</string>
@ -8,5 +12,6 @@
<string name="screen_encryption_reset_footer">"Tilbakestill identiteten din bare hvis du ikke har tilgang til en annen pålogget enhet og du har mistet gjenopprettingsnøkkelen."</string>
<string name="screen_encryption_reset_title">"Kan du ikke bekrefte? Du må tilbakestille identiteten din."</string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Slå av"</string>
<string name="screen_key_backup_disable_description_point_1">"Du vil ikke ha kryptert meldingshistorikk på nye enheter"</string>
<string name="screen_recovery_key_confirm_title">"Skriv inn gjenopprettingsnøkkelen din"</string>
</resources>

View file

@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_signed_out_reason_1">"Du har endret passordet ditt i en annen sesjon"</string>
<string name="screen_signed_out_reason_2">"Du har slettet sesjonen fra en annen sesjon"</string>
<string name="screen_signed_out_reason_3">"Serveradministratoren har ugyldiggjort tilgangen din"</string>
<string name="screen_signed_out_subtitle">"Du kan ha blitt logget ut av en av årsakene som er oppført nedenfor. Logg på igjen for å fortsette å bruke %s."</string>
<string name="screen_signed_out_title">"Du er logget ut"</string>
</resources>

View file

@ -40,7 +40,11 @@ import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ -169,7 +173,8 @@ class UserProfilePresenterTest {
@Test
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
val client = createFakeMatrixClient()
val ignoredUsersFlow = MutableStateFlow(persistentListOf<UserId>())
val client = createFakeMatrixClient(ignoredUsersFlow = ignoredUsersFlow)
val presenter = createUserProfilePresenter(
client = client,
userId = A_USER_ID
@ -178,20 +183,21 @@ class UserProfilePresenterTest {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf(A_USER_ID))
ignoredUsersFlow.emit(persistentListOf(A_USER_ID))
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf())
ignoredUsersFlow.emit(persistentListOf())
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
}
}
@Test
fun `present - BlockUser with error`() = runTest {
val matrixClient = createFakeMatrixClient()
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE))
val matrixClient = createFakeMatrixClient(
ignoreUserResult = { Result.failure(A_THROWABLE) }
)
val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test {
val initialState = awaitFirstItem()
@ -207,8 +213,9 @@ class UserProfilePresenterTest {
@Test
fun `present - UnblockUser with error`() = runTest {
val matrixClient = createFakeMatrixClient()
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE))
val matrixClient = createFakeMatrixClient(
unIgnoreUserResult = { Result.failure(A_THROWABLE) }
)
val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test {
val initialState = awaitFirstItem()
@ -374,10 +381,16 @@ class UserProfilePresenterTest {
private fun createFakeMatrixClient(
isUserVerified: Boolean = false,
ignoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf())
) = FakeMatrixClient(
encryptionService = FakeEncryptionService(
isUserVerifiedResult = { Result.success(isUserVerified) }
),
ignoreUserResult = ignoreUserResult,
unIgnoreUserResult = unIgnoreUserResult,
ignoredUsersFlow = ignoredUsersFlow
)
private fun createUserProfilePresenter(

View file

@ -6,7 +6,7 @@
android_gradle_plugin = "8.8.1"
kotlin = "2.1.10"
kotlinpoet = "2.1.0"
ksp = "2.1.10-1.0.30"
ksp = "2.1.10-1.0.31"
firebaseAppDistribution = "5.1.1"
# AndroidX
@ -42,7 +42,7 @@ datetime = "0.6.2"
serialization_json = "1.8.0"
#other
coil = "2.7.0"
coil = "3.1.0"
showkase = "1.0.3"
appyx = "1.6.0"
sqldelight = "2.0.2"
@ -77,7 +77,7 @@ kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", ve
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
gms_google_services = "com.google.gms:google-services:4.4.2"
# https://firebase.google.com/docs/android/setup#available-libraries
google_firebase_bom = "com.google.firebase:firebase-bom:33.9.0"
google_firebase_bom = "com.google.firebase:firebase-bom:33.10.0"
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
@ -149,7 +149,7 @@ test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" }
test_arch_core = "androidx.arch.core:core-testing:2.2.0"
test_junit = "junit:junit:4.13.2"
test_runner = "androidx.test:runner:1.6.2"
test_mockk = "io.mockk:mockk:1.13.16"
test_mockk = "io.mockk:mockk:1.13.17"
test_konsist = "com.lemonappdev:konsist:0.17.3"
test_turbine = "app.cash.turbine:turbine:1.2.0"
test_truth = "com.google.truth:truth:1.4.4"
@ -159,21 +159,22 @@ test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "
test_composable_preview_scanner = "io.github.sergio-sastre.ComposablePreviewScanner:android:0.5.1"
# Others
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
coil_test = { module = "io.coil-kt:coil-test", version.ref = "coil" }
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
coil_network_okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
coil_compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil_gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
coil_test = { module = "io.coil-kt.coil3:coil-test", version.ref = "coil" }
compound = { module = "io.element.android:compound-android", version = "25.2.26" }
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" }
kotlinx_collections_immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8"
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
showkase_processor = { module = "com.airbnb.android:showkase-processor", version.ref = "showkase" }
jsoup = "org.jsoup:jsoup:1.18.3"
jsoup = "org.jsoup:jsoup:1.19.1"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.2.26"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.3.6"
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 = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
@ -187,7 +188,7 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.2"
maplibre = "org.maplibre.gl:android-sdk:11.8.1"
maplibre = "org.maplibre.gl:android-sdk:11.8.2"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.1.0"
@ -233,7 +234,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" }
detekt = "io.gitlab.arturbosch.detekt:1.23.8"
ktlint = "org.jlleitschuh.gradle.ktlint:12.1.2"
ktlint = "org.jlleitschuh.gradle.ktlint:12.2.0"
dependencygraph = "com.savvasdalkitsis.module-dependency-graph:0.12"
dependencycheck = "org.owasp.dependencycheck:12.1.0"
dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysis" }

View file

@ -28,3 +28,21 @@ fun Boolean.toSecondaryEnabledColor(): Color {
ElementTheme.colors.textDisabled
}
}
@Composable
fun Boolean.toIconEnabledColor(): Color {
return if (this) {
ElementTheme.colors.iconPrimary
} else {
ElementTheme.colors.iconDisabled
}
}
@Composable
fun Boolean.toIconSecondaryEnabledColor(): Color {
return if (this) {
ElementTheme.colors.iconSecondary
} else {
ElementTheme.colors.iconDisabled
}
}

View file

@ -9,7 +9,6 @@ package io.element.android.libraries.designsystem.components
import android.graphics.Bitmap
import android.graphics.Typeface
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.text.TextPaint
import androidx.annotation.FloatRange
@ -85,9 +84,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import coil.imageLoader
import coil.request.DefaultRequestOptions
import coil.request.ImageRequest
import coil3.SingletonImageLoader
import coil3.request.ImageRequest
import coil3.request.allowHardware
import coil3.toBitmap
import com.airbnb.android.showkase.annotation.ShowkaseComposable
import com.vanniktech.blurhash.BlurHash
import io.element.android.compound.theme.ElementTheme
@ -328,7 +328,7 @@ fun Modifier.avatarBloom(
ImageRequest.Builder(context)
.data(avatarData)
// Allow cache and default dispatchers
.defaults(DefaultRequestOptions())
.defaults(ImageRequest.Defaults())
// Needed to be able to read pixels from the Bitmap for the hash
.allowHardware(false)
// Reduce size so it loads faster for large avatars
@ -340,9 +340,11 @@ fun Modifier.avatarBloom(
var blurHash by rememberSaveable(avatarData) { mutableStateOf<String?>(null) }
LaunchedEffect(avatarData) {
withContext(Dispatchers.IO) {
val drawable =
context.imageLoader.execute(painterRequest).drawable ?: return@withContext
val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@withContext
val bitmap = SingletonImageLoader.get(context)
.execute(painterRequest)
.image
?.toBitmap()
?: return@withContext
blurHash = BlurHash.encode(
bitmap = bitmap,
componentX = BloomDefaults.HASH_COMPONENTS,

View file

@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -26,10 +28,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent
import coil3.compose.AsyncImage
import coil3.compose.AsyncImagePainter
import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
@ -89,7 +91,8 @@ private fun ImageAvatar(
contentScale = ContentScale.Crop,
modifier = modifier
) {
when (val state = painter.state) {
val collectedState by painter.state.collectAsState()
when (val state = collectedState) {
is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent()
is AsyncImagePainter.State.Error -> {
SideEffect {

View file

@ -20,7 +20,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import coil.compose.AsyncImage
import coil3.compose.AsyncImage
@Composable
fun BlurHashAsyncImage(

View file

@ -47,10 +47,6 @@ internal fun PreferenceCategoryPreview() = ElementThemedPreview {
PreferenceCategory(
title = "Category title",
) {
PreferenceText(
title = "Title",
icon = CompoundIcons.ChatProblem(),
)
PreferenceSwitch(
title = "Switch",
icon = CompoundIcons.Threads(),

View file

@ -97,11 +97,6 @@ internal fun PreferencePagePreview() = ElementPreview {
PreferenceCategory(
title = "Category title",
) {
PreferenceText(
title = "Title",
subtitle = "Some other text",
icon = CompoundIcons.ChatProblem(),
)
PreferenceDivider()
PreferenceSwitch(
title = "Switch",

View file

@ -1,216 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
@Composable
fun PreferenceText(
title: String,
modifier: Modifier = Modifier,
enabled: Boolean = true,
subtitle: String? = null,
subtitleAnnotated: AnnotatedString? = null,
currentValue: String? = null,
loadingCurrentValue: Boolean = false,
icon: ImageVector? = null,
@DrawableRes iconResourceId: Int? = null,
showIconAreaIfNoIcon: Boolean = false,
showIconBadge: Boolean = false,
showEndBadge: Boolean = false,
tintColor: Color? = null,
onClick: () -> Unit = {},
) {
ListItem(
modifier = modifier,
enabled = enabled,
onClick = onClick,
leadingContent = preferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
showIconBadge = showIconBadge,
enabled = enabled,
showIconAreaIfNoIcon = showIconAreaIfNoIcon,
tintColor = tintColor,
),
headlineContent = {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
text = title,
color = tintColor ?: enabled.toEnabledColor(),
)
},
supportingContent = if (subtitle != null) {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle,
color = tintColor ?: enabled.toSecondaryEnabledColor(),
)
}
} else {
subtitleAnnotated?.let {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = it,
color = tintColor ?: enabled.toSecondaryEnabledColor(),
)
}
}
},
trailingContent = if (currentValue != null || loadingCurrentValue || showEndBadge) {
ListItemContent.Custom {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
if (currentValue != null) {
Text(
text = currentValue,
style = ElementTheme.typography.fontBodyXsMedium,
color = enabled.toSecondaryEnabledColor(),
)
} else if (loadingCurrentValue) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(20.dp),
strokeWidth = 2.dp
)
}
if (showEndBadge) {
val endBadgeStartPadding = if (currentValue != null || loadingCurrentValue) 16.dp else 0.dp
RedIndicatorAtom(
modifier = Modifier
.padding(start = endBadgeStartPadding)
)
}
}
}
} else {
null
}
)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceTextLightPreview() = ElementPreviewLight {
ContentToPreview(showEndBadge = false)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceTextDarkPreview() = ElementPreviewDark {
ContentToPreview(showEndBadge = false)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceTextWithEndBadgeLightPreview() = ElementPreviewLight {
ContentToPreview(showEndBadge = true)
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceTextWithEndBadgeDarkPreview() = ElementPreviewDark {
ContentToPreview(showEndBadge = true)
}
@ExcludeFromCoverage
@Composable
private fun ContentToPreview(showEndBadge: Boolean) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
PreferenceText(
title = "Title",
icon = CompoundIcons.ChatProblem(),
showEndBadge = showEndBadge,
)
PreferenceText(
title = "Title",
subtitle = "Some content",
icon = CompoundIcons.ChatProblem(),
showEndBadge = showEndBadge,
)
PreferenceText(
title = "Title",
subtitle = "Some content",
icon = CompoundIcons.ChatProblem(),
currentValue = "123",
showEndBadge = showEndBadge,
)
PreferenceText(
title = "Title",
subtitle = "Some content",
icon = CompoundIcons.ChatProblem(),
currentValue = "123",
enabled = false,
showEndBadge = showEndBadge,
)
PreferenceText(
title = "Title",
subtitle = "Some content",
icon = CompoundIcons.ChatProblem(),
loadingCurrentValue = true,
showEndBadge = showEndBadge,
)
PreferenceText(
title = "Title",
icon = CompoundIcons.ChatProblem(),
currentValue = "123",
showEndBadge = showEndBadge,
)
PreferenceText(
title = "Title",
icon = CompoundIcons.ChatProblem(),
loadingCurrentValue = true,
showEndBadge = showEndBadge,
)
PreferenceText(
title = "Title no icon with icon area",
showIconAreaIfNoIcon = true,
loadingCurrentValue = true,
showEndBadge = showEndBadge,
)
PreferenceText(
title = "Title no icon",
loadingCurrentValue = true,
showEndBadge = showEndBadge,
)
}
}

View file

@ -25,7 +25,7 @@ import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
import io.element.android.libraries.designsystem.toIconSecondaryEnabledColor
@Composable
fun preferenceIcon(
@ -68,7 +68,7 @@ private fun PreferenceIcon(
imageVector = icon,
resourceId = iconResourceId,
contentDescription = null,
tint = tintColor ?: enabled.toSecondaryEnabledColor(),
tint = tintColor ?: enabled.toIconSecondaryEnabledColor(),
modifier = Modifier
.size(24.dp),
)

View file

@ -9,9 +9,14 @@ package io.element.android.libraries.designsystem.preview
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.AsyncImagePreviewHandler
import coil3.compose.LocalAsyncImagePreviewHandler
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Surface
@OptIn(ExperimentalCoilApi::class)
@Composable
@Suppress("ModifierMissing")
fun ElementPreview(
@ -19,12 +24,14 @@ fun ElementPreview(
showBackground: Boolean = true,
content: @Composable () -> Unit
) {
ElementTheme(darkTheme = darkTheme) {
if (showBackground) {
// If we have a proper contentColor applied we need a Surface instead of a Box
Surface(content = content)
} else {
content()
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides AsyncImagePreviewHandler { null }) {
ElementTheme(darkTheme = darkTheme) {
if (showBackground) {
// If we have a proper contentColor applied we need a Surface instead of a Box
Surface(content = content)
} else {
content()
}
}
}
}

View file

@ -193,14 +193,14 @@ sealed interface ListItemStyle {
@Composable
fun leadingIconColor() = when (this) {
Default -> ListItemDefaultColors.icon
Default -> ListItemDefaultColors.leadingIcon
Primary -> ElementTheme.colors.iconPrimary
Destructive -> ElementTheme.colors.iconCriticalPrimary
}
@Composable
fun trailingIconColor() = when (this) {
Default -> ListItemDefaultColors.icon
Default -> ListItemDefaultColors.trailingIcon
Primary -> ElementTheme.colors.iconPrimary
Destructive -> ElementTheme.colors.iconCriticalPrimary
}
@ -210,15 +210,16 @@ object ListItemDefaultColors {
val headline: Color @Composable get() = ElementTheme.colors.textPrimary
val headlineDisabled: Color @Composable get() = ElementTheme.colors.textDisabled
val supportingText: Color @Composable get() = ElementTheme.materialColors.onSurfaceVariant
val icon: Color @Composable get() = ElementTheme.colors.iconTertiary
val leadingIcon: Color @Composable get() = ElementTheme.colors.iconSecondary
val trailingIcon: Color @Composable get() = ElementTheme.colors.iconPrimary
val iconDisabled: Color @Composable get() = ElementTheme.colors.iconDisabled
val colors: ListItemColors
@Composable get() = ListItemDefaults.colors(
headlineColor = headline,
supportingColor = supportingText,
leadingIconColor = icon,
trailingIconColor = icon,
leadingIconColor = leadingIcon,
trailingIconColor = trailingIcon,
disabledHeadlineColor = headlineDisabled,
disabledLeadingIconColor = iconDisabled,
disabledTrailingIconColor = iconDisabled,

View file

@ -165,7 +165,7 @@ enum class FeatureFlags(
key = "feature.event_cache",
title = "Use SDK Event cache",
description = "Warning: you must kill and restart the app for the change to take effect.",
defaultValue = { false },
defaultValue = { true },
isFinished = false,
),
}

Some files were not shown because too many files have changed in this diff Show more