Merge branch 'release/25.03.1' into main
This commit is contained in:
commit
232f1db645
675 changed files with 2857 additions and 2499 deletions
17
.github/workflows/blocked.yml
vendored
Normal file
17
.github/workflows/blocked.yml
vendored
Normal 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.");
|
||||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
77
CHANGES.md
77
CHANGES.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
2
fastlane/metadata/android/en-US/changelogs/202503010.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/202503010.txt
Normal 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
|
||||
|
|
@ -33,6 +33,9 @@ data class WidgetMessage(
|
|||
@SerialName("im.vector.hangup")
|
||||
HangUp,
|
||||
|
||||
@SerialName("io.element.close")
|
||||
Close,
|
||||
|
||||
@SerialName("send_event")
|
||||
SendEvent,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -22,5 +22,6 @@ interface CreateRoomEntryPoint : FeatureEntryPoint {
|
|||
|
||||
interface Callback : Plugin {
|
||||
fun onOpenRoom(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>)
|
||||
fun onOpenRoomDirectory()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,5 +15,6 @@ data class CreateRoomRootState(
|
|||
val applicationName: String,
|
||||
val userListState: UserListState,
|
||||
val startDmAction: AsyncAction<RoomId>,
|
||||
val isRoomDirectorySearchEnabled: Boolean,
|
||||
val eventSink: (CreateRoomRootEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 s’est 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 n’est 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
features/ftue/impl/src/main/res/values-nb/translations.xml
Normal file
11
features/ftue/impl/src/main/res/values-nb/translations.xml
Normal 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>
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ import io.element.android.libraries.architecture.AsyncAction
|
|||
|
||||
data class ConfirmingDeclineInvite(
|
||||
val inviteData: InviteData,
|
||||
val blockUser: Boolean,
|
||||
) : AsyncAction.Confirming
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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 & 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 & block"</string>
|
||||
<string name="screen_join_room_decline_and_block_button_title">"Decline and block"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
|||
fun onSessionConfirmRecoveryKeyClick()
|
||||
fun onRoomSettingsClick(roomId: RoomId)
|
||||
fun onReportBugClick()
|
||||
fun onRoomDirectorySearchClick()
|
||||
fun onLogoutForNativeSlidingSyncMigrationNeeded()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = { },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -47,10 +47,6 @@ internal fun PreferenceCategoryPreview() = ElementThemedPreview {
|
|||
PreferenceCategory(
|
||||
title = "Category title",
|
||||
) {
|
||||
PreferenceText(
|
||||
title = "Title",
|
||||
icon = CompoundIcons.ChatProblem(),
|
||||
)
|
||||
PreferenceSwitch(
|
||||
title = "Switch",
|
||||
icon = CompoundIcons.Threads(),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue