Merge branch 'develop' into renovate/org.maplibre.gl-android-sdk-10.x

This commit is contained in:
Benoit Marty 2024-05-03 14:46:37 +02:00 committed by GitHub
commit 7bdf9c821b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
316 changed files with 1881 additions and 506 deletions

View file

@ -9,8 +9,8 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx8g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.incremental=false -XX:+UseParallelGC
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8 --no-daemon CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 8
jobs: jobs:
debug: debug:
@ -41,21 +41,28 @@ jobs:
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v3
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK - name: Assemble debug Gplay APK
if: ${{ matrix.variant == 'debug' }} if: ${{ matrix.variant == 'debug' }}
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew :app:assembleGplayDebug :app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload APK APKs - name: Assemble debug Fdroid APK
if: ${{ matrix.variant == 'debug' }}
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug APKs
if: ${{ matrix.variant == 'debug' }} if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: elementx-debug name: elementx-debug
path: | path: |
app/build/outputs/apk/gplay/debug/*.apk app/build/outputs/apk/gplay/debug/*-universal-debug.apk
app/build/outputs/apk/fdroid/debug/*.apk app/build/outputs/apk/fdroid/debug/*-universal-debug.apk
- uses: rnkdsh/action-upload-diawi@v1.5.5 - uses: rnkdsh/action-upload-diawi@v1.5.5
id: diawi id: diawi
# Do not fail the whole build if Diawi upload fails # Do not fail the whole build if Diawi upload fails

View file

@ -74,6 +74,45 @@
<data android:scheme="io.element" /> <data android:scheme="io.element" />
</intent-filter> </intent-filter>
<!--
Element web links
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="*.element.io" />
</intent-filter>
<!--
matrix.to links
Note: On Android 12 and higher clicking a web link (that is not an Android App Link) always shows content in a web browser
https://developer.android.com/training/app-links#web-links
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="matrix.to" />
</intent-filter>
<!--
links from matrix.to website
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="element" />
<data android:host="user" />
<data android:host="room" />
</intent-filter>
</activity> </activity>
<provider <provider

View file

@ -39,6 +39,7 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.Theme import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.isDark import io.element.android.compound.theme.isDark
import io.element.android.compound.theme.mapToTheme import io.element.android.compound.theme.mapToTheme
import io.element.android.features.call.ui.ElementCallActivity
import io.element.android.features.lockscreen.api.handleSecureFlag import io.element.android.features.lockscreen.api.handleSecureFlag
import io.element.android.features.lockscreen.api.isLocked import io.element.android.features.lockscreen.api.isLocked
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
@ -58,6 +59,13 @@ class MainActivity : NodeActivity() {
Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}") Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}")
installSplashScreen() installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (ElementCallActivity.maybeStart(this, intent)) {
Timber.tag(loggerTag.value).w("Starting Element Call Activity")
if (savedInstanceState == null) {
finish()
return
}
}
appBindings = bindings() appBindings = bindings()
appBindings.lockScreenService().handleSecureFlag(this) appBindings.lockScreenService().handleSecureFlag(this)
enableEdgeToEdge() enableEdgeToEdge()
@ -135,11 +143,18 @@ class MainActivity : NodeActivity() {
* Called when: * Called when:
* - the launcher icon is clicked (if the app is already running); * - the launcher icon is clicked (if the app is already running);
* - a notification is clicked. * - a notification is clicked.
* - a deep link have been clicked
* - the app is going to background (<- this is strange) * - the app is going to background (<- this is strange)
*/ */
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
Timber.tag(loggerTag.value).w("onNewIntent") Timber.tag(loggerTag.value).w("onNewIntent")
if (ElementCallActivity.maybeStart(this, intent)) {
Timber.tag(loggerTag.value).w("Starting Element Call Activity")
return
}
// If the mainNode is not init yet, keep the intent for later. // If the mainNode is not init yet, keep the intent for later.
// It can happen when the activity is killed by the system. The methods are called in this order : // It can happen when the activity is killed by the system. The methods are called in this order :
// onCreate(savedInstanceState=true) -> onNewIntent -> onResume -> onMainNodeInit // onCreate(savedInstanceState=true) -> onNewIntent -> onResume -> onMainNodeInit

View file

@ -35,7 +35,6 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace import com.bumble.appyx.navmodel.backstack.operation.replace
import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
@ -57,16 +56,20 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForNavTargetAttached
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.MAIN_SPACE
import io.element.android.libraries.matrix.api.core.RoomId 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.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
@ -91,6 +94,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val createRoomEntryPoint: CreateRoomEntryPoint, private val createRoomEntryPoint: CreateRoomEntryPoint,
private val appNavigationStateService: AppNavigationStateService, private val appNavigationStateService: AppNavigationStateService,
private val secureBackupEntryPoint: SecureBackupEntryPoint, private val secureBackupEntryPoint: SecureBackupEntryPoint,
private val userProfileEntryPoint: UserProfileEntryPoint,
private val ftueEntryPoint: FtueEntryPoint, private val ftueEntryPoint: FtueEntryPoint,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
@ -197,6 +201,11 @@ class LoggedInFlowNode @AssistedInject constructor(
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages() val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
) : NavTarget ) : NavTarget
@Parcelize
data class UserProfile(
val userId: UserId,
) : NavTarget
@Parcelize @Parcelize
data class Settings( data class Settings(
val initialElement: PreferencesEntryPoint.InitialTarget = PreferencesEntryPoint.InitialTarget.Root val initialElement: PreferencesEntryPoint.InitialTarget = PreferencesEntryPoint.InitialTarget.Root
@ -270,14 +279,14 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
override fun onForwardedToSingleRoom(roomId: RoomId) { override fun onForwardedToSingleRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) } coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) }
} }
override fun onPermalinkClicked(data: PermalinkData) { override fun onPermalinkClicked(data: PermalinkData) {
when (data) { when (data) {
is PermalinkData.UserLink -> { is PermalinkData.UserLink -> {
// FIXME Add a user profile screen. // Should not happen (handled by MessagesNode)
Timber.e("User link clicked: ${data.userId}. TODO Add a user profile screen") Timber.e("User link clicked: ${data.userId}.")
} }
is PermalinkData.RoomLink -> { is PermalinkData.RoomLink -> {
backstack.push( backstack.push(
@ -306,6 +315,17 @@ class LoggedInFlowNode @AssistedInject constructor(
) )
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback)) createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
} }
is NavTarget.UserProfile -> {
val callback = object : UserProfileEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
}
}
userProfileEntryPoint.nodeBuilder(this, buildContext)
.params(UserProfileEntryPoint.Params(userId = navTarget.userId))
.callback(callback)
.build()
}
is NavTarget.Settings -> { is NavTarget.Settings -> {
val callback = object : PreferencesEntryPoint.Callback { val callback = object : PreferencesEntryPoint.Callback {
override fun onOpenBugReport() { override fun onOpenBugReport() {
@ -321,7 +341,7 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
} }
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement) val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
return preferencesEntryPoint.nodeBuilder(this, buildContext) preferencesEntryPoint.nodeBuilder(this, buildContext)
.params(inputs) .params(inputs)
.callback(callback) .callback(callback)
.build() .build()
@ -345,11 +365,6 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
NavTarget.Ftue -> { NavTarget.Ftue -> {
ftueEntryPoint.nodeBuilder(this, buildContext) ftueEntryPoint.nodeBuilder(this, buildContext)
.callback(object : FtueEntryPoint.Callback {
override fun onFtueFlowFinished() {
lifecycleScope.launch { attachRoomList() }
}
})
.build() .build()
} }
NavTarget.RoomDirectorySearch -> { NavTarget.RoomDirectorySearch -> {
@ -368,32 +383,42 @@ class LoggedInFlowNode @AssistedInject constructor(
} }
} }
suspend fun attachRoomList() { suspend fun attachRoom(roomIdOrAlias: RoomIdOrAlias, eventId: EventId? = null) {
if (!canShowRoomList()) return waitForNavTargetAttached { navTarget ->
attachChild<Node> { navTarget is NavTarget.RoomList
backstack.singleTop(NavTarget.RoomList)
} }
}
suspend fun attachRoom(roomId: RoomId) {
if (!canShowRoomList()) return
attachChild<RoomFlowNode> { attachChild<RoomFlowNode> {
backstack.singleTop(NavTarget.RoomList) backstack.push(
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) NavTarget.Room(
roomIdOrAlias = roomIdOrAlias,
initialElement = RoomNavigationTarget.Messages(
focusedEventId = eventId
)
)
)
} }
} }
private fun canShowRoomList(): Boolean { suspend fun attachUser(userId: UserId) {
return ftueService.state.value is FtueState.Complete waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList
}
attachChild<Node> {
backstack.push(
NavTarget.UserProfile(
userId = userId,
)
)
}
} }
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
Box(modifier = modifier) { Box(modifier = modifier) {
val lockScreenState by lockScreenStateService.lockState.collectAsState() val lockScreenState by lockScreenStateService.lockState.collectAsState()
val isFtueDisplayed by ftueService.state.collectAsState() val ftueState by ftueService.state.collectAsState()
BackstackView() BackstackView()
if (isFtueDisplayed is FtueState.Complete) { if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
} }
if (lockScreenState == LockScreenLockState.Locked) { if (lockScreenState == LockScreenLockState.Locked) {

View file

@ -55,6 +55,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.LoggedInState
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -279,17 +281,37 @@ class RootFlowNode @AssistedInject constructor(
when (resolvedIntent) { when (resolvedIntent) {
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData) is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction) is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
is ResolvedIntent.Permalink -> navigateTo(resolvedIntent.permalinkData)
} }
} }
private suspend fun navigateTo(permalinkData: PermalinkData) {
Timber.d("Navigating to $permalinkData")
attachSession(null)
.apply {
when (permalinkData) {
is PermalinkData.FallbackLink -> Unit
is PermalinkData.RoomEmailInviteLink -> Unit
is PermalinkData.RoomLink -> {
attachRoom(
roomIdOrAlias = permalinkData.roomIdOrAlias,
eventId = permalinkData.eventId,
)
}
is PermalinkData.UserLink -> {
attachUser(permalinkData.userId)
}
}
}
}
private suspend fun navigateTo(deeplinkData: DeeplinkData) { private suspend fun navigateTo(deeplinkData: DeeplinkData) {
Timber.d("Navigating to $deeplinkData") Timber.d("Navigating to $deeplinkData")
attachSession(deeplinkData.sessionId) attachSession(deeplinkData.sessionId)
.attachSession()
.apply { .apply {
when (deeplinkData) { when (deeplinkData) {
is DeeplinkData.Root -> attachRoomList() is DeeplinkData.Root -> Unit // The room list will always be shown, observing FtueState
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId) is DeeplinkData.Room -> attachRoom(deeplinkData.roomId.toRoomIdOrAlias())
} }
} }
} }
@ -298,10 +320,12 @@ class RootFlowNode @AssistedInject constructor(
oidcActionFlow.post(oidcAction) oidcActionFlow.post(oidcAction)
} }
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode { // [sessionId] will be null for permalink.
private suspend fun attachSession(sessionId: SessionId?): LoggedInFlowNode {
// TODO handle multi-session // TODO handle multi-session
return waitForChildAttached { navTarget -> return waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId)
} }
.attachSession()
} }
} }

View file

@ -21,27 +21,41 @@ import io.element.android.features.login.api.oidc.OidcAction
import io.element.android.features.login.api.oidc.OidcIntentResolver import io.element.android.features.login.api.oidc.OidcIntentResolver
import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
sealed interface ResolvedIntent { sealed interface ResolvedIntent {
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
} }
class IntentResolver @Inject constructor( class IntentResolver @Inject constructor(
private val deeplinkParser: DeeplinkParser, private val deeplinkParser: DeeplinkParser,
private val oidcIntentResolver: OidcIntentResolver private val oidcIntentResolver: OidcIntentResolver,
private val permalinkParser: PermalinkParser,
) { ) {
fun resolve(intent: Intent): ResolvedIntent? { fun resolve(intent: Intent): ResolvedIntent? {
if (intent.canBeIgnored()) return null if (intent.canBeIgnored()) return null
// Coming from a notification?
val deepLinkData = deeplinkParser.getFromIntent(intent) val deepLinkData = deeplinkParser.getFromIntent(intent)
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData) if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
// Coming during login using Oidc?
val oidcAction = oidcIntentResolver.resolve(intent) val oidcAction = oidcIntentResolver.resolve(intent)
if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction) if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
// External link clicked? (matrix.to, element.io, etc.)
val permalinkData = intent
.takeIf { it.action == Intent.ACTION_VIEW }
?.dataString
?.let { permalinkParser.parse(it) }
?.takeIf { it !is PermalinkData.FallbackLink }
if (permalinkData != null) return ResolvedIntent.Permalink(permalinkData)
// Unknown intent // Unknown intent
Timber.w("Unknown intent") Timber.w("Unknown intent")
return null return null

View file

@ -47,16 +47,17 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId 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.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ -68,7 +69,6 @@ class RoomFlowNode @AssistedInject constructor(
@Assisted val buildContext: BuildContext, @Assisted val buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val client: MatrixClient, private val client: MatrixClient,
private val roomMembershipObserver: RoomMembershipObserver,
private val joinRoomEntryPoint: JoinRoomEntryPoint, private val joinRoomEntryPoint: JoinRoomEntryPoint,
private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint, private val roomAliasResolverEntryPoint: RoomAliasResolverEntryPoint,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
@ -121,14 +121,17 @@ class RoomFlowNode @AssistedInject constructor(
} }
private fun subscribeToRoomInfoFlow(roomId: RoomId) { private fun subscribeToRoomInfoFlow(roomId: RoomId) {
client.getRoomInfoFlow( val roomInfoFlow = client.getRoomInfoFlow(
roomId = roomId roomId = roomId
) ).map { it.getOrNull() }
.onEach { roomInfo ->
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}") val isSpaceFlow = roomInfoFlow.map { it?.isSpace.orFalse() }.distinctUntilChanged()
val info = roomInfo.getOrNull() val currentMembershipFlow = roomInfoFlow.map { it?.currentUserMembership }.distinctUntilChanged()
if (info?.currentUserMembership == CurrentUserMembership.JOINED) { combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace ->
if (info.isSpace) { Timber.d("Room membership: $membership")
when (membership) {
CurrentUserMembership.JOINED -> {
if (isSpace) {
// It should not happen, but probably due to an issue in the sliding sync, // It should not happen, but probably due to an issue in the sliding sync,
// we can have a space here in case the space has just been joined. // we can have a space here in case the space has just been joined.
// So navigate to the JoinRoom target for now, which will // So navigate to the JoinRoom target for now, which will
@ -137,19 +140,17 @@ class RoomFlowNode @AssistedInject constructor(
} else { } else {
backstack.newRoot(NavTarget.JoinedRoom(roomId)) backstack.newRoot(NavTarget.JoinedRoom(roomId))
} }
} else { }
CurrentUserMembership.LEFT -> {
// Left the room, navigate out of this flow
navigateUp()
}
else -> {
// Was invited or the room is not known, display the join room screen
backstack.newRoot(NavTarget.JoinRoom(roomId)) backstack.newRoot(NavTarget.JoinRoom(roomId))
} }
} }
.launchIn(lifecycleScope) }.launchIn(lifecycleScope)
// When leaving the room from this session only, navigate up.
roomMembershipObserver.updates
.filter { update -> update.roomId == roomId && !update.isUserInRoom }
.onEach {
navigateUp()
}
.launchIn(lifecycleScope)
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {

View file

@ -18,6 +18,7 @@ package io.element.android.appnav.intent
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcAction
@ -26,9 +27,12 @@ import io.element.android.features.login.impl.oidc.OidcUrlParser
import io.element.android.libraries.deeplink.DeepLinkCreator import io.element.android.libraries.deeplink.DeepLinkCreator
import io.element.android.libraries.deeplink.DeeplinkData import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import org.junit.Assert.assertThrows import org.junit.Assert.assertThrows
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -162,9 +166,60 @@ class IntentResolverTest {
} }
} }
@Test
fun `test resolve external permalink`() {
val permalinkData = PermalinkData.UserLink(
userId = UserId("@alice:matrix.org")
)
val sut = createIntentResolver(
permalinkParserResult = { permalinkData }
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "https://matrix.to/#/@alice:matrix.org".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isEqualTo(
ResolvedIntent.Permalink(
permalinkData = permalinkData
)
)
}
@Test
fun `test resolve external permalink, FallbackLink should be ignored`() {
val sut = createIntentResolver(
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
data = "https://matrix.to/#/@alice:matrix.org".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isNull()
}
@Test
fun `test resolve external permalink, invalid action`() {
val permalinkData = PermalinkData.UserLink(
userId = UserId("@alice:matrix.org")
)
val sut = createIntentResolver(
permalinkParserResult = { permalinkData }
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_SEND
data = "https://matrix.to/invalid".toUri()
}
val result = sut.resolve(intent)
assertThat(result).isNull()
}
@Test @Test
fun `test resolve invalid`() { fun `test resolve invalid`() {
val sut = createIntentResolver() val sut = createIntentResolver(
permalinkParserResult = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply { val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
data = "io.element:/invalid".toUri() data = "io.element:/invalid".toUri()
@ -173,12 +228,17 @@ class IntentResolverTest {
assertThat(result).isNull() assertThat(result).isNull()
} }
private fun createIntentResolver(): IntentResolver { private fun createIntentResolver(
permalinkParserResult: () -> PermalinkData = { throw NotImplementedError() }
): IntentResolver {
return IntentResolver( return IntentResolver(
deeplinkParser = DeeplinkParser(), deeplinkParser = DeeplinkParser(),
oidcIntentResolver = DefaultOidcIntentResolver( oidcIntentResolver = DefaultOidcIntentResolver(
oidcUrlParser = OidcUrlParser() oidcUrlParser = OidcUrlParser()
), ),
permalinkParser = FakePermalinkParser(
result = permalinkParserResult
),
) )
} }
} }

1
changelog.d/2771.feature Normal file
View file

@ -0,0 +1 @@
Pretty-print event JSON in debug viewer

1
changelog.d/2776.feature Normal file
View file

@ -0,0 +1 @@
Add support for external permalinks.

1
changelog.d/2778.bugfix Normal file
View file

@ -0,0 +1 @@
Ensure the application open the room when a notification is clicked.

71
docs/deeplink.md Normal file
View file

@ -0,0 +1,71 @@
# Element X Android deeplink
<!--- TOC -->
* [Introduction](#introduction)
* [Asset Links](#asset-links)
* [Supported links](#supported-links)
* [Developer tools](#developer-tools)
<!--- END -->
## Introduction
Element X Android supports deep linking to specific screens in the application. This document explains how to use deep links in Element X Android.
### Asset Links
The asset links file is available at https://element.io/.well-known/assetlinks.json
### Supported links
Element Call link:
> https://call.element.io/Example
Link to a user:
> https://app.element.io/#/user/@alice:matrix.org
Link to a room by id or alias:
> https://app.element.io/#/room/!roomid:matrix.org
> https://app.element.io/#/room/#element-x-android:matrix.org
Link to a room with a specific event:
> https://app.element.io/#/room/!roomid:matrix.org/$eventid
Note that it will also work with other domain such as:
> https://mobile.element.io
> https://develop.element.io
> https://staging.element.io
## Developer tools
Using an Android 12 or higher emulator
Ensure links verification is enabled
```bash
adb shell am compat enable 175408749 io.element.android.x.debug
```
Reset link verifications for the given package id
```bash
adb shell pm set-app-links --package io.element.android.x.debug 0 all
```
Force the package id links to be verified
```bash
adb shell pm verify-app-links --re-verify io.element.android.x.debug
```
Print the link verification of the package id
```bash
adb shell pm get-app-links io.element.android.x.debug
```
```
io.element.android.x.debug:
ID: e2ece472-c266-4bf0-829c-be79959a6270
Signatures: [B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E]
Domain verification state:
*.element.io: 1024
```

View file

@ -34,15 +34,10 @@
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode" android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter android:autoVerify="true"> <!--
<action android:name="android.intent.action.VIEW" /> Note: intent-filter for https://call.element.io link is now managed by the MainActivity.
<category android:name="android.intent.category.DEFAULT" /> -->
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="call.element.io" />
</intent-filter>
<!-- Custom scheme to handle urls from other domains in the format: element://call?url=https%3A%2F%2Felement.io --> <!-- Custom scheme to handle urls from other domains in the format: element://call?url=https%3A%2F%2Felement.io -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />

View file

@ -17,6 +17,7 @@
package io.element.android.features.call.ui package io.element.android.features.call.ui
import android.Manifest import android.Manifest
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
@ -35,6 +36,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import com.bumble.appyx.core.integrationpoint.NodeComponentActivity import com.bumble.appyx.core.integrationpoint.NodeComponentActivity
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -47,6 +49,7 @@ import io.element.android.features.call.di.CallBindings
import io.element.android.features.call.utils.CallIntentDataParser import io.element.android.features.call.utils.CallIntentDataParser
import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.features.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.bool.orFalse
import javax.inject.Inject import javax.inject.Inject
class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator { class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
@ -63,6 +66,28 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
} }
context.startActivity(intent) context.startActivity(intent)
} }
/**
* Starts the [ElementCallActivity] if the intent contains a valid URL,
* and returns true if it's the case.
*/
fun maybeStart(
activity: Activity,
intent: Intent?,
): Boolean {
return intent?.data
?.takeIf { uri -> uri.scheme == "https" && uri.host == "call.element.io" }
?.let { uri ->
val callIntent = Intent(activity, ElementCallActivity::class.java).apply {
data = uri
}
// Disable animation since MainActivity has already been animated.
val options = ActivityOptionsCompat.makeCustomAnimation(activity, 0, 0)
activity.startActivity(callIntent, options.toBundle())
true
}
.orFalse()
}
} }
@Inject lateinit var callIntentDataParser: CallIntentDataParser @Inject lateinit var callIntentDataParser: CallIntentDataParser

View file

@ -7,7 +7,7 @@
<string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string> <string name="screen_create_room_private_option_title">"Privater Raum (nur auf Einladung)"</string>
<string name="screen_create_room_public_option_description">"Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."</string> <string name="screen_create_room_public_option_description">"Die Nachrichten sind nicht verschlüsselt und können von jedem gelesen werden. Die Verschlüsselung kann zu einem späteren Zeitpunkt aktiviert werden."</string>
<string name="screen_create_room_public_option_title">"Öffentlicher Raum (für alle)"</string> <string name="screen_create_room_public_option_title">"Öffentlicher Raum (für alle)"</string>
<string name="screen_create_room_room_name_label">"Raum-Name"</string> <string name="screen_create_room_room_name_label">"Raumname"</string>
<string name="screen_create_room_title">"Raum erstellen"</string> <string name="screen_create_room_title">"Raum erstellen"</string>
<string name="screen_create_room_topic_label">"Thema (optional)"</string> <string name="screen_create_room_topic_label">"Thema (optional)"</string>
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string> <string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>

View file

@ -18,18 +18,12 @@ package io.element.android.features.ftue.api
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
interface FtueEntryPoint : FeatureEntryPoint { interface FtueEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder { interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder
fun build(): Node fun build(): Node
} }
interface Callback : Plugin {
fun onFtueFlowFinished()
}
} }

View file

@ -31,11 +31,6 @@ class DefaultFtueEntryPoint @Inject constructor() : FtueEntryPoint {
val plugins = ArrayList<Plugin>() val plugins = ArrayList<Plugin>()
return object : FtueEntryPoint.NodeBuilder { return object : FtueEntryPoint.NodeBuilder {
override fun callback(callback: FtueEntryPoint.Callback): FtueEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun build(): Node { override fun build(): Node {
return parentNode.createNode<FtueFlowNode>(buildContext, plugins) return parentNode.createNode<FtueFlowNode>(buildContext, plugins)
} }

View file

@ -32,7 +32,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.analytics.api.AnalyticsEntryPoint
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode
import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode
import io.element.android.features.ftue.impl.state.DefaultFtueService import io.element.android.features.ftue.impl.state.DefaultFtueService
@ -86,8 +85,6 @@ class FtueFlowNode @AssistedInject constructor(
data object LockScreenSetup : NavTarget data object LockScreenSetup : NavTarget
} }
private val callback = plugins.filterIsInstance<FtueEntryPoint.Callback>().firstOrNull()
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
@ -157,7 +154,7 @@ class FtueFlowNode @AssistedInject constructor(
FtueStep.LockscreenSetup -> { FtueStep.LockscreenSetup -> {
backstack.newRoot(NavTarget.LockScreenSetup) backstack.newRoot(NavTarget.LockScreenSetup)
} }
null -> callback?.onFtueFlowFinished() null -> Unit
} }
} }

View file

@ -15,8 +15,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string> <string name="screen_qr_code_login_initial_state_item_2">"Націсніце на свой аватар"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Выберыце %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"“Звязаць новую прыладу”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Выберыце %1$s"</string> <string name="screen_qr_code_login_initial_state_item_4">"Выконвайце паказаныя інструкцыі"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"“Паказаць QR-код”"</string>
<string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string> <string name="screen_qr_code_login_initial_state_title">"Адкрыйце %1$s на іншай прыладзе, каб атрымаць QR-код"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Выкарыстоўвайце QR-код, паказаны на іншай прыладзе."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Паўтарыць спробу"</string>

View file

@ -15,8 +15,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string> <string name="screen_qr_code_login_initial_state_item_2">"Klikněte na svůj avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Vybrat %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string> <string name="screen_qr_code_login_initial_state_item_3_action">"\"Připojit nové zařízení\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Vybrat %1$s"</string> <string name="screen_qr_code_login_initial_state_item_4">"Postupujte podle uvedených pokynů"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Zobrazit QR kód\""</string>
<string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string> <string name="screen_qr_code_login_initial_state_title">"Otevřete %1$s na jiném zařízení pro získání QR kódu"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Použijte QR kód zobrazený na druhém zařízení."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Zkusit znovu"</string>

View file

@ -15,8 +15,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string> <string name="screen_qr_code_login_initial_state_item_2">"Klick auf deinen Avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Wähle %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string> <string name="screen_qr_code_login_initial_state_item_3_action">"\"Neues Gerät verknüpfen\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Wähle %1$s"</string> <string name="screen_qr_code_login_initial_state_item_4">"Befolge die angezeigten Anweisungen"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"\"QR-Code anzeigen\""</string>
<string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string> <string name="screen_qr_code_login_initial_state_title">"Öffne %1$s auf einem anderen Gerät, um den QR-Code zu erhalten"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Verwende den QR-Code, der auf dem anderen Gerät angezeigt wird."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Erneut versuchen"</string>

View file

@ -7,8 +7,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string> <string name="screen_qr_code_login_initial_state_item_2">"Cliquez sur votre image de profil"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Choisissez %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"“Associer une nouvelle session”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Choisissez %1$s"</string> <string name="screen_qr_code_login_initial_state_item_4">"Suivez les instructions affichées"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"“Afficher le QR code”"</string>
<string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string> <string name="screen_qr_code_login_initial_state_title">"Ouvrez %1$s sur un autre appareil pour obtenir le QR code"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur lautre appareil."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Scannez le QR code affiché sur lautre appareil."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>

View file

@ -15,8 +15,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string> <string name="screen_qr_code_login_initial_state_item_2">"Kattintson a profilképére"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Válassza ezt: %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"„Új eszköz összekapcsolása”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Válassza ezt: %1$s"</string> <string name="screen_qr_code_login_initial_state_item_4">"Kövesse a látható utasításokat"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"„QR-kód megjelenítése”"</string>
<string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string> <string name="screen_qr_code_login_initial_state_title">"Nyissa meg az %1$set egy másik eszközön a QR-kód lekéréséhez."</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Használja a másik eszközön látható QR-kódot."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Próbálja újra"</string>

View file

@ -15,8 +15,6 @@
<string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string> <string name="screen_qr_code_login_initial_state_item_2">"Klik pada avatar Anda"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Pilih %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Tautkan perangkat baru”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"“Tautkan perangkat baru”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Pilih %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"“Tampilkan kode QR”"</string>
<string name="screen_qr_code_login_initial_state_title">"Buka %1$s di perangkat lain untuk mendapatkan kode QR"</string> <string name="screen_qr_code_login_initial_state_title">"Buka %1$s di perangkat lain untuk mendapatkan kode QR"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Gunakan kode QR yang ditampilkan di perangkat lain."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Coba lagi"</string>

View file

@ -15,8 +15,6 @@
<string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string> <string name="screen_qr_code_login_initial_state_item_2">"Нажмите на свое изображение"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Выбрать %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string> <string name="screen_qr_code_login_initial_state_item_3_action">"\"Привязать новое устройство\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Выбрать %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"\"Показать QR-код\""</string>
<string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string> <string name="screen_qr_code_login_initial_state_title">"Откройте %1$s на другом устройстве, чтобы получить QR-код"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>

View file

@ -15,8 +15,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string> <string name="screen_qr_code_login_initial_state_item_2">"Kliknite na svoj obrázok"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Vyberte %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Vyberte %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"„Prepojiť nové zariadenie“"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"„Prepojiť nové zariadenie“"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Vyberte %1$s"</string> <string name="screen_qr_code_login_initial_state_item_4">"Postupujte podľa zobrazených pokynov"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"„Zobraziť QR kód“"</string>
<string name="screen_qr_code_login_initial_state_title">"Ak chcete získať QR kód, otvorte %1$s na inom zariadení"</string> <string name="screen_qr_code_login_initial_state_title">"Ak chcete získať QR kód, otvorte %1$s na inom zariadení"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Použite QR kód zobrazený na druhom zariadení."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Použite QR kód zobrazený na druhom zariadení."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Skúste to znova"</string>

View file

@ -15,8 +15,7 @@
<string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string> <string name="screen_qr_code_login_initial_state_item_2">"Click on your avatar"</string>
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Select %1$s"</string> <string name="screen_qr_code_login_initial_state_item_4">"Follow the instructions shown"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"“Show QR code”"</string>
<string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string> <string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>

View file

@ -16,7 +16,7 @@
<string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string> <string name="screen_app_lock_setup_confirm_pin">"PIN bestätigen"</string>
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string> <string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Aus Sicherheitsgründen kann dieser PIN-Code nicht verwendet werden."</string>
<string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Bitte eine andere PIN verwenden."</string> <string name="screen_app_lock_setup_pin_blacklisted_dialog_title">"Bitte eine andere PIN verwenden."</string>
<string name="screen_app_lock_setup_pin_context">"Sperre %1$s mit einem PIN Code, um den Zugriff auf Deine Chats zu beschränken. <string name="screen_app_lock_setup_pin_context">"Sperre %1$s mit einem PIN Code, um den Zugriff auf deine Chats zu beschränken.
Wähle etwas Einprägsames. Bei falscher Eingabe wirst du aus der App ausgeloggt."</string> Wähle etwas Einprägsames. Bei falscher Eingabe wirst du aus der App ausgeloggt."</string>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Bitte gib die gleiche PIN wie zuvor ein."</string> <string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Bitte gib die gleiche PIN wie zuvor ein."</string>

View file

@ -1,11 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_app_lock_biometric_authentication">"biometrisk autentisering"</string>
<string name="screen_app_lock_biometric_unlock">"biometrisk upplåsning"</string>
<string name="screen_app_lock_biometric_unlock_title_android">"Lås upp med biometri"</string>
<string name="screen_app_lock_forgot_pin">"Glömt PIN-kod?"</string> <string name="screen_app_lock_forgot_pin">"Glömt PIN-kod?"</string>
<string name="screen_app_lock_settings_change_pin">"Byt PIN-kod"</string> <string name="screen_app_lock_settings_change_pin">"Byt PIN-kod"</string>
<string name="screen_app_lock_settings_enable_biometric_unlock">"Tillåt biometrisk upplåsning"</string> <string name="screen_app_lock_settings_enable_biometric_unlock">"Tillåt biometrisk upplåsning"</string>
<string name="screen_app_lock_settings_remove_pin">"Ta bort PIN-kod"</string> <string name="screen_app_lock_settings_remove_pin">"Ta bort PIN-kod"</string>
<string name="screen_app_lock_settings_remove_pin_alert_message">"Är du säker på att du vill ta bort PIN-koden?"</string> <string name="screen_app_lock_settings_remove_pin_alert_message">"Är du säker på att du vill ta bort PIN-koden?"</string>
<string name="screen_app_lock_settings_remove_pin_alert_title">"Ta bort PIN-koden?"</string> <string name="screen_app_lock_settings_remove_pin_alert_title">"Ta bort PIN-koden?"</string>
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Tillåt %1$s"</string>
<string name="screen_app_lock_setup_biometric_unlock_skip">"Jag vill hellre använda PIN-kod"</string>
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Bespara dig själv lite tid och använd %1$s för att låsa upp appen varje gång"</string>
<string name="screen_app_lock_setup_choose_pin">"Välj PIN-kod"</string> <string name="screen_app_lock_setup_choose_pin">"Välj PIN-kod"</string>
<string name="screen_app_lock_setup_confirm_pin">"Bekräfta PIN-kod"</string> <string name="screen_app_lock_setup_confirm_pin">"Bekräfta PIN-kod"</string>
<string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Du kan inte välja detta som din PIN-kod av säkerhetsskäl"</string> <string name="screen_app_lock_setup_pin_blacklisted_dialog_content">"Du kan inte välja detta som din PIN-kod av säkerhetsskäl"</string>
@ -25,5 +31,7 @@ Välj något minnesvärt. Om du glömmer den här PIN-koden loggas du ut från a
<item quantity="one">"Fel PIN-kod. Du har %1$d försök kvar"</item> <item quantity="one">"Fel PIN-kod. Du har %1$d försök kvar"</item>
<item quantity="other">"Fel PIN-kod. Du har %1$d försök kvar"</item> <item quantity="other">"Fel PIN-kod. Du har %1$d försök kvar"</item>
</plurals> </plurals>
<string name="screen_app_lock_use_biometric_android">"Använd biometri"</string>
<string name="screen_app_lock_use_pin_android">"Använd PIN-kod"</string>
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string> <string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
</resources> </resources>

View file

@ -5,7 +5,7 @@
<string name="screen_signout_confirmation_dialog_title">"Abmelden"</string> <string name="screen_signout_confirmation_dialog_title">"Abmelden"</string>
<string name="screen_signout_in_progress_dialog_content">"Abmelden…"</string> <string name="screen_signout_in_progress_dialog_content">"Abmelden…"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten."</string> <string name="screen_signout_key_backup_disabled_subtitle">"Du bist dabei, dich von deiner letzten Sitzung abzumelden. Wenn du dich jetzt abmeldest, verlierst du den Zugriff auf deine verschlüsselten Nachrichten."</string>
<string name="screen_signout_key_backup_disabled_title">"Du hast das Backup ausgeschaltet"</string> <string name="screen_signout_key_backup_disabled_title">"Du hast das Backup deaktiviert."</string>
<string name="screen_signout_key_backup_offline_subtitle">"Deine Schlüssel wurden noch gesichert, als du offline gegangen bist. Stelle die Verbindung wieder her, damit deine Schlüssel gesichert werden können, bevor du dich abmeldest."</string> <string name="screen_signout_key_backup_offline_subtitle">"Deine Schlüssel wurden noch gesichert, als du offline gegangen bist. Stelle die Verbindung wieder her, damit deine Schlüssel gesichert werden können, bevor du dich abmeldest."</string>
<string name="screen_signout_key_backup_offline_title">"Deine Schlüssel werden noch gesichert"</string> <string name="screen_signout_key_backup_offline_title">"Deine Schlüssel werden noch gesichert"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Bitte warte, bis der Vorgang abgeschlossen ist, bevor du dich abmeldest."</string> <string name="screen_signout_key_backup_ongoing_subtitle">"Bitte warte, bis der Vorgang abgeschlossen ist, bevor du dich abmeldest."</string>

View file

@ -4,8 +4,15 @@
<string name="screen_signout_confirmation_dialog_submit">"Logga ut"</string> <string name="screen_signout_confirmation_dialog_submit">"Logga ut"</string>
<string name="screen_signout_confirmation_dialog_title">"Logga ut"</string> <string name="screen_signout_confirmation_dialog_title">"Logga ut"</string>
<string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string> <string name="screen_signout_in_progress_dialog_content">"Loggar ut …"</string>
<string name="screen_signout_key_backup_disabled_subtitle">"Du är på väg att logga ut ur din senaste session. Om du loggar ut nu kommer du att förlora åtkomsten till dina krypterade meddelanden."</string>
<string name="screen_signout_key_backup_disabled_title">"Du har stängt av säkerhetskopiering"</string> <string name="screen_signout_key_backup_disabled_title">"Du har stängt av säkerhetskopiering"</string>
<string name="screen_signout_key_backup_offline_subtitle">"Dina nycklar säkerhetskopierades fortfarande när du gick offline. Anslut igen så att dina nycklar kan säkerhetskopieras innan du loggar ut."</string>
<string name="screen_signout_key_backup_offline_title">"Dina nycklar säkerhetskopieras fortfarande"</string>
<string name="screen_signout_key_backup_ongoing_subtitle">"Vänta tills detta är klart innan du loggar ut."</string>
<string name="screen_signout_key_backup_ongoing_title">"Dina nycklar säkerhetskopieras fortfarande"</string>
<string name="screen_signout_preference_item">"Logga ut"</string> <string name="screen_signout_preference_item">"Logga ut"</string>
<string name="screen_signout_recovery_disabled_subtitle">"Du är på väg att logga ut ur din sista session. Om du loggar ut nu förlorar du åtkomsten till dina krypterade meddelanden."</string> <string name="screen_signout_recovery_disabled_subtitle">"Du är på väg att logga ut ur din sista session. Om du loggar ut nu förlorar du åtkomsten till dina krypterade meddelanden."</string>
<string name="screen_signout_recovery_disabled_title">"Återställning inte inställd"</string> <string name="screen_signout_recovery_disabled_title">"Återställning inte inställd"</string>
<string name="screen_signout_save_recovery_key_subtitle">"Du är på väg att logga ut från din senaste session. Om du loggar ut nu kan du förlora åtkomsten till dina krypterade meddelanden."</string>
<string name="screen_signout_save_recovery_key_title">"Har du sparat din återställningsnyckel?"</string>
</resources> </resources>

View file

@ -59,6 +59,8 @@ 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.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import org.json.JSONException
import org.json.JSONObject
/** /**
* Screen used to display debug info for events. * Screen used to display debug info for events.
@ -109,18 +111,27 @@ fun EventDebugInfoView(
} }
if (originalJson != null) { if (originalJson != null) {
item { item {
CollapsibleSection(title = "Original JSON:", text = originalJson, initiallyExpanded = sectionsInitiallyExpanded) CollapsibleSection(title = "Original JSON:", text = prettyJSON(originalJson), initiallyExpanded = sectionsInitiallyExpanded)
} }
} }
if (latestEditedJson != null) { if (latestEditedJson != null) {
item { item {
CollapsibleSection(title = "Latest edited JSON:", text = latestEditedJson, initiallyExpanded = sectionsInitiallyExpanded) CollapsibleSection(title = "Latest edited JSON:", text = prettyJSON(latestEditedJson), initiallyExpanded = sectionsInitiallyExpanded)
} }
} }
} }
} }
} }
private fun prettyJSON(maybeJSON: String): String {
return try {
JSONObject(maybeJSON).toString(2)
} catch (e: JSONException) {
// Prefer not pretty-printing over crashing if the data is not actually JSON
maybeJSON
}
}
@Composable @Composable
private fun CollapsibleSection( private fun CollapsibleSection(
title: String, title: String,

View file

@ -21,8 +21,10 @@
<string name="screen_room_attachment_source_poll">"Omröstning"</string> <string name="screen_room_attachment_source_poll">"Omröstning"</string>
<string name="screen_room_attachment_text_formatting">"Textformatering"</string> <string name="screen_room_attachment_text_formatting">"Textformatering"</string>
<string name="screen_room_encrypted_history_banner">"Meddelandehistoriken är för närvarande otillgänglig."</string> <string name="screen_room_encrypted_history_banner">"Meddelandehistoriken är för närvarande otillgänglig."</string>
<string name="screen_room_encrypted_history_banner_unverified">"Meddelandehistorik är inte tillgänglig i det här rummet. Verifiera den här enheten för att se din meddelandehistorik."</string>
<string name="screen_room_invite_again_alert_message">"Vill du bjuda tillbaka dem?"</string> <string name="screen_room_invite_again_alert_message">"Vill du bjuda tillbaka dem?"</string>
<string name="screen_room_invite_again_alert_title">"Du är ensam i den här chatten"</string> <string name="screen_room_invite_again_alert_title">"Du är ensam i den här chatten"</string>
<string name="screen_room_mentions_at_room_subtitle">"Meddela hela rummet"</string>
<string name="screen_room_mentions_at_room_title">"Alla"</string> <string name="screen_room_mentions_at_room_title">"Alla"</string>
<string name="screen_room_retry_send_menu_send_again_action">"Skicka igen"</string> <string name="screen_room_retry_send_menu_send_again_action">"Skicka igen"</string>
<string name="screen_room_retry_send_menu_title">"Ditt meddelande kunde inte skickas"</string> <string name="screen_room_retry_send_menu_title">"Ditt meddelande kunde inte skickas"</string>

View file

@ -58,6 +58,7 @@ dependencies {
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(projects.features.leaveroom.api) implementation(projects.features.leaveroom.api)
implementation(projects.features.createroom.api) implementation(projects.features.createroom.api)
implementation(projects.features.userprofile.shared)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)
implementation(projects.features.poll.api) implementation(projects.features.poll.api)

View file

@ -34,9 +34,10 @@ import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
import io.element.android.features.roomdetails.impl.members.details.avatar.AvatarPreviewNode
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode import io.element.android.features.roomdetails.impl.rolesandpermissions.RolesAndPermissionsFlowNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.userprofile.shared.avatar.AvatarPreviewNode
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
@ -78,7 +79,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data class RoomNotificationSettings( data class RoomNotificationSettings(
/** /**
* When presented from outsite the context of the room, the rooms settings UI is different. * When presented from outside the context of the room, the rooms settings UI is different.
* Figma designs: https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=5199-198932&mode=design&t=fTTvpuxYFjewYQOe-0 * Figma designs: https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?type=design&node-id=5199-198932&mode=design&t=fTTvpuxYFjewYQOe-0
*/ */
val showUserDefinedSettingStyle: Boolean val showUserDefinedSettingStyle: Boolean
@ -164,7 +165,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
} }
is NavTarget.RoomMemberDetails -> { is NavTarget.RoomMemberDetails -> {
val callback = object : RoomMemberDetailsNode.Callback { val callback = object : UserProfileNodeHelper.Callback {
override fun openAvatarPreview(username: String, avatarUrl: String) { override fun openAvatarPreview(username: String, avatarUrl: String) {
backstack.push(NavTarget.AvatarPreview(username, avatarUrl)) backstack.push(NavTarget.AvatarPreview(username, avatarUrl))
} }

View file

@ -17,7 +17,7 @@
package io.element.android.features.roomdetails.impl package io.element.android.features.roomdetails.impl
import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
@ -32,7 +32,7 @@ data class RoomDetailsState(
val memberCount: Long, val memberCount: Long,
val isEncrypted: Boolean, val isEncrypted: Boolean,
val roomType: RoomDetailsType, val roomType: RoomDetailsType,
val roomMemberDetailsState: RoomMemberDetailsState?, val roomMemberDetailsState: UserProfileState?,
val canEdit: Boolean, val canEdit: Boolean,
val canInvite: Boolean, val canInvite: Boolean,
val canShowNotificationSettings: Boolean, val canShowNotificationSettings: Boolean,

View file

@ -19,8 +19,8 @@ package io.element.android.features.roomdetails.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId 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.core.UserId
@ -90,7 +90,7 @@ fun aRoomDetailsState(
canEdit: Boolean = false, canEdit: Boolean = false,
canShowNotificationSettings: Boolean = true, canShowNotificationSettings: Boolean = true,
roomType: RoomDetailsType = RoomDetailsType.Room, roomType: RoomDetailsType = RoomDetailsType.Room,
roomMemberDetailsState: RoomMemberDetailsState? = null, roomMemberDetailsState: UserProfileState? = null,
leaveRoomState: LeaveRoomState = aLeaveRoomState(), leaveRoomState: LeaveRoomState = aLeaveRoomState(),
roomNotificationSettings: RoomNotificationSettings = aRoomNotificationSettings(), roomNotificationSettings: RoomNotificationSettings = aRoomNotificationSettings(),
isFavorite: Boolean = false, isFavorite: Boolean = false,
@ -130,5 +130,5 @@ fun aDmRoomDetailsState(
) = aRoomDetailsState( ) = aRoomDetailsState(
roomName = roomName, roomName = roomName,
roomType = RoomDetailsType.Dm(aDmRoomMember(isIgnored = isDmMemberIgnored)), roomType = RoomDetailsType.Dm(aDmRoomMember(isIgnored = isDmMemberIgnored)),
roomMemberDetailsState = aRoomMemberDetailsState() roomMemberDetailsState = aUserProfileState()
) )

View file

@ -49,10 +49,10 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.leaveroom.api.LeaveRoomView import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs import io.element.android.features.userprofile.shared.UserProfileHeaderSection
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection import io.element.android.features.userprofile.shared.UserProfileMainActionsSection
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.Avatar
@ -143,15 +143,15 @@ fun RoomDetailsView(
is RoomDetailsType.Dm -> { is RoomDetailsType.Dm -> {
val member = state.roomType.roomMember val member = state.roomType.roomMember
RoomMemberHeaderSection( UserProfileHeaderSection(
avatarUrl = state.roomAvatarUrl ?: member.avatarUrl, avatarUrl = state.roomAvatarUrl ?: member.avatarUrl,
userId = member.userId.value, userId = member.userId,
userName = state.roomName, userName = state.roomName,
openAvatarPreview = { avatarUrl -> openAvatarPreview = { avatarUrl ->
openAvatarPreview(member.getBestName(), avatarUrl) openAvatarPreview(member.getBestName(), avatarUrl)
}, },
) )
RoomMemberMainActionsSection(onShareUser = ::onShareMember) UserProfileMainActionsSection(onShareUser = ::onShareMember)
} }
} }
Spacer(Modifier.height(18.dp)) Spacer(Modifier.height(18.dp))

View file

@ -28,8 +28,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.roomdetails.impl.R import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent import io.element.android.features.userprofile.shared.UserProfileView
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
@ -38,8 +38,6 @@ 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.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import timber.log.Timber
import io.element.android.libraries.androidutils.R as AndroidUtilsR
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
class RoomMemberDetailsNode @AssistedInject constructor( class RoomMemberDetailsNode @AssistedInject constructor(
@ -49,18 +47,14 @@ class RoomMemberDetailsNode @AssistedInject constructor(
private val permalinkBuilder: PermalinkBuilder, private val permalinkBuilder: PermalinkBuilder,
presenterFactory: RoomMemberDetailsPresenter.Factory, presenterFactory: RoomMemberDetailsPresenter.Factory,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : NodeInputs {
fun openAvatarPreview(username: String, avatarUrl: String)
fun onStartDM(roomId: RoomId)
}
data class RoomMemberDetailsInput( data class RoomMemberDetailsInput(
val roomMemberId: UserId val roomMemberId: UserId
) : NodeInputs ) : NodeInputs
private val inputs = inputs<RoomMemberDetailsInput>() private val inputs = inputs<RoomMemberDetailsInput>()
private val callback = inputs<Callback>() private val callback = inputs<UserProfileNodeHelper.Callback>()
private val presenter = presenterFactory.create(inputs.roomMemberId) private val presenter = presenterFactory.create(inputs.roomMemberId)
private val userProfileNodeHelper = UserProfileNodeHelper(inputs.roomMemberId)
init { init {
lifecycle.subscribe( lifecycle.subscribe(
@ -75,17 +69,7 @@ class RoomMemberDetailsNode @AssistedInject constructor(
val context = LocalContext.current val context = LocalContext.current
fun onShareUser() { fun onShareUser() {
val permalinkResult = permalinkBuilder.permalinkForUser(inputs.roomMemberId) userProfileNodeHelper.onShareUser(context, permalinkBuilder)
permalinkResult.onSuccess { permalink ->
context.startSharePlainTextIntent(
activityResultLauncher = null,
chooserTitle = context.getString(R.string.screen_room_details_share_room_title),
text = permalink,
noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found)
)
}.onFailure {
Timber.e(it)
}
} }
fun onStartDM(roomId: RoomId) { fun onStartDM(roomId: RoomId) {
@ -95,11 +79,12 @@ class RoomMemberDetailsNode @AssistedInject constructor(
val state = presenter.present() val state = presenter.present()
LaunchedEffect(state.startDmActionState) { LaunchedEffect(state.startDmActionState) {
if (state.startDmActionState is AsyncAction.Success) { val result = state.startDmActionState
onStartDM(state.startDmActionState.data) if (result is AsyncAction.Success) {
onStartDM(result.data)
} }
} }
RoomMemberDetailsView( UserProfileView(
state = state, state = state,
modifier = modifier, modifier = modifier,
goBack = this::navigateUp, goBack = this::navigateUp,

View file

@ -28,7 +28,10 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.createroom.api.StartDMAction
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog import io.element.android.features.userprofile.shared.UserProfileEvents
import io.element.android.features.userprofile.shared.UserProfilePresenterHelper
import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.features.userprofile.shared.UserProfileState.ConfirmationDialog
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -39,7 +42,6 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -51,13 +53,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
private val client: MatrixClient, private val client: MatrixClient,
private val room: MatrixRoom, private val room: MatrixRoom,
private val startDMAction: StartDMAction, private val startDMAction: StartDMAction,
) : Presenter<RoomMemberDetailsState> { ) : Presenter<UserProfileState> {
interface Factory { interface Factory {
fun create(roomMemberId: UserId): RoomMemberDetailsPresenter fun create(roomMemberId: UserId): RoomMemberDetailsPresenter
} }
private val userProfilePresenterHelper = UserProfilePresenterHelper(
userId = roomMemberId,
client = client,
)
@Composable @Composable
override fun present(): RoomMemberDetailsState { override fun present(): UserProfileState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) } var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
val roomMember by room.getRoomMemberAsState(roomMemberId) val roomMember by room.getRoomMemberAsState(roomMemberId)
@ -81,34 +88,34 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
} }
} }
fun handleEvents(event: RoomMemberDetailsEvents) { fun handleEvents(event: UserProfileEvents) {
when (event) { when (event) {
is RoomMemberDetailsEvents.BlockUser -> { is UserProfileEvents.BlockUser -> {
if (event.needsConfirmation) { if (event.needsConfirmation) {
confirmationDialog = ConfirmationDialog.Block confirmationDialog = ConfirmationDialog.Block
} else { } else {
confirmationDialog = null confirmationDialog = null
coroutineScope.blockUser(roomMemberId, isBlocked) userProfilePresenterHelper.blockUser(coroutineScope, isBlocked)
} }
} }
is RoomMemberDetailsEvents.UnblockUser -> { is UserProfileEvents.UnblockUser -> {
if (event.needsConfirmation) { if (event.needsConfirmation) {
confirmationDialog = ConfirmationDialog.Unblock confirmationDialog = ConfirmationDialog.Unblock
} else { } else {
confirmationDialog = null confirmationDialog = null
coroutineScope.unblockUser(roomMemberId, isBlocked) userProfilePresenterHelper.unblockUser(coroutineScope, isBlocked)
} }
} }
RoomMemberDetailsEvents.ClearConfirmationDialog -> confirmationDialog = null UserProfileEvents.ClearConfirmationDialog -> confirmationDialog = null
RoomMemberDetailsEvents.ClearBlockUserError -> { UserProfileEvents.ClearBlockUserError -> {
isBlocked.value = AsyncData.Success(isBlocked.value.dataOrNull().orFalse()) isBlocked.value = AsyncData.Success(isBlocked.value.dataOrNull().orFalse())
} }
RoomMemberDetailsEvents.StartDM -> { UserProfileEvents.StartDM -> {
coroutineScope.launch { coroutineScope.launch {
startDMAction.execute(roomMemberId, startDmActionState) startDMAction.execute(roomMemberId, startDmActionState)
} }
} }
RoomMemberDetailsEvents.ClearStartDMState -> { UserProfileEvents.ClearStartDMState -> {
startDmActionState.value = AsyncAction.Uninitialized startDmActionState.value = AsyncAction.Uninitialized
} }
} }
@ -144,8 +151,8 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
) )
} }
return RoomMemberDetailsState( return UserProfileState(
userId = roomMemberId.value, userId = roomMemberId,
userName = userName, userName = userName,
avatarUrl = userAvatar, avatarUrl = userAvatar,
isBlocked = isBlocked.value, isBlocked = isBlocked.value,
@ -155,22 +162,4 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState<AsyncData<Boolean>>) = launch {
isBlockedState.value = AsyncData.Loading(false)
client.ignoreUser(userId)
.onFailure {
isBlockedState.value = AsyncData.Failure(it, false)
}
// Note: on success, ignoredUserList will be updated.
}
private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState<AsyncData<Boolean>>) = launch {
isBlockedState.value = AsyncData.Loading(true)
client.unignoreUser(userId)
.onFailure {
isBlockedState.value = AsyncData.Failure(it, true)
}
// Note: on success, ignoredUserList will be updated.
}
} }

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Заблакіраваць"</string>
<string name="screen_dm_details_block_alert_description">"Заблакіраваныя карыстальнікі не змогуць адпраўляць вам паведамленні, і ўсе іх паведамленні будуць схаваны. Вы можаце разблакіраваць іх у любы час."</string>
<string name="screen_dm_details_block_user">"Заблакіраваць карыстальніка"</string>
<string name="screen_dm_details_unblock_alert_action">"Разблакіраваць"</string>
<string name="screen_dm_details_unblock_alert_description">"Вы зноў зможаце ўбачыць усе паведамленні."</string>
<string name="screen_dm_details_unblock_user">"Разблакіраваць карыстальніка"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Пры абнаўленні налад апавяшчэнняў адбылася памылка."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"Пры абнаўленні налад апавяшчэнняў адбылася памылка."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш хатні сервер не падтрымлівае гэтую опцыю ў зашыфраваных пакоях, вы можаце не атрымаць апавяшчэнне ў некаторых пакоях."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Ваш хатні сервер не падтрымлівае гэтую опцыю ў зашыфраваных пакоях, вы можаце не атрымаць апавяшчэнне ў некаторых пакоях."</string>
<string name="screen_polls_history_title">"Апытанні"</string> <string name="screen_polls_history_title">"Апытанні"</string>
@ -117,5 +111,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Ролі"</string> <string name="screen_room_roles_and_permissions_roles_header">"Ролі"</string>
<string name="screen_room_roles_and_permissions_room_details">"Дэталі пакоя"</string> <string name="screen_room_roles_and_permissions_room_details">"Дэталі пакоя"</string>
<string name="screen_room_roles_and_permissions_title">"Ролі і дазволы"</string> <string name="screen_room_roles_and_permissions_title">"Ролі і дазволы"</string>
<string name="screen_start_chat_error_starting_chat">"Пры спробе пачаць чат адбылася памылка"</string>
</resources> </resources>

View file

@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Блокиране"</string>
<string name="screen_dm_details_block_user">"Блокиране на потребителя"</string>
<string name="screen_dm_details_unblock_alert_action">"Отблокиране"</string>
<string name="screen_dm_details_unblock_user">"Отблокиране на потребителя"</string>
<string name="screen_polls_history_title">"Анкети"</string> <string name="screen_polls_history_title">"Анкети"</string>
<string name="screen_room_change_role_section_users">"Членове"</string> <string name="screen_room_change_role_section_users">"Членове"</string>
<string name="screen_room_details_add_topic_title">"Добавяне на тема"</string> <string name="screen_room_details_add_topic_title">"Добавяне на тема"</string>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Zablokovat"</string>
<string name="screen_dm_details_block_alert_description">"Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny jejich zprávy budou skryty. Můžete je kdykoli odblokovat."</string>
<string name="screen_dm_details_block_user">"Zablokovat uživatele"</string>
<string name="screen_dm_details_unblock_alert_action">"Odblokovat"</string>
<string name="screen_dm_details_unblock_alert_description">"Znovu uvidíte všechny zprávy od nich."</string>
<string name="screen_dm_details_unblock_user">"Odblokovat uživatele"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Při aktualizaci nastavení oznámení došlo k chybě."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"Při aktualizaci nastavení oznámení došlo k chybě."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Váš domovský server tuto možnost v zašifrovaných místnostech nepodporuje, v některých místnostech nemusíte být upozorněni."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Váš domovský server tuto možnost v zašifrovaných místnostech nepodporuje, v některých místnostech nemusíte být upozorněni."</string>
<string name="screen_polls_history_title">"Hlasování"</string> <string name="screen_polls_history_title">"Hlasování"</string>
@ -117,5 +111,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Role"</string> <string name="screen_room_roles_and_permissions_roles_header">"Role"</string>
<string name="screen_room_roles_and_permissions_room_details">"Podrobnosti místnosti"</string> <string name="screen_room_roles_and_permissions_room_details">"Podrobnosti místnosti"</string>
<string name="screen_room_roles_and_permissions_title">"Role a oprávnění"</string> <string name="screen_room_roles_and_permissions_title">"Role a oprávnění"</string>
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blockieren"</string>
<string name="screen_dm_details_block_alert_description">"Blockierte Benutzer können Dir keine Nachrichten senden und alle ihre alten Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden."</string>
<string name="screen_dm_details_block_user">"Benutzer blockieren"</string>
<string name="screen_dm_details_unblock_alert_action">"Blockierung aufheben"</string>
<string name="screen_dm_details_unblock_alert_description">"Der Nutzer kann dir wieder Nachrichten senden &amp; alle Nachrichten des Nutzers werden wieder angezeigt."</string>
<string name="screen_dm_details_unblock_user">"Blockierung aufheben"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Dein Homeserver unterstützt diese Option in verschlüsselten Chat nicht. In einigen Chats wirst du möglicherweise nicht benachrichtigt."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Dein Homeserver unterstützt diese Option in verschlüsselten Chat nicht. In einigen Chats wirst du möglicherweise nicht benachrichtigt."</string>
<string name="screen_polls_history_title">"Umfragen"</string> <string name="screen_polls_history_title">"Umfragen"</string>
@ -20,15 +14,15 @@
<string name="screen_room_change_permissions_remove_people">"Personen entfernen"</string> <string name="screen_room_change_permissions_remove_people">"Personen entfernen"</string>
<string name="screen_room_change_permissions_room_avatar">"Avatar ändern"</string> <string name="screen_room_change_permissions_room_avatar">"Avatar ändern"</string>
<string name="screen_room_change_permissions_room_details">"Raum-Details anpassen"</string> <string name="screen_room_change_permissions_room_details">"Raum-Details anpassen"</string>
<string name="screen_room_change_permissions_room_name">"Raum-Name ändern"</string> <string name="screen_room_change_permissions_room_name">"Raumname ändern"</string>
<string name="screen_room_change_permissions_room_topic">"Raum-Thema ändern"</string> <string name="screen_room_change_permissions_room_topic">"Raumthema ändern"</string>
<string name="screen_room_change_permissions_send_messages">"Nachrichten senden"</string> <string name="screen_room_change_permissions_send_messages">"Nachrichten senden"</string>
<string name="screen_room_change_role_administrators_title">"Admins bearbeiten"</string> <string name="screen_room_change_role_administrators_title">"Admins bearbeiten"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Du kannst diese Aktion nicht mehr rückgängig machen. Du vergibst dieselbe Rolle, wie auch Du sie hast."</string> <string name="screen_room_change_role_confirm_add_admin_description">"Du kannst diese Aktion nicht mehr rückgängig machen. Du vergibst dieselbe Rolle, die du auch hast."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Als Administrator hinzufügen?"</string> <string name="screen_room_change_role_confirm_add_admin_title">"Als Administrator hinzufügen?"</string>
<string name="screen_room_change_role_confirm_demote_self_action">"Zurückstufen"</string> <string name="screen_room_change_role_confirm_demote_self_action">"Zurückstufen"</string>
<string name="screen_room_change_role_confirm_demote_self_description">"Du stufst dich selbst herab. Diese Änderung kann nicht rückgängig gemacht werden. Wenn du der letzte Benutzer mit dieser Rolle bist, ist es nicht möglich, diese Rolle wiederzuerlangen."</string> <string name="screen_room_change_role_confirm_demote_self_description">"Du stufst dich selbst herab. Diese Änderung kann nicht rückgängig gemacht werden. Wenn du der letzte Benutzer mit dieser Rolle bist, ist es nicht möglich, diese Rolle wiederzuerlangen."</string>
<string name="screen_room_change_role_confirm_demote_self_title">"Möchtest Du Dich selbst herabstufen?"</string> <string name="screen_room_change_role_confirm_demote_self_title">"Möchtest du dich selbst herabstufen?"</string>
<string name="screen_room_change_role_invited_member_name">"%1$s (Ausstehend)"</string> <string name="screen_room_change_role_invited_member_name">"%1$s (Ausstehend)"</string>
<string name="screen_room_change_role_invited_member_name_android">"(Ausstehend)"</string> <string name="screen_room_change_role_invited_member_name_android">"(Ausstehend)"</string>
<string name="screen_room_change_role_moderators_admin_section_footer">"Administratoren haben automatisch Moderatorenrechte"</string> <string name="screen_room_change_role_moderators_admin_section_footer">"Administratoren haben automatisch Moderatorenrechte"</string>
@ -56,7 +50,7 @@
<string name="screen_room_details_notification_mode_default">"Standard"</string> <string name="screen_room_details_notification_mode_default">"Standard"</string>
<string name="screen_room_details_notification_title">"Benachrichtigungen"</string> <string name="screen_room_details_notification_title">"Benachrichtigungen"</string>
<string name="screen_room_details_roles_and_permissions">"Rollen und Berechtigungen"</string> <string name="screen_room_details_roles_and_permissions">"Rollen und Berechtigungen"</string>
<string name="screen_room_details_room_name_label">"Raum-Name"</string> <string name="screen_room_details_room_name_label">"Raumname"</string>
<string name="screen_room_details_security_title">"Sicherheit"</string> <string name="screen_room_details_security_title">"Sicherheit"</string>
<string name="screen_room_details_share_room_title">"Teilen"</string> <string name="screen_room_details_share_room_title">"Teilen"</string>
<string name="screen_room_details_title">"Informationen"</string> <string name="screen_room_details_title">"Informationen"</string>
@ -116,5 +110,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Rollen"</string> <string name="screen_room_roles_and_permissions_roles_header">"Rollen"</string>
<string name="screen_room_roles_and_permissions_room_details">"Raum-Details anpassen"</string> <string name="screen_room_roles_and_permissions_room_details">"Raum-Details anpassen"</string>
<string name="screen_room_roles_and_permissions_title">"Rollen und Berechtigungen"</string> <string name="screen_room_roles_and_permissions_title">"Rollen und Berechtigungen"</string>
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Bloquear"</string>
<string name="screen_dm_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y todos sus mensajes se ocultarán. Puedes desbloquearlos cuando quieras."</string>
<string name="screen_dm_details_block_user">"Bloquear usuario"</string>
<string name="screen_dm_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_dm_details_unblock_alert_description">"Podrás ver todos sus mensajes de nuevo."</string>
<string name="screen_dm_details_unblock_user">"Desbloquear usuario"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Se ha producido un error al actualizar la configuración de notificaciones."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"Se ha producido un error al actualizar la configuración de notificaciones."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Tu servidor principal no admite esta opción en salas cifradas, puede que no recibas notificaciones en algunas salas."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Tu servidor principal no admite esta opción en salas cifradas, puede que no recibas notificaciones en algunas salas."</string>
<string name="screen_polls_history_title">"Encuestas"</string> <string name="screen_polls_history_title">"Encuestas"</string>
@ -51,5 +45,4 @@
<string name="screen_room_notification_settings_mode_all_messages">"Todos los mensajes"</string> <string name="screen_room_notification_settings_mode_all_messages">"Todos los mensajes"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Únicamente Menciones y Palabras clave"</string> <string name="screen_room_notification_settings_mode_mentions_and_keywords">"Únicamente Menciones y Palabras clave"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"En esta sala, notificarme por"</string> <string name="screen_room_notification_settings_room_custom_settings_title">"En esta sala, notificarme por"</string>
<string name="screen_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Bloquer"</string>
<string name="screen_dm_details_block_alert_description">"Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment."</string>
<string name="screen_dm_details_block_user">"Bloquer lutilisateur"</string>
<string name="screen_dm_details_unblock_alert_action">"Débloquer"</string>
<string name="screen_dm_details_unblock_alert_description">"Vous pourrez à nouveau voir tous ses messages."</string>
<string name="screen_dm_details_unblock_user">"Débloquer lutilisateur"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Une erreur sest produite lors de la mise à jour du paramètre de notification."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"Une erreur sest produite lors de la mise à jour du paramètre de notification."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Votre serveur daccueil ne supporte pas cette option pour les salons chiffrés, vous pourriez ne pas être notifié(e) dans certains salons."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Votre serveur daccueil ne supporte pas cette option pour les salons chiffrés, vous pourriez ne pas être notifié(e) dans certains salons."</string>
<string name="screen_polls_history_title">"Sondages"</string> <string name="screen_polls_history_title">"Sondages"</string>
@ -116,5 +110,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Rôles"</string> <string name="screen_room_roles_and_permissions_roles_header">"Rôles"</string>
<string name="screen_room_roles_and_permissions_room_details">"Détails du salon"</string> <string name="screen_room_roles_and_permissions_room_details">"Détails du salon"</string>
<string name="screen_room_roles_and_permissions_title">"Rôles et autorisations"</string> <string name="screen_room_roles_and_permissions_title">"Rôles et autorisations"</string>
<string name="screen_start_chat_error_starting_chat">"Une erreur sest produite lors de la tentative de création de la discussion"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Letiltás"</string>
<string name="screen_dm_details_block_alert_description">"A letiltott felhasználók nem fognak tudni üzeneteket küldeni, és az összes üzenetük rejtve lesz. Bármikor feloldhatja a letiltásukat."</string>
<string name="screen_dm_details_block_user">"Felhasználó letiltása"</string>
<string name="screen_dm_details_unblock_alert_action">"Letiltás feloldása"</string>
<string name="screen_dm_details_unblock_alert_description">"Újra láthatja az összes üzenetét."</string>
<string name="screen_dm_details_unblock_user">"Felhasználó kitiltásának feloldása"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Hiba történt az értesítési beállítás frissítésekor."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"Hiba történt az értesítési beállítás frissítésekor."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, előfordulhat, hogy egyes szobákban nem kap értesítést."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"A Matrix-kiszolgálója nem támogatja ezt a beállítást a titkosított szobákban, előfordulhat, hogy egyes szobákban nem kap értesítést."</string>
<string name="screen_polls_history_title">"Szavazások"</string> <string name="screen_polls_history_title">"Szavazások"</string>
@ -116,5 +110,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Szerepkörök"</string> <string name="screen_room_roles_and_permissions_roles_header">"Szerepkörök"</string>
<string name="screen_room_roles_and_permissions_room_details">"Szoba részletei"</string> <string name="screen_room_roles_and_permissions_room_details">"Szoba részletei"</string>
<string name="screen_room_roles_and_permissions_title">"Szerepkörök és jogosultságok"</string> <string name="screen_room_roles_and_permissions_title">"Szerepkörök és jogosultságok"</string>
<string name="screen_start_chat_error_starting_chat">"Hiba történt a csevegés indításakor"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blokir"</string>
<string name="screen_dm_details_block_alert_description">"Pengguna yang diblokir tidak akan dapat mengirim Anda pesan dan semua pesan mereka akan disembunyikan. Anda dapat membuka blokirnya kapan saja."</string>
<string name="screen_dm_details_block_user">"Blokir pengguna"</string>
<string name="screen_dm_details_unblock_alert_action">"Buka blokir"</string>
<string name="screen_dm_details_unblock_alert_description">"Anda akan dapat melihat semua pesan dari mereka lagi."</string>
<string name="screen_dm_details_unblock_user">"Buka blokir pengguna"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Terjadi kesalahan saat memperbarui pengaturan pemberitahuan."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"Terjadi kesalahan saat memperbarui pengaturan pemberitahuan."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Homeserver Anda tidak mendukung opsi ini dalam ruangan terenkripsi, Anda mungkin tidak diberi tahu dalam beberapa ruangan."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Homeserver Anda tidak mendukung opsi ini dalam ruangan terenkripsi, Anda mungkin tidak diberi tahu dalam beberapa ruangan."</string>
<string name="screen_polls_history_title">"Pemungutan suara"</string> <string name="screen_polls_history_title">"Pemungutan suara"</string>
@ -115,5 +109,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Peran"</string> <string name="screen_room_roles_and_permissions_roles_header">"Peran"</string>
<string name="screen_room_roles_and_permissions_room_details">"Detail ruangan"</string> <string name="screen_room_roles_and_permissions_room_details">"Detail ruangan"</string>
<string name="screen_room_roles_and_permissions_title">"Peran dan perizinan"</string> <string name="screen_room_roles_and_permissions_title">"Peran dan perizinan"</string>
<string name="screen_start_chat_error_starting_chat">"Terjadi kesalahan saat mencoba memulai obrolan"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blocca"</string>
<string name="screen_dm_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti quelli già ricevuti saranno nascosti. Puoi sbloccarli in qualsiasi momento."</string>
<string name="screen_dm_details_block_user">"Blocca utente"</string>
<string name="screen_dm_details_unblock_alert_action">"Sblocca"</string>
<string name="screen_dm_details_unblock_alert_description">"Potrai vedere di nuovo tutti i suoi messaggi."</string>
<string name="screen_dm_details_unblock_user">"Sblocca utente"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Si è verificato un errore durante l\'aggiornamento delle impostazioni di notifica."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"Si è verificato un errore durante l\'aggiornamento delle impostazioni di notifica."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Il tuo homeserver non supporta questa opzione nelle stanze crifrate, quindi potresti non ricevere notifiche in alcune stanze."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Il tuo homeserver non supporta questa opzione nelle stanze crifrate, quindi potresti non ricevere notifiche in alcune stanze."</string>
<string name="screen_polls_history_title">"Sondaggi"</string> <string name="screen_polls_history_title">"Sondaggi"</string>
@ -113,5 +107,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Ruoli"</string> <string name="screen_room_roles_and_permissions_roles_header">"Ruoli"</string>
<string name="screen_room_roles_and_permissions_room_details">"Dettagli della stanza"</string> <string name="screen_room_roles_and_permissions_room_details">"Dettagli della stanza"</string>
<string name="screen_room_roles_and_permissions_title">"Ruoli e autorizzazioni"</string> <string name="screen_room_roles_and_permissions_title">"Ruoli e autorizzazioni"</string>
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blocați"</string>
<string name="screen_dm_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
<string name="screen_dm_details_block_user">"Blocați utilizatorul"</string>
<string name="screen_dm_details_unblock_alert_action">"Deblocați"</string>
<string name="screen_dm_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
<string name="screen_dm_details_unblock_user">"Deblocați utilizatorul"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"A apărut o eroare în timpul actualizării setărilor pentru notificari."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"A apărut o eroare în timpul actualizării setărilor pentru notificari."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Serverul dumneavoastră nu acceptă această opțiune în camerele criptate, este posibil să nu primiți notificări în unele camere."</string>
<string name="screen_polls_history_title">"Sondaje"</string> <string name="screen_polls_history_title">"Sondaje"</string>
@ -63,5 +57,4 @@
<string name="screen_room_notification_settings_mode_all_messages">"Toate mesajele"</string> <string name="screen_room_notification_settings_mode_all_messages">"Toate mesajele"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Numai mențiuni și cuvinte cheie"</string> <string name="screen_room_notification_settings_mode_mentions_and_keywords">"Numai mențiuni și cuvinte cheie"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"În această cameră, anunțați-mă pentru"</string> <string name="screen_room_notification_settings_room_custom_settings_title">"În această cameră, anunțați-mă pentru"</string>
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Заблокировать"</string>
<string name="screen_dm_details_block_alert_description">"Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."</string>
<string name="screen_dm_details_block_user">"Заблокировать пользователя"</string>
<string name="screen_dm_details_unblock_alert_action">"Разблокировать"</string>
<string name="screen_dm_details_unblock_alert_description">"Вы снова сможете увидеть все сообщения."</string>
<string name="screen_dm_details_unblock_user">"Разблокировать пользователя"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"При обновлении настроек уведомления произошла ошибка."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"При обновлении настроек уведомления произошла ошибка."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, в некоторых комнатах вы можете не получать уведомления."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, в некоторых комнатах вы можете не получать уведомления."</string>
<string name="screen_polls_history_title">"Опросы"</string> <string name="screen_polls_history_title">"Опросы"</string>
@ -117,5 +111,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Роли"</string> <string name="screen_room_roles_and_permissions_roles_header">"Роли"</string>
<string name="screen_room_roles_and_permissions_room_details">"Информация о комнате"</string> <string name="screen_room_roles_and_permissions_room_details">"Информация о комнате"</string>
<string name="screen_room_roles_and_permissions_title">"Роли и разрешения"</string> <string name="screen_room_roles_and_permissions_title">"Роли и разрешения"</string>
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при попытке открытия комнаты"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Zablokovať"</string>
<string name="screen_dm_details_block_alert_description">"Blokovaní používatelia vám nebudú môcť posielať správy a všetky ich správy budú skryté. Môžete ich kedykoľvek odblokovať."</string>
<string name="screen_dm_details_block_user">"Zablokovať používateľa"</string>
<string name="screen_dm_details_unblock_alert_action">"Odblokovať"</string>
<string name="screen_dm_details_unblock_alert_description">"Všetky správy od nich budete môcť opäť vidieť."</string>
<string name="screen_dm_details_unblock_user">"Odblokovať používateľa"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Pri aktualizácii nastavenia oznámenia došlo k chybe."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"Pri aktualizácii nastavenia oznámenia došlo k chybe."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v niektorých miestnostiach nemusíte dostať upozornenie."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v niektorých miestnostiach nemusíte dostať upozornenie."</string>
<string name="screen_polls_history_title">"Ankety"</string> <string name="screen_polls_history_title">"Ankety"</string>
@ -117,5 +111,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Roly"</string> <string name="screen_room_roles_and_permissions_roles_header">"Roly"</string>
<string name="screen_room_roles_and_permissions_room_details">"Podrobnosti o miestnosti"</string> <string name="screen_room_roles_and_permissions_room_details">"Podrobnosti o miestnosti"</string>
<string name="screen_room_roles_and_permissions_title">"Roly a povolenia"</string> <string name="screen_room_roles_and_permissions_title">"Roly a povolenia"</string>
<string name="screen_start_chat_error_starting_chat">"Pri pokuse o spustenie konverzácie sa vyskytla chyba"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blockera"</string>
<string name="screen_dm_details_block_alert_description">"Blockerade användare kommer inte att kunna skicka meddelanden till dig och alla deras meddelanden kommer att döljas. Du kan avblockera dem när som helst."</string>
<string name="screen_dm_details_block_user">"Blockera användare"</string>
<string name="screen_dm_details_unblock_alert_action">"Avblockera"</string>
<string name="screen_dm_details_unblock_alert_description">"Du kommer att kunna se alla meddelanden från dem igen."</string>
<string name="screen_dm_details_unblock_user">"Avblockera användare"</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_edit_failed_updating_default_mode">"Ett fel uppstod vid uppdatering av aviseringsinställningen."</string>
<string name="screen_room_change_permissions_everyone">"Alla"</string> <string name="screen_room_change_permissions_everyone">"Alla"</string>
<string name="screen_room_details_add_topic_title">"Lägg till ämne"</string> <string name="screen_room_details_add_topic_title">"Lägg till ämne"</string>
@ -48,5 +42,4 @@
<string name="screen_room_notification_settings_mode_all_messages">"Alla meddelanden"</string> <string name="screen_room_notification_settings_mode_all_messages">"Alla meddelanden"</string>
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Endast omnämnanden och nyckelord"</string> <string name="screen_room_notification_settings_mode_mentions_and_keywords">"Endast omnämnanden och nyckelord"</string>
<string name="screen_room_notification_settings_room_custom_settings_title">"I det här rummet, meddela mig för"</string> <string name="screen_room_notification_settings_room_custom_settings_title">"I det här rummet, meddela mig för"</string>
<string name="screen_start_chat_error_starting_chat">"Ett fel uppstod när du försökte starta en chatt"</string>
</resources> </resources>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Заблокувати"</string>
<string name="screen_dm_details_block_alert_description">"Заблоковані користувачі не зможуть надсилати Вам повідомлення, і всі їхні повідомлення будуть приховані. Ви можете розблокувати їх у будь-який час."</string>
<string name="screen_dm_details_block_user">"Заблокувати користувача"</string>
<string name="screen_dm_details_unblock_alert_action">"Розблокувати"</string>
<string name="screen_dm_details_unblock_alert_description">"Ви знову зможете бачити всі повідомлення від них."</string>
<string name="screen_dm_details_unblock_user">"Розблокувати користувача"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Під час оновлення налаштувань сповіщень сталася помилка."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"Під час оновлення налаштувань сповіщень сталася помилка."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашній сервер не підтримує цю опцію в зашифрованих кімнатах, ви можете не отримати сповіщення в деяких кімнатах."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашній сервер не підтримує цю опцію в зашифрованих кімнатах, ви можете не отримати сповіщення в деяких кімнатах."</string>
<string name="screen_polls_history_title">"Опитування"</string> <string name="screen_polls_history_title">"Опитування"</string>
@ -113,5 +107,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Ролі"</string> <string name="screen_room_roles_and_permissions_roles_header">"Ролі"</string>
<string name="screen_room_roles_and_permissions_room_details">"Деталі кімнати"</string> <string name="screen_room_roles_and_permissions_room_details">"Деталі кімнати"</string>
<string name="screen_room_roles_and_permissions_title">"Ролі та дозволи"</string> <string name="screen_room_roles_and_permissions_title">"Ролі та дозволи"</string>
<string name="screen_start_chat_error_starting_chat">"Під час спроби почати чат сталася помилка"</string>
</resources> </resources>

View file

@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"封鎖"</string>
<string name="screen_dm_details_block_user">"封鎖使用者"</string>
<string name="screen_dm_details_unblock_alert_action">"解除封鎖"</string>
<string name="screen_dm_details_unblock_user">"解除封鎖使用者"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"更新通知設定時發生錯誤。"</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"更新通知設定時發生錯誤。"</string>
<string name="screen_polls_history_title">"所有投票"</string> <string name="screen_polls_history_title">"所有投票"</string>
<string name="screen_room_change_permissions_everyone">"所有人"</string> <string name="screen_room_change_permissions_everyone">"所有人"</string>

View file

@ -1,11 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Block"</string>
<string name="screen_dm_details_block_alert_description">"Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime."</string>
<string name="screen_dm_details_block_user">"Block user"</string>
<string name="screen_dm_details_unblock_alert_action">"Unblock"</string>
<string name="screen_dm_details_unblock_alert_description">"You\'ll be able to see all messages from them again."</string>
<string name="screen_dm_details_unblock_user">"Unblock user"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string> <string name="screen_notification_settings_edit_failed_updating_default_mode">"An error occurred while updating the notification setting."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."</string> <string name="screen_notification_settings_mentions_only_disclaimer">"Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."</string>
<string name="screen_polls_history_title">"Polls"</string> <string name="screen_polls_history_title">"Polls"</string>
@ -116,5 +110,4 @@
<string name="screen_room_roles_and_permissions_roles_header">"Roles"</string> <string name="screen_room_roles_and_permissions_roles_header">"Roles"</string>
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string> <string name="screen_room_roles_and_permissions_room_details">"Room details"</string>
<string name="screen_room_roles_and_permissions_title">"Roles and permissions"</string> <string name="screen_room_roles_and_permissions_title">"Roles and permissions"</string>
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
</resources> </resources>

View file

@ -25,9 +25,9 @@ import io.element.android.features.createroom.api.StartDMAction
import io.element.android.features.createroom.test.FakeStartDMAction import io.element.android.features.createroom.test.FakeStartDMAction
import io.element.android.features.roomdetails.aMatrixRoom import io.element.android.features.roomdetails.aMatrixRoom
import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.userprofile.shared.UserProfileEvents
import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -66,7 +66,7 @@ class RoomMemberDetailsPresenterTests {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.userId).isEqualTo(roomMember.userId.value) assertThat(initialState.userId).isEqualTo(roomMember.userId)
assertThat(initialState.userName).isEqualTo(roomMember.displayName) assertThat(initialState.userName).isEqualTo(roomMember.displayName)
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(roomMember.isIgnored)) assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(roomMember.isIgnored))
@ -157,12 +157,12 @@ class RoomMemberDetailsPresenterTests {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = true))
val dialogState = awaitItem() val dialogState = awaitItem()
assertThat(dialogState.displayConfirmationDialog).isEqualTo(RoomMemberDetailsState.ConfirmationDialog.Block) assertThat(dialogState.displayConfirmationDialog).isEqualTo(UserProfileState.ConfirmationDialog.Block)
dialogState.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
assertThat(awaitItem().displayConfirmationDialog).isNull() assertThat(awaitItem().displayConfirmationDialog).isNull()
ensureAllEventsConsumed() ensureAllEventsConsumed()
@ -181,12 +181,12 @@ class RoomMemberDetailsPresenterTests {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue() assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf(roomMember.userId)) client.emitIgnoreUserList(listOf(roomMember.userId))
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue() assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue() assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf()) client.emitIgnoreUserList(listOf())
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse() assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
@ -202,12 +202,12 @@ class RoomMemberDetailsPresenterTests {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue() assertThat(awaitItem().isBlocked.isLoading()).isTrue()
val errorState = awaitItem() val errorState = awaitItem()
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE) assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
// Clear error // Clear error
initialState.eventSink(RoomMemberDetailsEvents.ClearBlockUserError) initialState.eventSink(UserProfileEvents.ClearBlockUserError)
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(false)) assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(false))
} }
} }
@ -221,12 +221,12 @@ class RoomMemberDetailsPresenterTests {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue() assertThat(awaitItem().isBlocked.isLoading()).isTrue()
val errorState = awaitItem() val errorState = awaitItem()
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE) assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
// Clear error // Clear error
initialState.eventSink(RoomMemberDetailsEvents.ClearBlockUserError) initialState.eventSink(UserProfileEvents.ClearBlockUserError)
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true)) assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true))
} }
} }
@ -238,12 +238,12 @@ class RoomMemberDetailsPresenterTests {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true))
val dialogState = awaitItem() val dialogState = awaitItem()
assertThat(dialogState.displayConfirmationDialog).isEqualTo(RoomMemberDetailsState.ConfirmationDialog.Unblock) assertThat(dialogState.displayConfirmationDialog).isEqualTo(UserProfileState.ConfirmationDialog.Unblock)
dialogState.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
assertThat(awaitItem().displayConfirmationDialog).isNull() assertThat(awaitItem().displayConfirmationDialog).isNull()
ensureAllEventsConsumed() ensureAllEventsConsumed()
@ -264,18 +264,18 @@ class RoomMemberDetailsPresenterTests {
// Failure // Failure
startDMAction.givenExecuteResult(startDMFailureResult) startDMAction.givenExecuteResult(startDMFailureResult)
initialState.eventSink(RoomMemberDetailsEvents.StartDM) initialState.eventSink(UserProfileEvents.StartDM)
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.startDmActionState).isEqualTo(startDMFailureResult) assertThat(state.startDmActionState).isEqualTo(startDMFailureResult)
state.eventSink(RoomMemberDetailsEvents.ClearStartDMState) state.eventSink(UserProfileEvents.ClearStartDMState)
} }
// Success // Success
startDMAction.givenExecuteResult(startDMSuccessResult) startDMAction.givenExecuteResult(startDMSuccessResult)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.startDmActionState).isEqualTo(AsyncAction.Uninitialized) assertThat(state.startDmActionState).isEqualTo(AsyncAction.Uninitialized)
state.eventSink(RoomMemberDetailsEvents.StartDM) state.eventSink(UserProfileEvents.StartDM)
} }
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
awaitItem().also { state -> awaitItem().also { state ->

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="confirm_recovery_key_banner_message">"Din chattsäkerhetskopia är för närvarande inte synkroniserad. Du måste ange din återställningsnyckel för att behålla åtkomsten till din chattsäkerhetskopia."</string>
<string name="confirm_recovery_key_banner_title">"Ange din återställningsnyckel"</string>
<string name="screen_invites_decline_chat_message">"Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?"</string> <string name="screen_invites_decline_chat_message">"Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?"</string>
<string name="screen_invites_decline_chat_title">"Avböj inbjudan"</string> <string name="screen_invites_decline_chat_title">"Avböj inbjudan"</string>
<string name="screen_invites_decline_direct_chat_message">"Är du säker på att du vill avböja denna privata chatt med %1$s?"</string> <string name="screen_invites_decline_direct_chat_message">"Är du säker på att du vill avböja denna privata chatt med %1$s?"</string>

View file

@ -8,6 +8,8 @@
<string name="screen_roomlist_a11y_create_message">"建立新的對話或聊天室"</string> <string name="screen_roomlist_a11y_create_message">"建立新的對話或聊天室"</string>
<string name="screen_roomlist_filter_favourites">"我的最愛"</string> <string name="screen_roomlist_filter_favourites">"我的最愛"</string>
<string name="screen_roomlist_filter_people">"夥伴"</string> <string name="screen_roomlist_filter_people">"夥伴"</string>
<string name="screen_roomlist_filter_rooms">"聊天室"</string>
<string name="screen_roomlist_filter_unreads">"未讀"</string>
<string name="screen_roomlist_main_space_title">"所有聊天室"</string> <string name="screen_roomlist_main_space_title">"所有聊天室"</string>
<string name="session_verification_banner_message">"您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。"</string> <string name="session_verification_banner_message">"您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。"</string>
<string name="session_verification_banner_title">"驗證這是您本人"</string> <string name="session_verification_banner_title">"驗證這是您本人"</string>

View file

@ -31,13 +31,14 @@
</string> </string>
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Ausschalten"</string> <string name="screen_key_backup_disable_confirmation_action_turn_off">"Ausschalten"</string>
<string name="screen_key_backup_disable_confirmation_description">"Du verlierst deine verschlüsselten Nachrichten, wenn du auf allen Geräten abgemeldet bist."</string> <string name="screen_key_backup_disable_confirmation_description">"Du verlierst deine verschlüsselten Nachrichten, wenn du auf allen Geräten abgemeldet bist."</string>
<string name="screen_key_backup_disable_confirmation_title">"Bist du sicher, dass du das Backup ausschalten willst?"</string> <string name="screen_key_backup_disable_confirmation_title">"Bist du sicher, dass du das Backup deaktivieren willst?"</string>
<string name="screen_key_backup_disable_description">"Wenn du das Backup ausschaltest, wird dein aktuelles Backup des Verschlüsselungsschlüssels entfernt und andere Sicherheitsfunktionen werden deaktiviert. In diesem Fall wirst du:"</string> <string name="screen_key_backup_disable_description">"Wenn du das Backup deaktivierst, wird dein aktuelles Backup des Verschlüsselungsschlüssels entfernt und andere Sicherheitsfunktionen werden deaktiviert.
<string name="screen_key_backup_disable_description_point_1">"Keine Historie für verschlüsselte Nachrichten auf neuen Geräten"</string> Das bedeutet:"</string>
<string name="screen_key_backup_disable_description_point_1">"Keine Historie für verschlüsselte Nachrichten auf neuen Geräten ."</string>
<string name="screen_key_backup_disable_description_point_2">"Du verlierst den Zugriff auf deine verschlüsselten Nachrichten, wenn du dich überall von %1$s abmeldest"</string> <string name="screen_key_backup_disable_description_point_2">"Du verlierst den Zugriff auf deine verschlüsselten Nachrichten, wenn du dich überall von %1$s abmeldest"</string>
<string name="screen_key_backup_disable_title">"Bist du sicher, dass du das Backup ausschalten willst?"</string> <string name="screen_key_backup_disable_title">"Bist du sicher, dass du das Backup deaktivieren willst?"</string>
<string name="screen_recovery_key_change_description">"Besorge dir einen neuen Wiederherstellungsschlüssel, wenn du deinen alten verloren hast. Nachdem du deinen Wiederherstellungsschlüssel geändert hast, funktioniert dein alter Schlüssel nicht mehr."</string> <string name="screen_recovery_key_change_description">"Hier kannst Du einen neuen Wiederherstellungsschlüssel erstellen. Nachdem Du einen neuen Wiederherstellungsschlüssel erstellt hast, funktioniert dein alter Schlüssel nicht mehr."</string>
<string name="screen_recovery_key_change_generate_key">"Erstelle einen neuen Wiederherstellungsschlüssel"</string> <string name="screen_recovery_key_change_generate_key">"Wiederherstellungsschlüssel erstellen"</string>
<string name="screen_recovery_key_change_generate_key_description">"Stelle sicher, dass du deinen Wiederherstellungsschlüssel an einem sicheren Ort aufbewahren kannst"</string> <string name="screen_recovery_key_change_generate_key_description">"Stelle sicher, dass du deinen Wiederherstellungsschlüssel an einem sicheren Ort aufbewahren kannst"</string>
<string name="screen_recovery_key_change_success">"Wiederherstellungsschlüssel geändert"</string> <string name="screen_recovery_key_change_success">"Wiederherstellungsschlüssel geändert"</string>
<string name="screen_recovery_key_change_title">"Wiederherstellungsschlüssel ändern?"</string> <string name="screen_recovery_key_change_title">"Wiederherstellungsschlüssel ändern?"</string>
@ -55,7 +56,7 @@
" oder Passcode" " oder Passcode"
</string> </string>
<string name="screen_recovery_key_confirm_key_placeholder">"Eingeben…"</string> <string name="screen_recovery_key_confirm_key_placeholder">"Eingeben…"</string>
<string name="screen_recovery_key_confirm_lost_recovery_key">"Hast Du Deinen Wiederherstellungschlüssel vergessen?"</string> <string name="screen_recovery_key_confirm_lost_recovery_key">"Hast du deinen Wiederherstellungschlüssel vergessen?"</string>
<string name="screen_recovery_key_confirm_success">"Wiederherstellungsschlüssel bestätigt"</string> <string name="screen_recovery_key_confirm_success">"Wiederherstellungsschlüssel bestätigt"</string>
<string name="screen_recovery_key_confirm_title">"Bitte Wiederherstellungsschlüssel eingeben"</string> <string name="screen_recovery_key_confirm_title">"Bitte Wiederherstellungsschlüssel eingeben"</string>
<string name="screen_recovery_key_copied_to_clipboard">"Wiederherstellungsschlüssel kopiert"</string> <string name="screen_recovery_key_copied_to_clipboard">"Wiederherstellungsschlüssel kopiert"</string>

View file

@ -22,10 +22,13 @@
<string name="screen_recovery_key_change_success">"Återställningsnyckel ändrad"</string> <string name="screen_recovery_key_change_success">"Återställningsnyckel ändrad"</string>
<string name="screen_recovery_key_change_title">"Byt återställningsnyckel?"</string> <string name="screen_recovery_key_change_title">"Byt återställningsnyckel?"</string>
<string name="screen_recovery_key_confirm_description">"Se till att ingen kan se den här skärmen"</string> <string name="screen_recovery_key_confirm_description">"Se till att ingen kan se den här skärmen"</string>
<string name="screen_recovery_key_confirm_error_title">"Felaktig återställningsnyckel"</string>
<string name="screen_recovery_key_confirm_key_description">"Om du har en säkerhetsnyckel eller säkerhetsfras så funkar den också."</string> <string name="screen_recovery_key_confirm_key_description">"Om du har en säkerhetsnyckel eller säkerhetsfras så funkar den också."</string>
<string name="screen_recovery_key_confirm_key_placeholder">"Ange …"</string> <string name="screen_recovery_key_confirm_key_placeholder">"Ange …"</string>
<string name="screen_recovery_key_confirm_success">"Återställningsnyckel bekräftad"</string> <string name="screen_recovery_key_confirm_success">"Återställningsnyckel bekräftad"</string>
<string name="screen_recovery_key_confirm_title">"Ange din återställningsnyckel"</string> <string name="screen_recovery_key_confirm_title">"Ange din återställningsnyckel"</string>
<string name="screen_recovery_key_copied_to_clipboard">"Kopierade återställningsnyckel"</string>
<string name="screen_recovery_key_generating_key">"Genererar …"</string>
<string name="screen_recovery_key_save_action">"Spara återställningsnyckeln"</string> <string name="screen_recovery_key_save_action">"Spara återställningsnyckeln"</string>
<string name="screen_recovery_key_save_description">"Skriv ner din återställningsnyckel någonstans säkert eller spara den i en lösenordshanterare."</string> <string name="screen_recovery_key_save_description">"Skriv ner din återställningsnyckel någonstans säkert eller spara den i en lösenordshanterare."</string>
<string name="screen_recovery_key_save_key_description">"Tryck för att kopiera återställningsnyckeln"</string> <string name="screen_recovery_key_save_key_description">"Tryck för att kopiera återställningsnyckeln"</string>

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.userprofile.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.userprofile.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
interface UserProfileEntryPoint : FeatureEntryPoint {
data class Params(val userId: UserId) : NodeInputs
interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId)
}
interface NodeBuilder {
fun params(params: Params): NodeBuilder
fun callback(callback: Callback): NodeBuilder
fun build(): Node
}
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.userprofile.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
anvil(projects.anvilcodegen)
implementation(projects.anvilannotations)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.mediaviewer.api)
api(projects.features.userprofile.api)
api(projects.features.userprofile.shared)
implementation(libs.coil.compose)
implementation(projects.features.createroom.api)
implementation(projects.services.analytics.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.mockk)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.createroom.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
ksp(libs.showkase.processor)
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.userprofile.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultUserProfileEntryPoint @Inject constructor() : UserProfileEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): UserProfileEntryPoint.NodeBuilder {
return object : UserProfileEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>()
override fun params(params: UserProfileEntryPoint.Params): UserProfileEntryPoint.NodeBuilder {
plugins += params
return this
}
override fun callback(callback: UserProfileEntryPoint.Callback): UserProfileEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun build(): Node {
return parentNode.createNode<UserProfileFlowNode>(buildContext, plugins)
}
}
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.userprofile.impl
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.userprofile.impl.root.UserProfileNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.userprofile.shared.avatar.AvatarPreviewNode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class)
class UserProfileFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : BaseFlowNode<UserProfileFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
sealed interface NavTarget : Parcelable {
@Parcelize
data object Root : NavTarget
@Parcelize
data class AvatarPreview(val name: String, val avatarUrl: String) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Root -> {
val callback = object : UserProfileNodeHelper.Callback {
override fun openAvatarPreview(username: String, avatarUrl: String) {
backstack.push(NavTarget.AvatarPreview(username, avatarUrl))
}
override fun onStartDM(roomId: RoomId) {
plugins<UserProfileEntryPoint.Callback>().forEach { it.onOpenRoom(roomId) }
}
}
val params = UserProfileNode.UserProfileInputs(userId = inputs<UserProfileEntryPoint.Params>().userId)
createNode<UserProfileNode>(buildContext, listOf(callback, params))
}
is NavTarget.AvatarPreview -> {
// We need to fake the MimeType here for the viewer to work.
val mimeType = MimeTypes.Images
val input = MediaViewerNode.Inputs(
mediaInfo = MediaInfo(
name = navTarget.name,
mimeType = mimeType,
formattedFileSize = "",
fileExtension = ""
),
mediaSource = MediaSource(url = navTarget.avatarUrl),
thumbnailSource = null,
canDownload = false,
canShare = false,
)
createNode<AvatarPreviewNode>(buildContext, listOf(input))
}
}
}
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.userprofile.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.features.createroom.api.StartDMAction
import io.element.android.features.userprofile.impl.root.UserProfilePresenter
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
@Module
@ContributesTo(SessionScope::class)
object UserProfileModule {
@Provides
fun provideUserProfilePresenterFactory(
matrixClient: MatrixClient,
startDMAction: StartDMAction,
): UserProfilePresenter.Factory {
return object : UserProfilePresenter.Factory {
override fun create(userId: UserId): UserProfilePresenter {
return UserProfilePresenter(userId, matrixClient, startDMAction)
}
}
}
}

View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.userprofile.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
import io.element.android.features.userprofile.shared.UserProfileView
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
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.permalink.PermalinkBuilder
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(SessionScope::class)
class UserProfileNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val analyticsService: AnalyticsService,
private val permalinkBuilder: PermalinkBuilder,
presenterFactory: UserProfilePresenter.Factory,
) : Node(buildContext, plugins = plugins) {
data class UserProfileInputs(
val userId: UserId
) : NodeInputs
private val inputs = inputs<UserProfileInputs>()
private val callback = inputs<UserProfileNodeHelper.Callback>()
private val presenter = presenterFactory.create(inputs.userId)
private val userProfileNodeHelper = UserProfileNodeHelper(inputs.userId)
init {
lifecycle.subscribe(
onResume = {
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.User))
}
)
}
@Composable
override fun View(modifier: Modifier) {
val context = LocalContext.current
fun onShareUser() {
userProfileNodeHelper.onShareUser(context, permalinkBuilder)
}
fun onStartDM(roomId: RoomId) {
callback.onStartDM(roomId)
}
val state = presenter.present()
LaunchedEffect(state.startDmActionState) {
val result = state.startDmActionState
if (result is AsyncAction.Success) {
onStartDM(result.data)
}
}
UserProfileView(
state = state,
modifier = modifier,
goBack = this::navigateUp,
onShareUser = ::onShareUser,
onDMStarted = ::onStartDM,
openAvatarPreview = callback::openAvatarPreview,
)
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.userprofile.impl.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.features.createroom.api.StartDMAction
import io.element.android.features.userprofile.shared.UserProfileEvents
import io.element.android.features.userprofile.shared.UserProfilePresenterHelper
import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.features.userprofile.shared.UserProfileState.ConfirmationDialog
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse
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.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class UserProfilePresenter @AssistedInject constructor(
@Assisted private val userId: UserId,
private val client: MatrixClient,
private val startDMAction: StartDMAction,
) : Presenter<UserProfileState> {
interface Factory {
fun create(userId: UserId): UserProfilePresenter
}
private val userProfilePresenterHelper = UserProfilePresenterHelper(
userId = userId,
client = client,
)
@Composable
override fun present(): UserProfileState {
val coroutineScope = rememberCoroutineScope()
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
var userProfile by remember { mutableStateOf<MatrixUser?>(null) }
val startDmActionState: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val isBlocked: MutableState<AsyncData<Boolean>> = remember { mutableStateOf(AsyncData.Uninitialized) }
LaunchedEffect(Unit) {
client.ignoredUsersFlow
.map { ignoredUsers -> userId in ignoredUsers }
.distinctUntilChanged()
.onEach { isBlocked.value = AsyncData.Success(it) }
.launchIn(this)
}
LaunchedEffect(Unit) {
userProfile = client.getProfile(userId).getOrNull()
}
fun handleEvents(event: UserProfileEvents) {
when (event) {
is UserProfileEvents.BlockUser -> {
if (event.needsConfirmation) {
confirmationDialog = ConfirmationDialog.Block
} else {
confirmationDialog = null
userProfilePresenterHelper.blockUser(coroutineScope, isBlocked)
}
}
is UserProfileEvents.UnblockUser -> {
if (event.needsConfirmation) {
confirmationDialog = ConfirmationDialog.Unblock
} else {
confirmationDialog = null
userProfilePresenterHelper.unblockUser(coroutineScope, isBlocked)
}
}
UserProfileEvents.ClearConfirmationDialog -> confirmationDialog = null
UserProfileEvents.ClearBlockUserError -> {
isBlocked.value = AsyncData.Success(isBlocked.value.dataOrNull().orFalse())
}
UserProfileEvents.StartDM -> {
coroutineScope.launch {
startDMAction.execute(userId, startDmActionState)
}
}
UserProfileEvents.ClearStartDMState -> {
startDmActionState.value = AsyncAction.Uninitialized
}
}
}
return UserProfileState(
userId = userId,
userName = userProfile?.displayName,
avatarUrl = userProfile?.avatarUrl,
isBlocked = isBlocked.value,
startDmActionState = startDmActionState.value,
displayConfirmationDialog = confirmationDialog,
isCurrentUser = client.isMe(userId),
eventSink = ::handleEvents
)
}
}

View file

@ -0,0 +1,238 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.userprofile.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.createroom.api.StartDMAction
import io.element.android.features.createroom.test.FakeStartDMAction
import io.element.android.features.userprofile.impl.root.UserProfilePresenter
import io.element.android.features.userprofile.shared.UserProfileEvents
import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ExperimentalCoroutinesApi
class UserProfilePresenterTests {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - returns the user profile data`() = runTest {
val matrixUser = aMatrixUser(A_USER_ID.value, "Alice", "anAvatarUrl")
val client = FakeMatrixClient().apply {
givenGetProfileResult(A_USER_ID, Result.success(matrixUser))
}
val presenter = createUserProfilePresenter(
client = client,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.userId).isEqualTo(matrixUser.userId)
assertThat(initialState.userName).isEqualTo(matrixUser.displayName)
assertThat(initialState.avatarUrl).isEqualTo(matrixUser.avatarUrl)
assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(false))
}
}
@Test
fun `present - returns empty data in case of failure`() = runTest {
val client = FakeMatrixClient().apply {
givenGetProfileResult(A_USER_ID, Result.failure(AN_EXCEPTION))
}
val presenter = createUserProfilePresenter(
client = client,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.userId).isEqualTo(A_USER_ID)
assertThat(initialState.userName).isNull()
assertThat(initialState.avatarUrl).isNull()
assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(false))
}
}
@Test
fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest {
val presenter = createUserProfilePresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = true))
val dialogState = awaitItem()
assertThat(dialogState.displayConfirmationDialog).isEqualTo(UserProfileState.ConfirmationDialog.Block)
dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
assertThat(awaitItem().displayConfirmationDialog).isNull()
ensureAllEventsConsumed()
}
}
@Test
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
val client = FakeMatrixClient()
val presenter = createUserProfilePresenter(
client = client,
userId = A_USER_ID
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf(A_USER_ID))
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf())
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
}
}
@Test
fun `present - BlockUser with error`() = runTest {
val matrixClient = FakeMatrixClient()
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE))
val presenter = createUserProfilePresenter(client = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
val errorState = awaitItem()
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
// Clear error
initialState.eventSink(UserProfileEvents.ClearBlockUserError)
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(false))
}
}
@Test
fun `present - UnblockUser with error`() = runTest {
val matrixClient = FakeMatrixClient()
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE))
val presenter = createUserProfilePresenter(client = matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
val errorState = awaitItem()
assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
// Clear error
initialState.eventSink(UserProfileEvents.ClearBlockUserError)
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true))
}
}
@Test
fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest {
val presenter = createUserProfilePresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true))
val dialogState = awaitItem()
assertThat(dialogState.displayConfirmationDialog).isEqualTo(UserProfileState.ConfirmationDialog.Unblock)
dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog)
assertThat(awaitItem().displayConfirmationDialog).isNull()
ensureAllEventsConsumed()
}
}
@Test
fun `present - start DM action complete scenario`() = runTest {
val startDMAction = FakeStartDMAction()
val presenter = createUserProfilePresenter(startDMAction = startDMAction)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java)
val startDMSuccessResult = AsyncAction.Success(A_ROOM_ID)
val startDMFailureResult = AsyncAction.Failure(A_THROWABLE)
// Failure
startDMAction.givenExecuteResult(startDMFailureResult)
initialState.eventSink(UserProfileEvents.StartDM)
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
awaitItem().also { state ->
assertThat(state.startDmActionState).isEqualTo(startDMFailureResult)
state.eventSink(UserProfileEvents.ClearStartDMState)
}
// Success
startDMAction.givenExecuteResult(startDMSuccessResult)
awaitItem().also { state ->
assertThat(state.startDmActionState).isEqualTo(AsyncAction.Uninitialized)
state.eventSink(UserProfileEvents.StartDM)
}
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
awaitItem().also { state ->
assertThat(state.startDmActionState).isEqualTo(startDMSuccessResult)
}
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
skipItems(1)
return awaitItem()
}
private fun createUserProfilePresenter(
client: MatrixClient = FakeMatrixClient(),
userId: UserId = UserId("@alice:server.org"),
startDMAction: StartDMAction = FakeStartDMAction()
): UserProfilePresenter {
return UserProfilePresenter(
userId = userId,
client = client,
startDMAction = startDMAction
)
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.userprofile.shared"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
anvil(projects.anvilcodegen)
implementation(projects.anvilannotations)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.mediaviewer.api)
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.testtags)
api(projects.features.userprofile.api)
api(projects.services.apperror.api)
implementation(libs.coil.compose)
implementation(projects.features.createroom.api)
implementation(projects.services.analytics.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
ksp(libs.showkase.processor)
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,13 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomdetails.impl.members.details package io.element.android.features.userprofile.shared
sealed interface RoomMemberDetailsEvents { sealed interface UserProfileEvents {
data object StartDM : RoomMemberDetailsEvents data object StartDM : UserProfileEvents
data object ClearStartDMState : RoomMemberDetailsEvents data object ClearStartDMState : UserProfileEvents
data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents data class BlockUser(val needsConfirmation: Boolean = false) : UserProfileEvents
data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents data class UnblockUser(val needsConfirmation: Boolean = false) : UserProfileEvents
data object ClearBlockUserError : RoomMemberDetailsEvents data object ClearBlockUserError : UserProfileEvents
data object ClearConfirmationDialog : RoomMemberDetailsEvents data object ClearConfirmationDialog : UserProfileEvents
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomdetails.impl.members.details package io.element.android.features.userprofile.shared
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -37,13 +37,14 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
@Composable @Composable
fun RoomMemberHeaderSection( fun UserProfileHeaderSection(
avatarUrl: String?, avatarUrl: String?,
userId: String, userId: UserId,
userName: String?, userName: String?,
openAvatarPreview: (url: String) -> Unit, openAvatarPreview: (url: String) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -51,7 +52,7 @@ fun RoomMemberHeaderSection(
Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier = Modifier.size(70.dp)) { Box(modifier = Modifier.size(70.dp)) {
Avatar( Avatar(
avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.UserHeader), avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
modifier = Modifier modifier = Modifier
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) } .clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.fillMaxSize() .fillMaxSize()
@ -68,7 +69,7 @@ fun RoomMemberHeaderSection(
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
} }
Text( Text(
text = userId, text = userId.value,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
modifier = Modifier modifier = Modifier

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomdetails.impl.members.details package io.element.android.features.userprofile.shared
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -27,7 +27,7 @@ import io.element.android.libraries.designsystem.components.button.MainActionBut
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun RoomMemberMainActionsSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) { fun UserProfileMainActionsSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) {
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
MainActionButton( MainActionButton(
title = stringResource(CommonStrings.action_share), title = stringResource(CommonStrings.action_share),

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.userprofile.shared
import android.content.Context
import io.element.android.libraries.androidutils.R
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
import io.element.android.libraries.architecture.NodeInputs
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.permalink.PermalinkBuilder
import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber
class UserProfileNodeHelper(
private val userId: UserId,
) {
interface Callback : NodeInputs {
fun openAvatarPreview(username: String, avatarUrl: String)
fun onStartDM(roomId: RoomId)
}
fun onShareUser(
context: Context,
permalinkBuilder: PermalinkBuilder,
) {
val permalinkResult = permalinkBuilder.permalinkForUser(userId)
permalinkResult.onSuccess { permalink ->
context.startSharePlainTextIntent(
activityResultLauncher = null,
chooserTitle = context.getString(CommonStrings.action_share),
text = permalink,
noActivityFoundMessage = context.getString(R.string.error_no_compatible_app_found)
)
}.onFailure {
Timber.e(it)
}
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.userprofile.shared
import androidx.compose.runtime.MutableState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class UserProfilePresenterHelper(
private val userId: UserId,
private val client: MatrixClient,
) {
fun blockUser(
scope: CoroutineScope,
isBlockedState: MutableState<AsyncData<Boolean>>,
) = scope.launch {
isBlockedState.value = AsyncData.Loading(false)
client.ignoreUser(userId)
.onFailure {
isBlockedState.value = AsyncData.Failure(it, false)
}
// Note: on success, ignoredUserList will be updated.
}
fun unblockUser(
scope: CoroutineScope,
isBlockedState: MutableState<AsyncData<Boolean>>,
) = scope.launch {
isBlockedState.value = AsyncData.Loading(true)
client.unignoreUser(userId)
.onFailure {
isBlockedState.value = AsyncData.Failure(it, true)
}
// Note: on success, ignoredUserList will be updated.
}
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,21 +14,22 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomdetails.impl.members.details package io.element.android.features.userprofile.shared
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
data class RoomMemberDetailsState( data class UserProfileState(
val userId: String, val userId: UserId,
val userName: String?, val userName: String?,
val avatarUrl: String?, val avatarUrl: String?,
val isBlocked: AsyncData<Boolean>, val isBlocked: AsyncData<Boolean>,
val startDmActionState: AsyncAction<RoomId>, val startDmActionState: AsyncAction<RoomId>,
val displayConfirmationDialog: ConfirmationDialog?, val displayConfirmationDialog: ConfirmationDialog?,
val isCurrentUser: Boolean, val isCurrentUser: Boolean,
val eventSink: (RoomMemberDetailsEvents) -> Unit val eventSink: (UserProfileEvents) -> Unit
) { ) {
enum class ConfirmationDialog { enum class ConfirmationDialog {
Block, Block,

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,37 +14,38 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomdetails.impl.members.details package io.element.android.features.userprofile.shared
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberDetailsState> { open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> {
override val values: Sequence<RoomMemberDetailsState> override val values: Sequence<UserProfileState>
get() = sequenceOf( get() = sequenceOf(
aRoomMemberDetailsState(), aUserProfileState(),
aRoomMemberDetailsState(userName = null), aUserProfileState(userName = null),
aRoomMemberDetailsState(isBlocked = AsyncData.Success(true)), aUserProfileState(isBlocked = AsyncData.Success(true)),
aRoomMemberDetailsState(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block), aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block),
aRoomMemberDetailsState(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock), aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock),
aRoomMemberDetailsState(isBlocked = AsyncData.Loading(true)), aUserProfileState(isBlocked = AsyncData.Loading(true)),
aRoomMemberDetailsState(startDmActionState = AsyncAction.Loading), aUserProfileState(startDmActionState = AsyncAction.Loading),
// Add other states here // Add other states here
) )
} }
fun aRoomMemberDetailsState( fun aUserProfileState(
userId: String = "@daniel:domain.com", userId: UserId = UserId("@daniel:domain.com"),
userName: String? = "Daniel", userName: String? = "Daniel",
avatarUrl: String? = null, avatarUrl: String? = null,
isBlocked: AsyncData<Boolean> = AsyncData.Success(false), isBlocked: AsyncData<Boolean> = AsyncData.Success(false),
startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized, startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized,
displayConfirmationDialog: RoomMemberDetailsState.ConfirmationDialog? = null, displayConfirmationDialog: UserProfileState.ConfirmationDialog? = null,
isCurrentUser: Boolean = false, isCurrentUser: Boolean = false,
eventSink: (RoomMemberDetailsEvents) -> Unit = {}, eventSink: (UserProfileEvents) -> Unit = {},
) = RoomMemberDetailsState( ) = UserProfileState(
userId = userId, userId = userId,
userName = userName, userName = userName,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomdetails.impl.members.details package io.element.android.features.userprofile.shared
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -30,17 +30,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomdetails.impl.R import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults import io.element.android.libraries.designsystem.components.async.AsyncActionViewDefaults
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.theme.components.IconSource 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.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle import io.element.android.libraries.designsystem.theme.components.ListItemStyle
@ -52,8 +49,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun RoomMemberDetailsView( fun UserProfileView(
state: RoomMemberDetailsState, state: UserProfileState,
onShareUser: () -> Unit, onShareUser: () -> Unit,
onDMStarted: (RoomId) -> Unit, onDMStarted: (RoomId) -> Unit,
goBack: () -> Unit, goBack: () -> Unit,
@ -72,21 +69,21 @@ fun RoomMemberDetailsView(
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
RoomMemberHeaderSection( UserProfileHeaderSection(
avatarUrl = state.avatarUrl, avatarUrl = state.avatarUrl,
userId = state.userId, userId = state.userId,
userName = state.userName, userName = state.userName,
openAvatarPreview = { avatarUrl -> openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.userName ?: state.userId, avatarUrl) openAvatarPreview(state.userName ?: state.userId.value, avatarUrl)
}, },
) )
RoomMemberMainActionsSection(onShareUser = onShareUser) UserProfileMainActionsSection(onShareUser = onShareUser)
Spacer(modifier = Modifier.height(26.dp)) Spacer(modifier = Modifier.height(26.dp))
if (!state.isCurrentUser) { if (!state.isCurrentUser) {
StartDMSection(onStartDMClicked = { state.eventSink(RoomMemberDetailsEvents.StartDM) }) StartDMSection(onStartDMClicked = { state.eventSink(UserProfileEvents.StartDM) })
BlockUserSection(state) BlockUserSection(state)
BlockUserDialogs(state) BlockUserDialogs(state)
} }
@ -99,8 +96,8 @@ fun RoomMemberDetailsView(
}, },
onSuccess = onDMStarted, onSuccess = onDMStarted,
errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) }, errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) },
onRetry = { state.eventSink(RoomMemberDetailsEvents.StartDM) }, onRetry = { state.eventSink(UserProfileEvents.StartDM) },
onErrorDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearStartDMState) }, onErrorDismiss = { state.eventSink(UserProfileEvents.ClearStartDMState) },
) )
} }
} }
@ -118,20 +115,12 @@ private fun StartDMSection(
) )
} }
@PreviewWithLargeHeight @PreviewsDayNight
@Composable @Composable
internal fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) = internal fun UserProfileViewPreview(
ElementPreviewLight { ContentToPreview(state) } @PreviewParameter(UserProfileStateProvider::class) state: UserProfileState
) = ElementPreview {
@PreviewWithLargeHeight UserProfileView(
@Composable
internal fun RoomMemberDetailsViewDarkPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) =
ElementPreviewDark { ContentToPreview(state) }
@ExcludeFromCoverage
@Composable
private fun ContentToPreview(state: RoomMemberDetailsState) {
RoomMemberDetailsView(
state = state, state = state,
onShareUser = {}, onShareUser = {},
goBack = {}, goBack = {},

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,18 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomdetails.impl.members.details.avatar package io.element.android.features.userprofile.shared.avatar
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter
@ContributesNode(RoomScope::class) @ContributesNode(SessionScope::class)
class AvatarPreviewNode @AssistedInject constructor( class AvatarPreviewNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,41 +14,41 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomdetails.impl.blockuser package io.element.android.features.userprofile.shared.blockuser
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import io.element.android.features.roomdetails.impl.R import io.element.android.features.userprofile.shared.R
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.userprofile.shared.UserProfileEvents
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
@Composable @Composable
fun BlockUserDialogs(state: RoomMemberDetailsState) { fun BlockUserDialogs(state: UserProfileState) {
when (state.displayConfirmationDialog) { when (state.displayConfirmationDialog) {
null -> Unit null -> Unit
RoomMemberDetailsState.ConfirmationDialog.Block -> { UserProfileState.ConfirmationDialog.Block -> {
BlockConfirmationDialog( BlockConfirmationDialog(
onBlockAction = { onBlockAction = {
state.eventSink( state.eventSink(
RoomMemberDetailsEvents.BlockUser( UserProfileEvents.BlockUser(
needsConfirmation = false needsConfirmation = false
) )
) )
}, },
onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) } onDismiss = { state.eventSink(UserProfileEvents.ClearConfirmationDialog) }
) )
} }
RoomMemberDetailsState.ConfirmationDialog.Unblock -> { UserProfileState.ConfirmationDialog.Unblock -> {
UnblockConfirmationDialog( UnblockConfirmationDialog(
onUnblockAction = { onUnblockAction = {
state.eventSink( state.eventSink(
RoomMemberDetailsEvents.UnblockUser( UserProfileEvents.UnblockUser(
needsConfirmation = false needsConfirmation = false
) )
) )
}, },
onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) } onDismiss = { state.eventSink(UserProfileEvents.ClearConfirmationDialog) }
) )
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2023 New Vector Ltd * Copyright (c) 2024 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomdetails.impl.blockuser package io.element.android.features.userprofile.shared.blockuser
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.progressSemantics import androidx.compose.foundation.progressSemantics
@ -23,9 +23,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomdetails.impl.R import io.element.android.features.userprofile.shared.R
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.userprofile.shared.UserProfileEvents
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
@ -39,8 +39,14 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
internal fun BlockUserSection(state: RoomMemberDetailsState) { fun BlockUserSection(
PreferenceCategory(showDivider = false) { state: UserProfileState,
modifier: Modifier = Modifier,
) {
PreferenceCategory(
modifier = modifier,
showDivider = false,
) {
when (state.isBlocked) { when (state.isBlocked) {
is AsyncData.Failure -> PreferenceBlockUser(isBlocked = state.isBlocked.prevData, isLoading = false, eventSink = state.eventSink) is AsyncData.Failure -> PreferenceBlockUser(isBlocked = state.isBlocked.prevData, isLoading = false, eventSink = state.eventSink)
is AsyncData.Loading -> PreferenceBlockUser(isBlocked = state.isBlocked.prevData, isLoading = true, eventSink = state.eventSink) is AsyncData.Loading -> PreferenceBlockUser(isBlocked = state.isBlocked.prevData, isLoading = true, eventSink = state.eventSink)
@ -51,13 +57,13 @@ internal fun BlockUserSection(state: RoomMemberDetailsState) {
if (state.isBlocked is AsyncData.Failure) { if (state.isBlocked is AsyncData.Failure) {
RetryDialog( RetryDialog(
content = stringResource(CommonStrings.error_unknown), content = stringResource(CommonStrings.error_unknown),
onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearBlockUserError) }, onDismiss = { state.eventSink(UserProfileEvents.ClearBlockUserError) },
onRetry = { onRetry = {
val event = when (state.isBlocked.prevData) { val event = when (state.isBlocked.prevData) {
true -> RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false) true -> UserProfileEvents.UnblockUser(needsConfirmation = false)
false -> RoomMemberDetailsEvents.BlockUser(needsConfirmation = false) false -> UserProfileEvents.BlockUser(needsConfirmation = false)
// null case Should not happen // null case Should not happen
null -> RoomMemberDetailsEvents.ClearBlockUserError null -> UserProfileEvents.ClearBlockUserError
} }
state.eventSink(event) state.eventSink(event)
}, },
@ -69,7 +75,7 @@ internal fun BlockUserSection(state: RoomMemberDetailsState) {
private fun PreferenceBlockUser( private fun PreferenceBlockUser(
isBlocked: Boolean?, isBlocked: Boolean?,
isLoading: Boolean, isLoading: Boolean,
eventSink: (RoomMemberDetailsEvents) -> Unit, eventSink: (UserProfileEvents) -> Unit,
) { ) {
val loadingCurrentValue = @Composable { val loadingCurrentValue = @Composable {
CircularProgressIndicator( CircularProgressIndicator(
@ -83,7 +89,7 @@ private fun PreferenceBlockUser(
ListItem( ListItem(
headlineContent = { Text(stringResource(R.string.screen_dm_details_unblock_user)) }, headlineContent = { Text(stringResource(R.string.screen_dm_details_unblock_user)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
onClick = { if (!isLoading) eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) }, onClick = { if (!isLoading) eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true)) },
trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null, trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null,
style = ListItemStyle.Primary, style = ListItemStyle.Primary,
) )
@ -92,7 +98,7 @@ private fun PreferenceBlockUser(
headlineContent = { Text(stringResource(R.string.screen_dm_details_block_user)) }, headlineContent = { Text(stringResource(R.string.screen_dm_details_block_user)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())),
style = ListItemStyle.Destructive, style = ListItemStyle.Destructive,
onClick = { if (!isLoading) eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) }, onClick = { if (!isLoading) eventSink(UserProfileEvents.BlockUser(needsConfirmation = true)) },
trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null, trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null,
) )
} }

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Заблакіраваць"</string>
<string name="screen_dm_details_block_alert_description">"Заблакіраваныя карыстальнікі не змогуць адпраўляць вам паведамленні, і ўсе іх паведамленні будуць схаваны. Вы можаце разблакіраваць іх у любы час."</string>
<string name="screen_dm_details_block_user">"Заблакіраваць карыстальніка"</string>
<string name="screen_dm_details_unblock_alert_action">"Разблакіраваць"</string>
<string name="screen_dm_details_unblock_alert_description">"Вы зноў зможаце ўбачыць усе паведамленні."</string>
<string name="screen_dm_details_unblock_user">"Разблакіраваць карыстальніка"</string>
<string name="screen_start_chat_error_starting_chat">"Пры спробе пачаць чат адбылася памылка"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Блокиране"</string>
<string name="screen_dm_details_block_user">"Блокиране на потребителя"</string>
<string name="screen_dm_details_unblock_alert_action">"Отблокиране"</string>
<string name="screen_dm_details_unblock_user">"Отблокиране на потребителя"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Zablokovat"</string>
<string name="screen_dm_details_block_alert_description">"Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny jejich zprávy budou skryty. Můžete je kdykoli odblokovat."</string>
<string name="screen_dm_details_block_user">"Zablokovat uživatele"</string>
<string name="screen_dm_details_unblock_alert_action">"Odblokovat"</string>
<string name="screen_dm_details_unblock_alert_description">"Znovu uvidíte všechny zprávy od nich."</string>
<string name="screen_dm_details_unblock_user">"Odblokovat uživatele"</string>
<string name="screen_start_chat_error_starting_chat">"Při pokusu o zahájení chatu došlo k chybě"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blockieren"</string>
<string name="screen_dm_details_block_alert_description">"Blockierte Benutzer können Dir keine Nachrichten senden und alle ihre alten Nachrichten werden ausgeblendet. Die Blockierung kann jederzeit aufgehoben werden."</string>
<string name="screen_dm_details_block_user">"Benutzer blockieren"</string>
<string name="screen_dm_details_unblock_alert_action">"Blockierung aufheben"</string>
<string name="screen_dm_details_unblock_alert_description">"Der Nutzer kann dir wieder Nachrichten senden &amp; alle Nachrichten des Nutzers werden wieder angezeigt."</string>
<string name="screen_dm_details_unblock_user">"Blockierung aufheben"</string>
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Bloquear"</string>
<string name="screen_dm_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y todos sus mensajes se ocultarán. Puedes desbloquearlos cuando quieras."</string>
<string name="screen_dm_details_block_user">"Bloquear usuario"</string>
<string name="screen_dm_details_unblock_alert_action">"Desbloquear"</string>
<string name="screen_dm_details_unblock_alert_description">"Podrás ver todos sus mensajes de nuevo."</string>
<string name="screen_dm_details_unblock_user">"Desbloquear usuario"</string>
<string name="screen_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Bloquer"</string>
<string name="screen_dm_details_block_alert_description">"Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment."</string>
<string name="screen_dm_details_block_user">"Bloquer lutilisateur"</string>
<string name="screen_dm_details_unblock_alert_action">"Débloquer"</string>
<string name="screen_dm_details_unblock_alert_description">"Vous pourrez à nouveau voir tous ses messages."</string>
<string name="screen_dm_details_unblock_user">"Débloquer lutilisateur"</string>
<string name="screen_start_chat_error_starting_chat">"Une erreur sest produite lors de la tentative de création de la discussion"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Letiltás"</string>
<string name="screen_dm_details_block_alert_description">"A letiltott felhasználók nem fognak tudni üzeneteket küldeni, és az összes üzenetük rejtve lesz. Bármikor feloldhatja a letiltásukat."</string>
<string name="screen_dm_details_block_user">"Felhasználó letiltása"</string>
<string name="screen_dm_details_unblock_alert_action">"Letiltás feloldása"</string>
<string name="screen_dm_details_unblock_alert_description">"Újra láthatja az összes üzenetét."</string>
<string name="screen_dm_details_unblock_user">"Felhasználó kitiltásának feloldása"</string>
<string name="screen_start_chat_error_starting_chat">"Hiba történt a csevegés indításakor"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blocca"</string>
<string name="screen_dm_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti messaggi e tutti quelli già ricevuti saranno nascosti. Puoi sbloccarli in qualsiasi momento."</string>
<string name="screen_dm_details_block_user">"Blocca utente"</string>
<string name="screen_dm_details_unblock_alert_action">"Sblocca"</string>
<string name="screen_dm_details_unblock_alert_description">"Potrai vedere di nuovo tutti i suoi messaggi."</string>
<string name="screen_dm_details_unblock_user">"Sblocca utente"</string>
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blocați"</string>
<string name="screen_dm_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
<string name="screen_dm_details_block_user">"Blocați utilizatorul"</string>
<string name="screen_dm_details_unblock_alert_action">"Deblocați"</string>
<string name="screen_dm_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
<string name="screen_dm_details_unblock_user">"Deblocați utilizatorul"</string>
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Заблокировать"</string>
<string name="screen_dm_details_block_alert_description">"Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."</string>
<string name="screen_dm_details_block_user">"Заблокировать пользователя"</string>
<string name="screen_dm_details_unblock_alert_action">"Разблокировать"</string>
<string name="screen_dm_details_unblock_alert_description">"Вы снова сможете увидеть все сообщения."</string>
<string name="screen_dm_details_unblock_user">"Разблокировать пользователя"</string>
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при попытке открытия комнаты"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Zablokovať"</string>
<string name="screen_dm_details_block_alert_description">"Blokovaní používatelia vám nebudú môcť posielať správy a všetky ich správy budú skryté. Môžete ich kedykoľvek odblokovať."</string>
<string name="screen_dm_details_block_user">"Zablokovať používateľa"</string>
<string name="screen_dm_details_unblock_alert_action">"Odblokovať"</string>
<string name="screen_dm_details_unblock_alert_description">"Všetky správy od nich budete môcť opäť vidieť."</string>
<string name="screen_dm_details_unblock_user">"Odblokovať používateľa"</string>
<string name="screen_start_chat_error_starting_chat">"Pri pokuse o spustenie konverzácie sa vyskytla chyba"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Blockera"</string>
<string name="screen_dm_details_block_alert_description">"Blockerade användare kommer inte att kunna skicka meddelanden till dig och alla deras meddelanden kommer att döljas. Du kan avblockera dem när som helst."</string>
<string name="screen_dm_details_block_user">"Blockera användare"</string>
<string name="screen_dm_details_unblock_alert_action">"Avblockera"</string>
<string name="screen_dm_details_unblock_alert_description">"Du kommer att kunna se alla meddelanden från dem igen."</string>
<string name="screen_dm_details_unblock_user">"Avblockera användare"</string>
<string name="screen_start_chat_error_starting_chat">"Ett fel uppstod när du försökte starta en chatt"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Заблокувати"</string>
<string name="screen_dm_details_block_alert_description">"Заблоковані користувачі не зможуть надсилати Вам повідомлення, і всі їхні повідомлення будуть приховані. Ви можете розблокувати їх у будь-який час."</string>
<string name="screen_dm_details_block_user">"Заблокувати користувача"</string>
<string name="screen_dm_details_unblock_alert_action">"Розблокувати"</string>
<string name="screen_dm_details_unblock_alert_description">"Ви знову зможете бачити всі повідомлення від них."</string>
<string name="screen_dm_details_unblock_user">"Розблокувати користувача"</string>
<string name="screen_start_chat_error_starting_chat">"Під час спроби почати чат сталася помилка"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"封鎖"</string>
<string name="screen_dm_details_block_user">"封鎖使用者"</string>
<string name="screen_dm_details_unblock_alert_action">"解除封鎖"</string>
<string name="screen_dm_details_unblock_user">"解除封鎖使用者"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_dm_details_block_alert_action">"Block"</string>
<string name="screen_dm_details_block_alert_description">"Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime."</string>
<string name="screen_dm_details_block_user">"Block user"</string>
<string name="screen_dm_details_unblock_alert_action">"Unblock"</string>
<string name="screen_dm_details_unblock_alert_description">"You\'ll be able to see all messages from them again."</string>
<string name="screen_dm_details_unblock_user">"Unblock user"</string>
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
</resources>

View file

@ -14,15 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.features.roomdetails.impl.blockuser package io.element.android.features.userprofile.shared.blockuser
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdetails.impl.R import io.element.android.features.userprofile.shared.R
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.userprofile.shared.UserProfileEvents
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState import io.element.android.features.userprofile.shared.aUserProfileState
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
@ -36,61 +36,61 @@ class BlockUserDialogsTest {
@Test @Test
fun `confirm block user emit expected Event`() { fun `confirm block user emit expected Event`() {
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent { rule.setContent {
BlockUserDialogs( BlockUserDialogs(
state = aRoomMemberDetailsState( state = aUserProfileState(
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
} }
rule.clickOn(R.string.screen_dm_details_block_alert_action) rule.clickOn(R.string.screen_dm_details_block_alert_action)
eventsRecorder.assertSingle(RoomMemberDetailsEvents.BlockUser(false)) eventsRecorder.assertSingle(UserProfileEvents.BlockUser(false))
} }
@Test @Test
fun `cancel block user emit expected Event`() { fun `cancel block user emit expected Event`() {
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent { rule.setContent {
BlockUserDialogs( BlockUserDialogs(
state = aRoomMemberDetailsState( state = aUserProfileState(
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
} }
rule.clickOn(CommonStrings.action_cancel) rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(RoomMemberDetailsEvents.ClearConfirmationDialog) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
} }
@Test @Test
fun `confirm unblock user emit expected Event`() { fun `confirm unblock user emit expected Event`() {
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent { rule.setContent {
BlockUserDialogs( BlockUserDialogs(
state = aRoomMemberDetailsState( state = aUserProfileState(
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
} }
rule.clickOn(R.string.screen_dm_details_unblock_alert_action) rule.clickOn(R.string.screen_dm_details_unblock_alert_action)
eventsRecorder.assertSingle(RoomMemberDetailsEvents.UnblockUser(false)) eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(false))
} }
@Test @Test
fun `cancel unblock user emit expected Event`() { fun `cancel unblock user emit expected Event`() {
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>() val eventsRecorder = EventsRecorder<UserProfileEvents>()
rule.setContent { rule.setContent {
BlockUserDialogs( BlockUserDialogs(
state = aRoomMemberDetailsState( state = aUserProfileState(
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock, displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock,
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
) )
} }
rule.clickOn(CommonStrings.action_cancel) rule.clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(RoomMemberDetailsEvents.ClearConfirmationDialog) eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog)
} }
} }

View file

@ -9,6 +9,7 @@
<string name="screen_session_verification_positive_button_canceled">"Försök att verifiera igen"</string> <string name="screen_session_verification_positive_button_canceled">"Försök att verifiera igen"</string>
<string name="screen_session_verification_positive_button_initial">"Jag är redo"</string> <string name="screen_session_verification_positive_button_initial">"Jag är redo"</string>
<string name="screen_session_verification_positive_button_verifying_ongoing">"Väntar på att matcha"</string> <string name="screen_session_verification_positive_button_verifying_ongoing">"Väntar på att matcha"</string>
<string name="screen_session_verification_ready_subtitle">"Jämför en unik uppsättning emojis."</string>
<string name="screen_session_verification_request_accepted_subtitle">"Jämför de unika emojierna och se till att de visas i samma ordning."</string> <string name="screen_session_verification_request_accepted_subtitle">"Jämför de unika emojierna och se till att de visas i samma ordning."</string>
<string name="screen_session_verification_they_dont_match">"De matchar inte"</string> <string name="screen_session_verification_they_dont_match">"De matchar inte"</string>
<string name="screen_session_verification_they_match">"De matchar"</string> <string name="screen_session_verification_they_match">"De matchar"</string>

View file

@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmed_title">"裝置已認證"</string> <string name="screen_identity_confirmation_create_new_recovery_key">"建立新的復原金鑰"</string>
<string name="screen_identity_use_another_device">"使用另一個裝置"</string> <string name="screen_identity_confirmation_subtitle">"驗證這部裝置以設定安全通訊。"</string>
<string name="screen_identity_confirmation_title">"確認這是你本人"</string>
<string name="screen_identity_confirmed_subtitle">"您可以安全地讀取和發送訊息了,與您聊天的人也可以信任這部裝置。"</string>
<string name="screen_identity_confirmed_title">"裝置已驗證"</string>
<string name="screen_identity_use_another_device">"使用另一部裝置"</string>
<string name="screen_identity_waiting_on_other_device">"正在等待其他裝置……"</string> <string name="screen_identity_waiting_on_other_device">"正在等待其他裝置……"</string>
<string name="screen_session_verification_cancelled_subtitle">"似乎出了一點問題。有可能是因為等候逾時,或是請求被拒絕。"</string> <string name="screen_session_verification_cancelled_subtitle">"似乎出了一點問題。有可能是因為等候逾時,或是請求被拒絕。"</string>
<string name="screen_session_verification_compare_emojis_subtitle">"確認顯示在其他工作階段上的表情符號是否和下方的相同。"</string> <string name="screen_session_verification_compare_emojis_subtitle">"確認顯示在其他工作階段上的表情符號是否和下方的相同。"</string>

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