Browse Source

Open user profile and room with event from permalink

pull/2776/head
Benoit Marty 5 months ago committed by Benoit Marty
parent
commit
0476bd95c8
  1. 29
      app/src/main/AndroidManifest.xml
  2. 52
      appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
  3. 31
      appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
  4. 16
      appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
  5. 6
      appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
  6. 2
      features/createroom/impl/src/main/res/values-de/translations.xml
  7. 3
      features/ftue/impl/src/main/res/values-be/translations.xml
  8. 3
      features/ftue/impl/src/main/res/values-cs/translations.xml
  9. 3
      features/ftue/impl/src/main/res/values-de/translations.xml
  10. 3
      features/ftue/impl/src/main/res/values-fr/translations.xml
  11. 3
      features/ftue/impl/src/main/res/values-hu/translations.xml
  12. 2
      features/ftue/impl/src/main/res/values-in/translations.xml
  13. 2
      features/ftue/impl/src/main/res/values-ru/translations.xml
  14. 3
      features/ftue/impl/src/main/res/values-sk/translations.xml
  15. 3
      features/ftue/impl/src/main/res/values/localazy.xml
  16. 2
      features/lockscreen/impl/src/main/res/values-de/translations.xml
  17. 8
      features/lockscreen/impl/src/main/res/values-sv/translations.xml
  18. 2
      features/logout/impl/src/main/res/values-de/translations.xml
  19. 7
      features/logout/impl/src/main/res/values-sv/translations.xml
  20. 2
      features/messages/impl/src/main/res/values-sv/translations.xml
  21. 1
      features/roomdetails/impl/build.gradle.kts
  22. 7
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
  23. 4
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
  24. 8
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
  25. 14
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
  26. 33
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt
  27. 55
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt
  28. 7
      features/roomdetails/impl/src/main/res/values-be/translations.xml
  29. 4
      features/roomdetails/impl/src/main/res/values-bg/translations.xml
  30. 7
      features/roomdetails/impl/src/main/res/values-cs/translations.xml
  31. 17
      features/roomdetails/impl/src/main/res/values-de/translations.xml
  32. 7
      features/roomdetails/impl/src/main/res/values-es/translations.xml
  33. 7
      features/roomdetails/impl/src/main/res/values-fr/translations.xml
  34. 7
      features/roomdetails/impl/src/main/res/values-hu/translations.xml
  35. 7
      features/roomdetails/impl/src/main/res/values-in/translations.xml
  36. 7
      features/roomdetails/impl/src/main/res/values-it/translations.xml
  37. 7
      features/roomdetails/impl/src/main/res/values-ro/translations.xml
  38. 7
      features/roomdetails/impl/src/main/res/values-ru/translations.xml
  39. 7
      features/roomdetails/impl/src/main/res/values-sk/translations.xml
  40. 7
      features/roomdetails/impl/src/main/res/values-sv/translations.xml
  41. 7
      features/roomdetails/impl/src/main/res/values-uk/translations.xml
  42. 4
      features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml
  43. 7
      features/roomdetails/impl/src/main/res/values/localazy.xml
  44. 36
      features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt
  45. 2
      features/roomlist/impl/src/main/res/values-sv/translations.xml
  46. 2
      features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml
  47. 15
      features/securebackup/impl/src/main/res/values-de/translations.xml
  48. 3
      features/securebackup/impl/src/main/res/values-sv/translations.xml
  49. 29
      features/userprofile/api/build.gradle.kts
  50. 41
      features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEntryPoint.kt
  51. 69
      features/userprofile/impl/build.gradle.kts
  52. 49
      features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfileEntryPoint.kt
  53. 106
      features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt
  54. 42
      features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/di/UserProfileModule.kt
  55. 96
      features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt
  56. 124
      features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt
  57. 238
      features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTests.kt
  58. 71
      features/userprofile/shared/build.gradle.kts
  59. 18
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileEvents.kt
  60. 13
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt
  61. 6
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileMainActionsSection.kt
  62. 53
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt
  63. 53
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfilePresenterHelper.kt
  64. 11
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileState.kt
  65. 33
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt
  66. 49
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt
  67. 8
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/avatar/AvatarPreviewNode.kt
  68. 24
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogs.kt
  69. 34
      features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt
  70. 10
      features/userprofile/shared/src/main/res/values-be/translations.xml
  71. 7
      features/userprofile/shared/src/main/res/values-bg/translations.xml
  72. 10
      features/userprofile/shared/src/main/res/values-cs/translations.xml
  73. 10
      features/userprofile/shared/src/main/res/values-de/translations.xml
  74. 10
      features/userprofile/shared/src/main/res/values-es/translations.xml
  75. 10
      features/userprofile/shared/src/main/res/values-fr/translations.xml
  76. 10
      features/userprofile/shared/src/main/res/values-hu/translations.xml
  77. 10
      features/userprofile/shared/src/main/res/values-it/translations.xml
  78. 10
      features/userprofile/shared/src/main/res/values-ro/translations.xml
  79. 10
      features/userprofile/shared/src/main/res/values-ru/translations.xml
  80. 10
      features/userprofile/shared/src/main/res/values-sk/translations.xml
  81. 10
      features/userprofile/shared/src/main/res/values-sv/translations.xml
  82. 10
      features/userprofile/shared/src/main/res/values-uk/translations.xml
  83. 7
      features/userprofile/shared/src/main/res/values-zh-rTW/translations.xml
  84. 10
      features/userprofile/shared/src/main/res/values/localazy.xml
  85. 42
      features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt
  86. 1
      features/verifysession/impl/src/main/res/values-sv/translations.xml
  87. 8
      features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml
  88. 2
      libraries/eventformatter/impl/src/main/res/values-fr/translations.xml
  89. 1
      libraries/ui-strings/src/main/res/values-cs/translations.xml
  90. 9
      libraries/ui-strings/src/main/res/values-de/translations.xml
  91. 1
      libraries/ui-strings/src/main/res/values-hu/translations.xml
  92. 7
      libraries/ui-strings/src/main/res/values-sv/translations.xml
  93. 9
      tools/localazy/config.json

29
app/src/main/AndroidManifest.xml

@ -74,6 +74,35 @@ @@ -74,6 +74,35 @@
<data android:scheme="io.element" />
</intent-filter>
<!--
Element web 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 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="app.element.io" />
<data android:host="develop.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>
</activity>
<provider

52
appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

@ -56,6 +56,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription @@ -56,6 +56,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint
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.BaseFlowNode
import io.element.android.libraries.architecture.createNode
@ -64,9 +65,11 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch @@ -64,9 +65,11 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SessionScope
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.RoomId
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.permalink.PermalinkData
import io.element.android.libraries.matrix.api.sync.SyncState
@ -91,6 +94,7 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -91,6 +94,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
private val secureBackupEntryPoint: SecureBackupEntryPoint,
private val userProfileEntryPoint: UserProfileEntryPoint,
private val ftueEntryPoint: FtueEntryPoint,
private val coroutineScope: CoroutineScope,
private val networkMonitor: NetworkMonitor,
@ -197,6 +201,11 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -197,6 +201,11 @@ class LoggedInFlowNode @AssistedInject constructor(
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages()
) : NavTarget
@Parcelize
data class UserProfile(
val userId: UserId,
) : NavTarget
@Parcelize
data class Settings(
val initialElement: PreferencesEntryPoint.InitialTarget = PreferencesEntryPoint.InitialTarget.Root
@ -270,14 +279,14 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -270,14 +279,14 @@ class LoggedInFlowNode @AssistedInject constructor(
}
override fun onForwardedToSingleRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) }
coroutineScope.launch { attachRoom(roomId.toRoomIdOrAlias()) }
}
override fun onPermalinkClicked(data: PermalinkData) {
when (data) {
is PermalinkData.UserLink -> {
// FIXME Add a user profile screen.
Timber.e("User link clicked: ${data.userId}. TODO Add a user profile screen")
// Should not happen (handled by MessagesNode)
Timber.e("User link clicked: ${data.userId}.")
}
is PermalinkData.RoomLink -> {
backstack.push(
@ -306,6 +315,17 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -306,6 +315,17 @@ class LoggedInFlowNode @AssistedInject constructor(
)
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 -> {
val callback = object : PreferencesEntryPoint.Callback {
override fun onOpenBugReport() {
@ -321,7 +341,7 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -321,7 +341,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
return preferencesEntryPoint.nodeBuilder(this, buildContext)
preferencesEntryPoint.nodeBuilder(this, buildContext)
.params(inputs)
.callback(callback)
.build()
@ -363,12 +383,32 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -363,12 +383,32 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}
suspend fun attachRoom(roomId: RoomId) {
suspend fun attachRoom(roomIdOrAlias: RoomIdOrAlias, eventId: EventId? = null) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList
}
attachChild<RoomFlowNode> {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias()))
backstack.push(
NavTarget.Room(
roomIdOrAlias = roomIdOrAlias,
initialElement = RoomNavigationTarget.Messages(
focusedEventId = eventId
)
)
)
}
}
suspend fun attachUser(userId: UserId) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.RoomList
}
attachChild<Node> {
backstack.push(
NavTarget.UserProfile(
userId = userId,
)
)
}
}

31
appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt

@ -55,6 +55,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre @@ -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.matrix.api.auth.MatrixAuthenticationService
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 kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
@ -279,9 +281,31 @@ class RootFlowNode @AssistedInject constructor( @@ -279,9 +281,31 @@ class RootFlowNode @AssistedInject constructor(
when (resolvedIntent) {
is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
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)
.attachSession()
.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) {
Timber.d("Navigating to $deeplinkData")
attachSession(deeplinkData.sessionId)
@ -289,7 +313,7 @@ class RootFlowNode @AssistedInject constructor( @@ -289,7 +313,7 @@ class RootFlowNode @AssistedInject constructor(
.apply {
when (deeplinkData) {
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 +322,11 @@ class RootFlowNode @AssistedInject constructor( @@ -298,10 +322,11 @@ class RootFlowNode @AssistedInject constructor(
oidcActionFlow.post(oidcAction)
}
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
// [sessionId] will be null for permalink.
private suspend fun attachSession(sessionId: SessionId?): LoggedInAppScopeFlowNode {
// TODO handle multi-session
return waitForChildAttached { navTarget ->
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
navTarget is NavTarget.LoggedInFlow && (sessionId == null || navTarget.sessionId == sessionId)
}
}
}

16
appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt

@ -21,27 +21,41 @@ import io.element.android.features.login.api.oidc.OidcAction @@ -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.libraries.deeplink.DeeplinkData
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 javax.inject.Inject
sealed interface ResolvedIntent {
data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
data class Permalink(val permalinkData: PermalinkData) : ResolvedIntent
}
class IntentResolver @Inject constructor(
private val deeplinkParser: DeeplinkParser,
private val oidcIntentResolver: OidcIntentResolver
private val oidcIntentResolver: OidcIntentResolver,
private val permalinkParser: PermalinkParser,
) {
fun resolve(intent: Intent): ResolvedIntent? {
if (intent.canBeIgnored()) return null
// Coming from a notification?
val deepLinkData = deeplinkParser.getFromIntent(intent)
if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
// Coming during login using Oidc?
val oidcAction = oidcIntentResolver.resolve(intent)
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
Timber.w("Unknown intent")
return null

6
appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt

@ -18,6 +18,7 @@ package io.element.android.appnav.intent @@ -18,6 +18,7 @@ package io.element.android.appnav.intent
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.oidc.OidcAction
@ -26,9 +27,11 @@ import io.element.android.features.login.impl.oidc.OidcUrlParser @@ -26,9 +27,11 @@ import io.element.android.features.login.impl.oidc.OidcUrlParser
import io.element.android.libraries.deeplink.DeepLinkCreator
import io.element.android.libraries.deeplink.DeeplinkData
import io.element.android.libraries.deeplink.DeeplinkParser
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_SESSION_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.Test
import org.junit.runner.RunWith
@ -179,6 +182,9 @@ class IntentResolverTest { @@ -179,6 +182,9 @@ class IntentResolverTest {
oidcIntentResolver = DefaultOidcIntentResolver(
oidcUrlParser = OidcUrlParser()
),
permalinkParser = FakePermalinkParser(
result = { PermalinkData.FallbackLink(Uri.parse("https://matrix.org")) }
),
)
}
}

2
features/createroom/impl/src/main/res/values-de/translations.xml

@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
<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_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_topic_label">"Thema (optional)"</string>
<string name="screen_start_chat_error_starting_chat">"Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"</string>

3
features/ftue/impl/src/main/res/values-be/translations.xml

@ -15,8 +15,7 @@ @@ -15,8 +15,7 @@
<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_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_item_4">"Выконвайце паказаныя інструкцыі"</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_retry_button">"Паўтарыць спробу"</string>

3
features/ftue/impl/src/main/res/values-cs/translations.xml

@ -15,8 +15,7 @@ @@ -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_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_4">"Vybrat %1$s"</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_item_4">"Postupujte podle uvedených pokynů"</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_retry_button">"Zkusit znovu"</string>

3
features/ftue/impl/src/main/res/values-de/translations.xml

@ -15,8 +15,7 @@ @@ -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_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_4">"Wähle %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"\"QR-Code anzeigen\""</string>
<string name="screen_qr_code_login_initial_state_item_4">"Befolge die angezeigten Anweisungen"</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_retry_button">"Erneut versuchen"</string>

3
features/ftue/impl/src/main/res/values-fr/translations.xml

@ -7,8 +7,7 @@ @@ -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_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_4">"Choisissez %1$s"</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_item_4">"Suivez les instructions affichées"</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 l’autre appareil."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Essayer à nouveau"</string>

3
features/ftue/impl/src/main/res/values-hu/translations.xml

@ -15,8 +15,7 @@ @@ -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_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_4">"Válassza ezt: %1$s"</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_item_4">"Kövesse a látható utasításokat"</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_retry_button">"Próbálja újra"</string>

2
features/ftue/impl/src/main/res/values-in/translations.xml

@ -15,8 +15,6 @@ @@ -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_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_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_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>

2
features/ftue/impl/src/main/res/values-ru/translations.xml

@ -15,8 +15,6 @@ @@ -15,8 +15,6 @@
<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_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_invalid_scan_state_description">"Используйте QR-код, показанный на другом устройстве."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Повторить попытку"</string>

3
features/ftue/impl/src/main/res/values-sk/translations.xml

@ -15,8 +15,7 @@ @@ -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_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_4">"Vyberte %1$s"</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_item_4">"Postupujte podľa zobrazených pokynov"</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_retry_button">"Skúste to znova"</string>

3
features/ftue/impl/src/main/res/values/localazy.xml

@ -15,8 +15,7 @@ @@ -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_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_4">"Select %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_4_action">"“Show QR code”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Follow the instructions shown"</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_retry_button">"Try again"</string>

2
features/lockscreen/impl/src/main/res/values-de/translations.xml

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
<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_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>
<string name="screen_app_lock_setup_pin_mismatch_dialog_content">"Bitte gib die gleiche PIN wie zuvor ein."</string>

8
features/lockscreen/impl/src/main/res/values-sv/translations.xml

@ -1,11 +1,17 @@ @@ -1,11 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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_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_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_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>
@ -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 @@ -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="other">"Fel PIN-kod. Du har %1$d försök kvar"</item>
</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>
</resources>

2
features/logout/impl/src/main/res/values-de/translations.xml

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
<string name="screen_signout_confirmation_dialog_title">"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_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_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>

7
features/logout/impl/src/main/res/values-sv/translations.xml

@ -4,8 +4,15 @@ @@ -4,8 +4,15 @@
<string name="screen_signout_confirmation_dialog_submit">"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_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_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_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_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>

2
features/messages/impl/src/main/res/values-sv/translations.xml

@ -21,8 +21,10 @@ @@ -21,8 +21,10 @@
<string name="screen_room_attachment_source_poll">"Omröstning"</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_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_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_retry_send_menu_send_again_action">"Skicka igen"</string>
<string name="screen_room_retry_send_menu_title">"Ditt meddelande kunde inte skickas"</string>

1
features/roomdetails/impl/build.gradle.kts

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

7
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt

@ -34,9 +34,10 @@ import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode @@ -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.members.RoomMemberListNode
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.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.BaseFlowNode
import io.element.android.libraries.architecture.createNode
@ -78,7 +79,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( @@ -78,7 +79,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Parcelize
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
*/
val showUserDefinedSettingStyle: Boolean
@ -164,7 +165,7 @@ class RoomDetailsFlowNode @AssistedInject constructor( @@ -164,7 +165,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
}
is NavTarget.RoomMemberDetails -> {
val callback = object : RoomMemberDetailsNode.Callback {
val callback = object : UserProfileNodeHelper.Callback {
override fun openAvatarPreview(username: String, avatarUrl: String) {
backstack.push(NavTarget.AvatarPreview(username, avatarUrl))
}

4
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt

@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
package io.element.android.features.roomdetails.impl
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.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
@ -32,7 +32,7 @@ data class RoomDetailsState( @@ -32,7 +32,7 @@ data class RoomDetailsState(
val memberCount: Long,
val isEncrypted: Boolean,
val roomType: RoomDetailsType,
val roomMemberDetailsState: RoomMemberDetailsState?,
val roomMemberDetailsState: UserProfileState?,
val canEdit: Boolean,
val canInvite: Boolean,
val canShowNotificationSettings: Boolean,

8
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt

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

14
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt

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

33
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt

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

55
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt

@ -28,7 +28,10 @@ import androidx.compose.runtime.setValue @@ -28,7 +28,10 @@ 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.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.AsyncData
import io.element.android.libraries.architecture.Presenter
@ -39,7 +42,6 @@ import io.element.android.libraries.matrix.api.core.UserId @@ -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.user.MatrixUser
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@ -51,13 +53,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( @@ -51,13 +53,18 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
private val client: MatrixClient,
private val room: MatrixRoom,
private val startDMAction: StartDMAction,
) : Presenter<RoomMemberDetailsState> {
) : Presenter<UserProfileState> {
interface Factory {
fun create(roomMemberId: UserId): RoomMemberDetailsPresenter
}
private val userProfilePresenterHelper = UserProfilePresenterHelper(
userId = roomMemberId,
client = client,
)
@Composable
override fun present(): RoomMemberDetailsState {
override fun present(): UserProfileState {
val coroutineScope = rememberCoroutineScope()
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
val roomMember by room.getRoomMemberAsState(roomMemberId)
@ -81,34 +88,34 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( @@ -81,34 +88,34 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
}
}
fun handleEvents(event: RoomMemberDetailsEvents) {
fun handleEvents(event: UserProfileEvents) {
when (event) {
is RoomMemberDetailsEvents.BlockUser -> {
is UserProfileEvents.BlockUser -> {
if (event.needsConfirmation) {
confirmationDialog = ConfirmationDialog.Block
} else {
confirmationDialog = null
coroutineScope.blockUser(roomMemberId, isBlocked)
userProfilePresenterHelper.blockUser(coroutineScope, isBlocked)
}
}
is RoomMemberDetailsEvents.UnblockUser -> {
is UserProfileEvents.UnblockUser -> {
if (event.needsConfirmation) {
confirmationDialog = ConfirmationDialog.Unblock
} else {
confirmationDialog = null
coroutineScope.unblockUser(roomMemberId, isBlocked)
userProfilePresenterHelper.unblockUser(coroutineScope, isBlocked)
}
}
RoomMemberDetailsEvents.ClearConfirmationDialog -> confirmationDialog = null
RoomMemberDetailsEvents.ClearBlockUserError -> {
UserProfileEvents.ClearConfirmationDialog -> confirmationDialog = null
UserProfileEvents.ClearBlockUserError -> {
isBlocked.value = AsyncData.Success(isBlocked.value.dataOrNull().orFalse())
}
RoomMemberDetailsEvents.StartDM -> {
UserProfileEvents.StartDM -> {
coroutineScope.launch {
startDMAction.execute(roomMemberId, startDmActionState)
}
}
RoomMemberDetailsEvents.ClearStartDMState -> {
UserProfileEvents.ClearStartDMState -> {
startDmActionState.value = AsyncAction.Uninitialized
}
}
@ -144,8 +151,8 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( @@ -144,8 +151,8 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
)
}
return RoomMemberDetailsState(
userId = roomMemberId.value,
return UserProfileState(
userId = roomMemberId,
userName = userName,
avatarUrl = userAvatar,
isBlocked = isBlocked.value,
@ -155,22 +162,4 @@ class RoomMemberDetailsPresenter @AssistedInject constructor( @@ -155,22 +162,4 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
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.
}
}

7
features/roomdetails/impl/src/main/res/values-be/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_notification_settings_edit_failed_updating_default_mode">"Пры абнаўленні налад апавяшчэнняў адбылася памылка."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш хатні сервер не падтрымлівае гэтую опцыю ў зашыфраваных пакоях, вы можаце не атрымаць апавяшчэнне ў некаторых пакоях."</string>
<string name="screen_polls_history_title">"Апытанні"</string>
@ -117,5 +111,4 @@ @@ -117,5 +111,4 @@
<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_title">"Ролі і дазволы"</string>
<string name="screen_start_chat_error_starting_chat">"Пры спробе пачаць чат адбылася памылка"</string>
</resources>

4
features/roomdetails/impl/src/main/res/values-bg/translations.xml

@ -1,9 +1,5 @@ @@ -1,9 +1,5 @@
<?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>
<string name="screen_polls_history_title">"Анкети"</string>
<string name="screen_room_change_role_section_users">"Членове"</string>
<string name="screen_room_details_add_topic_title">"Добавяне на тема"</string>

7
features/roomdetails/impl/src/main/res/values-cs/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_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_polls_history_title">"Hlasování"</string>
@ -117,5 +111,4 @@ @@ -117,5 +111,4 @@
<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_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>

17
features/roomdetails/impl/src/main/res/values-de/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_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_polls_history_title">"Umfragen"</string>
@ -20,15 +14,15 @@ @@ -20,15 +14,15 @@
<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_details">"Raum-Details anpassen"</string>
<string name="screen_room_change_permissions_room_name">"Raum-Name ändern"</string>
<string name="screen_room_change_permissions_room_topic">"Raum-Thema ändern"</string>
<string name="screen_room_change_permissions_room_name">"Raumname ä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_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_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_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_android">"(Ausstehend)"</string>
<string name="screen_room_change_role_moderators_admin_section_footer">"Administratoren haben automatisch Moderatorenrechte"</string>
@ -56,7 +50,7 @@ @@ -56,7 +50,7 @@
<string name="screen_room_details_notification_mode_default">"Standard"</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_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_share_room_title">"Teilen"</string>
<string name="screen_room_details_title">"Informationen"</string>
@ -116,5 +110,4 @@ @@ -116,5 +110,4 @@
<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_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>

7
features/roomdetails/impl/src/main/res/values-es/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_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_polls_history_title">"Encuestas"</string>
@ -51,5 +45,4 @@ @@ -51,5 +45,4 @@
<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_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>

7
features/roomdetails/impl/src/main/res/values-fr/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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 l’utilisateur"</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 l’utilisateur"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Une erreur s’est produite lors de la mise à jour du paramètre de notification."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Votre serveur d’accueil 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>
@ -116,5 +110,4 @@ @@ -116,5 +110,4 @@
<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_title">"Rôles et autorisations"</string>
<string name="screen_start_chat_error_starting_chat">"Une erreur s’est produite lors de la tentative de création de la discussion"</string>
</resources>

7
features/roomdetails/impl/src/main/res/values-hu/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_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_polls_history_title">"Szavazások"</string>
@ -116,5 +110,4 @@ @@ -116,5 +110,4 @@
<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_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>

7
features/roomdetails/impl/src/main/res/values-in/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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">"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_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>
@ -115,5 +109,4 @@ @@ -115,5 +109,4 @@
<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_title">"Peran dan perizinan"</string>
<string name="screen_start_chat_error_starting_chat">"Terjadi kesalahan saat mencoba memulai obrolan"</string>
</resources>

7
features/roomdetails/impl/src/main/res/values-it/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_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_polls_history_title">"Sondaggi"</string>
@ -113,5 +107,4 @@ @@ -113,5 +107,4 @@
<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_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>

7
features/roomdetails/impl/src/main/res/values-ro/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_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_polls_history_title">"Sondaje"</string>
@ -63,5 +57,4 @@ @@ -63,5 +57,4 @@
<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_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>

7
features/roomdetails/impl/src/main/res/values-ru/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_notification_settings_edit_failed_updating_default_mode">"При обновлении настроек уведомления произошла ошибка."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, в некоторых комнатах вы можете не получать уведомления."</string>
<string name="screen_polls_history_title">"Опросы"</string>
@ -117,5 +111,4 @@ @@ -117,5 +111,4 @@
<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_title">"Роли и разрешения"</string>
<string name="screen_start_chat_error_starting_chat">"Произошла ошибка при попытке открытия комнаты"</string>
</resources>

7
features/roomdetails/impl/src/main/res/values-sk/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_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_polls_history_title">"Ankety"</string>
@ -117,5 +111,4 @@ @@ -117,5 +111,4 @@
<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_title">"Roly a povolenia"</string>
<string name="screen_start_chat_error_starting_chat">"Pri pokuse o spustenie konverzácie sa vyskytla chyba"</string>
</resources>

7
features/roomdetails/impl/src/main/res/values-sv/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_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_details_add_topic_title">"Lägg till ämne"</string>
@ -48,5 +42,4 @@ @@ -48,5 +42,4 @@
<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_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>

7
features/roomdetails/impl/src/main/res/values-uk/translations.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_notification_settings_edit_failed_updating_default_mode">"Під час оновлення налаштувань сповіщень сталася помилка."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Ваш домашній сервер не підтримує цю опцію в зашифрованих кімнатах, ви можете не отримати сповіщення в деяких кімнатах."</string>
<string name="screen_polls_history_title">"Опитування"</string>
@ -113,5 +107,4 @@ @@ -113,5 +107,4 @@
<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_title">"Ролі та дозволи"</string>
<string name="screen_start_chat_error_starting_chat">"Під час спроби почати чат сталася помилка"</string>
</resources>

4
features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml

@ -1,9 +1,5 @@ @@ -1,9 +1,5 @@
<?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>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"更新通知設定時發生錯誤。"</string>
<string name="screen_polls_history_title">"所有投票"</string>
<string name="screen_room_change_permissions_everyone">"所有人"</string>

7
features/roomdetails/impl/src/main/res/values/localazy.xml

@ -1,11 +1,5 @@ @@ -1,11 +1,5 @@
<?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_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_polls_history_title">"Polls"</string>
@ -116,5 +110,4 @@ @@ -116,5 +110,4 @@
<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_title">"Roles and permissions"</string>
<string name="screen_start_chat_error_starting_chat">"An error occurred when trying to start a chat"</string>
</resources>

36
features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt

@ -25,9 +25,9 @@ import io.element.android.features.createroom.api.StartDMAction @@ -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.roomdetails.aMatrixRoom
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.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.AsyncData
import io.element.android.libraries.matrix.api.MatrixClient
@ -66,7 +66,7 @@ class RoomMemberDetailsPresenterTests { @@ -66,7 +66,7 @@ class RoomMemberDetailsPresenterTests {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(initialState.userId).isEqualTo(roomMember.userId.value)
assertThat(initialState.userId).isEqualTo(roomMember.userId)
assertThat(initialState.userName).isEqualTo(roomMember.displayName)
assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
assertThat(initialState.isBlocked).isEqualTo(AsyncData.Success(roomMember.isIgnored))
@ -157,12 +157,12 @@ class RoomMemberDetailsPresenterTests { @@ -157,12 +157,12 @@ class RoomMemberDetailsPresenterTests {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true))
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = true))
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()
ensureAllEventsConsumed()
@ -181,12 +181,12 @@ class RoomMemberDetailsPresenterTests { @@ -181,12 +181,12 @@ class RoomMemberDetailsPresenterTests {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf(roomMember.userId))
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false))
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf())
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
@ -202,12 +202,12 @@ class RoomMemberDetailsPresenterTests { @@ -202,12 +202,12 @@ class RoomMemberDetailsPresenterTests {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
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(RoomMemberDetailsEvents.ClearBlockUserError)
initialState.eventSink(UserProfileEvents.ClearBlockUserError)
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(false))
}
}
@ -221,12 +221,12 @@ class RoomMemberDetailsPresenterTests { @@ -221,12 +221,12 @@ class RoomMemberDetailsPresenterTests {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false))
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(RoomMemberDetailsEvents.ClearBlockUserError)
initialState.eventSink(UserProfileEvents.ClearBlockUserError)
assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true))
}
}
@ -238,12 +238,12 @@ class RoomMemberDetailsPresenterTests { @@ -238,12 +238,12 @@ class RoomMemberDetailsPresenterTests {
presenter.present()
}.test {
val initialState = awaitFirstItem()
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true))
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true))
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()
ensureAllEventsConsumed()
@ -264,18 +264,18 @@ class RoomMemberDetailsPresenterTests { @@ -264,18 +264,18 @@ class RoomMemberDetailsPresenterTests {
// Failure
startDMAction.givenExecuteResult(startDMFailureResult)
initialState.eventSink(RoomMemberDetailsEvents.StartDM)
initialState.eventSink(UserProfileEvents.StartDM)
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
awaitItem().also { state ->
assertThat(state.startDmActionState).isEqualTo(startDMFailureResult)
state.eventSink(RoomMemberDetailsEvents.ClearStartDMState)
state.eventSink(UserProfileEvents.ClearStartDMState)
}
// Success
startDMAction.givenExecuteResult(startDMSuccessResult)
awaitItem().also { state ->
assertThat(state.startDmActionState).isEqualTo(AsyncAction.Uninitialized)
state.eventSink(RoomMemberDetailsEvents.StartDM)
state.eventSink(UserProfileEvents.StartDM)
}
assertThat(awaitItem().startDmActionState).isInstanceOf(AsyncAction.Loading::class.java)
awaitItem().also { state ->

2
features/roomlist/impl/src/main/res/values-sv/translations.xml

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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_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>

2
features/roomlist/impl/src/main/res/values-zh-rTW/translations.xml

@ -8,6 +8,8 @@ @@ -8,6 +8,8 @@
<string name="screen_roomlist_a11y_create_message">"建立新的對話或聊天室"</string>
<string name="screen_roomlist_filter_favourites">"我的最愛"</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="session_verification_banner_message">"您似乎正在使用新的裝置。請使用另一個裝置進行驗證,以存取您的加密訊息。"</string>
<string name="session_verification_banner_title">"驗證這是您本人"</string>

15
features/securebackup/impl/src/main/res/values-de/translations.xml

@ -31,13 +31,14 @@ @@ -31,13 +31,14 @@
</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_title">"Bist du sicher, dass du das Backup ausschalten 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_point_1">"Keine Historie für verschlüsselte Nachrichten auf neuen Geräten"</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 deaktivierst, wird dein aktuelles Backup des Verschlüsselungsschlüssels entfernt und andere Sicherheitsfunktionen werden deaktiviert.
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_title">"Bist du sicher, dass du das Backup ausschalten 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_generate_key">"Erstelle einen neuen Wiederherstellungsschlüssel"</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">"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">"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_success">"Wiederherstellungsschlüssel geändert"</string>
<string name="screen_recovery_key_change_title">"Wiederherstellungsschlüssel ändern?"</string>
@ -55,7 +56,7 @@ @@ -55,7 +56,7 @@
" oder Passcode"
</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_title">"Bitte Wiederherstellungsschlüssel eingeben"</string>
<string name="screen_recovery_key_copied_to_clipboard">"Wiederherstellungsschlüssel kopiert"</string>

3
features/securebackup/impl/src/main/res/values-sv/translations.xml

@ -22,10 +22,13 @@ @@ -22,10 +22,13 @@
<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_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_placeholder">"Ange …"</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_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_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>

29
features/userprofile/api/build.gradle.kts

@ -0,0 +1,29 @@ @@ -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)
}

41
features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEntryPoint.kt

@ -0,0 +1,41 @@ @@ -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
}

69
features/userprofile/impl/build.gradle.kts

@ -0,0 +1,69 @@ @@ -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)
}

49
features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/DefaultUserProfileEntryPoint.kt

@ -0,0 +1,49 @@ @@ -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)
}
}
}
}

106
features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt

@ -0,0 +1,106 @@ @@ -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.shared.avatar.AvatarPreviewNode
import io.element.android.features.userprofile.impl.root.UserProfileNode
import io.element.android.features.userprofile.shared.UserProfileNodeHelper
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()
}
}

42
features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/di/UserProfileModule.kt

@ -0,0 +1,42 @@ @@ -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)
}
}
}
}

96
features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfileNode.kt

@ -0,0 +1,96 @@ @@ -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,
)
}
}

124
features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt

@ -0,0 +1,124 @@ @@ -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
)
}
}

238
features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTests.kt

@ -0,0 +1,238 @@ @@ -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
)
}
}

71
features/userprofile/shared/build.gradle.kts

@ -0,0 +1,71 @@ @@ -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)
}

18
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt → features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileEvents.kt

@ -1,5 +1,5 @@ @@ -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");
* you may not use this file except in compliance with the License.
@ -14,13 +14,13 @@ @@ -14,13 +14,13 @@
* limitations under the License.
*/
package io.element.android.features.roomdetails.impl.members.details
package io.element.android.features.userprofile.shared
sealed interface RoomMemberDetailsEvents {
data object StartDM : RoomMemberDetailsEvents
data object ClearStartDMState : RoomMemberDetailsEvents
data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents
data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents
data object ClearBlockUserError : RoomMemberDetailsEvents
data object ClearConfirmationDialog : RoomMemberDetailsEvents
sealed interface UserProfileEvents {
data object StartDM : UserProfileEvents
data object ClearStartDMState : UserProfileEvents
data class BlockUser(val needsConfirmation: Boolean = false) : UserProfileEvents
data class UnblockUser(val needsConfirmation: Boolean = false) : UserProfileEvents
data object ClearBlockUserError : UserProfileEvents
data object ClearConfirmationDialog : UserProfileEvents
}

13
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberHeaderSection.kt → features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt

@ -1,5 +1,5 @@ @@ -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");
* you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* 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.layout.Box
@ -37,13 +37,14 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -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.AvatarSize
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.testTag
@Composable
fun RoomMemberHeaderSection(
fun UserProfileHeaderSection(
avatarUrl: String?,
userId: String,
userId: UserId,
userName: String?,
openAvatarPreview: (url: String) -> Unit,
modifier: Modifier = Modifier
@ -51,7 +52,7 @@ fun RoomMemberHeaderSection( @@ -51,7 +52,7 @@ fun RoomMemberHeaderSection(
Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier = Modifier.size(70.dp)) {
Avatar(
avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.UserHeader),
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
modifier = Modifier
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.fillMaxSize()
@ -68,7 +69,7 @@ fun RoomMemberHeaderSection( @@ -68,7 +69,7 @@ fun RoomMemberHeaderSection(
Spacer(modifier = Modifier.height(6.dp))
}
Text(
text = userId,
text = userId.value,
style = ElementTheme.typography.fontBodyLgRegular,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier

6
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberMainActionsSection.kt → features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileMainActionsSection.kt

@ -1,5 +1,5 @@ @@ -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");
* you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* 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.Row
@ -27,7 +27,7 @@ import io.element.android.libraries.designsystem.components.button.MainActionBut @@ -27,7 +27,7 @@ import io.element.android.libraries.designsystem.components.button.MainActionBut
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun RoomMemberMainActionsSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) {
fun UserProfileMainActionsSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) {
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
MainActionButton(
title = stringResource(CommonStrings.action_share),

53
features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileNodeHelper.kt

@ -0,0 +1,53 @@ @@ -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)
}
}
}

53
features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfilePresenterHelper.kt

@ -0,0 +1,53 @@ @@ -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.
}
}

11
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt → features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileState.kt

@ -1,5 +1,5 @@ @@ -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");
* you may not use this file except in compliance with the License.
@ -14,21 +14,22 @@ @@ -14,21 +14,22 @@
* 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.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
data class RoomMemberDetailsState(
val userId: String,
data class UserProfileState(
val userId: UserId,
val userName: String?,
val avatarUrl: String?,
val isBlocked: AsyncData<Boolean>,
val startDmActionState: AsyncAction<RoomId>,
val displayConfirmationDialog: ConfirmationDialog?,
val isCurrentUser: Boolean,
val eventSink: (RoomMemberDetailsEvents) -> Unit
val eventSink: (UserProfileEvents) -> Unit
) {
enum class ConfirmationDialog {
Block,

33
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt → features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt

@ -1,5 +1,5 @@ @@ -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");
* you may not use this file except in compliance with the License.
@ -14,37 +14,38 @@ @@ -14,37 +14,38 @@
* 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 io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberDetailsState> {
override val values: Sequence<RoomMemberDetailsState>
open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> {
override val values: Sequence<UserProfileState>
get() = sequenceOf(
aRoomMemberDetailsState(),
aRoomMemberDetailsState(userName = null),
aRoomMemberDetailsState(isBlocked = AsyncData.Success(true)),
aRoomMemberDetailsState(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block),
aRoomMemberDetailsState(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock),
aRoomMemberDetailsState(isBlocked = AsyncData.Loading(true)),
aRoomMemberDetailsState(startDmActionState = AsyncAction.Loading),
aUserProfileState(),
aUserProfileState(userName = null),
aUserProfileState(isBlocked = AsyncData.Success(true)),
aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block),
aUserProfileState(displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock),
aUserProfileState(isBlocked = AsyncData.Loading(true)),
aUserProfileState(startDmActionState = AsyncAction.Loading),
// Add other states here
)
}
fun aRoomMemberDetailsState(
userId: String = "@daniel:domain.com",
fun aUserProfileState(
userId: UserId = UserId("@daniel:domain.com"),
userName: String? = "Daniel",
avatarUrl: String? = null,
isBlocked: AsyncData<Boolean> = AsyncData.Success(false),
startDmActionState: AsyncAction<RoomId> = AsyncAction.Uninitialized,
displayConfirmationDialog: RoomMemberDetailsState.ConfirmationDialog? = null,
displayConfirmationDialog: UserProfileState.ConfirmationDialog? = null,
isCurrentUser: Boolean = false,
eventSink: (RoomMemberDetailsEvents) -> Unit = {},
) = RoomMemberDetailsState(
eventSink: (UserProfileEvents) -> Unit = {},
) = UserProfileState(
userId = userId,
userName = userName,
avatarUrl = avatarUrl,

49
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt → features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt

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

8
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/avatar/AvatarPreviewNode.kt → features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/avatar/AvatarPreviewNode.kt

@ -1,5 +1,5 @@ @@ -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");
* you may not use this file except in compliance with the License.
@ -14,18 +14,18 @@ @@ -14,18 +14,18 @@
* 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.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
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.MediaViewerPresenter
@ContributesNode(RoomScope::class)
@ContributesNode(SessionScope::class)
class AvatarPreviewNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,

24
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogs.kt → features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogs.kt

@ -1,5 +1,5 @@ @@ -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");
* you may not use this file except in compliance with the License.
@ -14,41 +14,41 @@ @@ -14,41 +14,41 @@
* 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.ui.res.stringResource
import io.element.android.features.roomdetails.impl.R
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
import io.element.android.features.userprofile.shared.R
import io.element.android.features.userprofile.shared.UserProfileEvents
import io.element.android.features.userprofile.shared.UserProfileState
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
@Composable
fun BlockUserDialogs(state: RoomMemberDetailsState) {
fun BlockUserDialogs(state: UserProfileState) {
when (state.displayConfirmationDialog) {
null -> Unit
RoomMemberDetailsState.ConfirmationDialog.Block -> {
UserProfileState.ConfirmationDialog.Block -> {
BlockConfirmationDialog(
onBlockAction = {
state.eventSink(
RoomMemberDetailsEvents.BlockUser(
UserProfileEvents.BlockUser(
needsConfirmation = false
)
)
},
onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) }
onDismiss = { state.eventSink(UserProfileEvents.ClearConfirmationDialog) }
)
}
RoomMemberDetailsState.ConfirmationDialog.Unblock -> {
UserProfileState.ConfirmationDialog.Unblock -> {
UnblockConfirmationDialog(
onUnblockAction = {
state.eventSink(
RoomMemberDetailsEvents.UnblockUser(
UserProfileEvents.UnblockUser(
needsConfirmation = false
)
)
},
onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearConfirmationDialog) }
onDismiss = { state.eventSink(UserProfileEvents.ClearConfirmationDialog) }
)
}
}

34
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserSection.kt → features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt

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

10
features/userprofile/shared/src/main/res/values-be/translations.xml

@ -0,0 +1,10 @@ @@ -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>

7
features/userprofile/shared/src/main/res/values-bg/translations.xml

@ -0,0 +1,7 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values-cs/translations.xml

@ -0,0 +1,10 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values-de/translations.xml

@ -0,0 +1,10 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values-es/translations.xml

@ -0,0 +1,10 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values-fr/translations.xml

@ -0,0 +1,10 @@ @@ -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 l’utilisateur"</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 l’utilisateur"</string>
<string name="screen_start_chat_error_starting_chat">"Une erreur s’est produite lors de la tentative de création de la discussion"</string>
</resources>

10
features/userprofile/shared/src/main/res/values-hu/translations.xml

@ -0,0 +1,10 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values-it/translations.xml

@ -0,0 +1,10 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values-ro/translations.xml

@ -0,0 +1,10 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values-ru/translations.xml

@ -0,0 +1,10 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values-sk/translations.xml

@ -0,0 +1,10 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values-sv/translations.xml

@ -0,0 +1,10 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values-uk/translations.xml

@ -0,0 +1,10 @@ @@ -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>

7
features/userprofile/shared/src/main/res/values-zh-rTW/translations.xml

@ -0,0 +1,7 @@ @@ -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>

10
features/userprofile/shared/src/main/res/values/localazy.xml

@ -0,0 +1,10 @@ @@ -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>

42
features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/blockuser/BlockUserDialogsTest.kt → features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt

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

1
features/verifysession/impl/src/main/res/values-sv/translations.xml

@ -9,6 +9,7 @@ @@ -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_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_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_they_dont_match">"De matchar inte"</string>
<string name="screen_session_verification_they_match">"De matchar"</string>

8
features/verifysession/impl/src/main/res/values-zh-rTW/translations.xml

@ -1,7 +1,11 @@ @@ -1,7 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_identity_confirmed_title">"裝置已認證"</string>
<string name="screen_identity_use_another_device">"使用另一個裝置"</string>
<string name="screen_identity_confirmation_create_new_recovery_key">"建立新的復原金鑰"</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_session_verification_cancelled_subtitle">"似乎出了一點問題。有可能是因為等候逾時,或是請求被拒絕。"</string>
<string name="screen_session_verification_compare_emojis_subtitle">"確認顯示在其他工作階段上的表情符號是否和下方的相同。"</string>

2
libraries/eventformatter/impl/src/main/res/values-fr/translations.xml

@ -30,7 +30,7 @@ @@ -30,7 +30,7 @@
<string name="state_event_room_join_by_you">"Vous avez rejoint le salon"</string>
<string name="state_event_room_knock">"%1$s a demandé à rejoindre"</string>
<string name="state_event_room_knock_accepted">"%1$s a autorisé %2$s à rejoindre"</string>
<string name="state_event_room_knock_accepted_by_you">"Vous avez autoriser %1$s à joindre le salon"</string>
<string name="state_event_room_knock_accepted_by_you">"Vous avez autorisé %1$s à joindre le salon"</string>
<string name="state_event_room_knock_by_you">"Vous avez demandé à rejoindre"</string>
<string name="state_event_room_knock_denied">"%1$s a rejeté la demande de %2$s pour rejoindre"</string>
<string name="state_event_room_knock_denied_by_you">"Vous avez rejeté la demande de %1$s pour rejoindre"</string>

1
libraries/ui-strings/src/main/res/values-cs/translations.xml

@ -244,6 +244,7 @@ @@ -244,6 +244,7 @@
<string name="error_failed_loading_messages">"Načítání zpráv se nezdařilo"</string>
<string name="error_failed_locating_user">"%1$s nemá přístup k vaší poloze. Zkuste to prosím později."</string>
<string name="error_failed_uploading_voice_message">"Nepodařilo se nahrát hlasovou zprávu."</string>
<string name="error_message_not_found">"Zpráva nebyla nalezena"</string>
<string name="error_missing_location_auth_android">"%1$s nemá oprávnění k přístupu k vaší poloze. Přístup můžete povolit v Nastavení."</string>
<string name="error_missing_location_rationale_android">"%1$s nemá oprávnění k přístupu k vaší poloze. Povolit přístup níže."</string>
<string name="error_missing_microphone_voice_rationale_android">"%1$s nemá oprávnění k přístupu k mikrofonu. Povolte přístup k nahrávání hlasové zprávy."</string>

9
libraries/ui-strings/src/main/res/values-de/translations.xml

@ -158,7 +158,7 @@ @@ -158,7 +158,7 @@
<string name="common_modern">"Modern"</string>
<string name="common_mute">"Stummschalten"</string>
<string name="common_no_results">"Keine Ergebnisse"</string>
<string name="common_no_room_name">"Kein Raum-Name"</string>
<string name="common_no_room_name">"Kein Raumname"</string>
<string name="common_offline">"Offline"</string>
<string name="common_or">"oder"</string>
<string name="common_password">"Passwort"</string>
@ -177,9 +177,7 @@ @@ -177,9 +177,7 @@
<string name="common_privacy_policy">"Datenschutz­erklärung"</string>
<string name="common_reaction">"Reaktion"</string>
<string name="common_reactions">"Reaktionen"</string>
<string name="common_recovery_key">
<b>"Wiederherstellungsschlüssel"</b>
</string>
<string name="common_recovery_key">"Wiederherstellungsschlüssel"</string>
<string name="common_refreshing">"Wird erneuert…"</string>
<string name="common_replying_to">"%1$s antworten"</string>
<string name="common_report_a_bug">"Einen Fehler melden"</string>
@ -187,7 +185,7 @@ @@ -187,7 +185,7 @@
<string name="common_report_submitted">"Bericht eingereicht"</string>
<string name="common_rich_text_editor">"Rich-Text-Editor"</string>
<string name="common_room">"Raum"</string>
<string name="common_room_name">"Raum-Name"</string>
<string name="common_room_name">"Raumname"</string>
<string name="common_room_name_placeholder">"z.B. dein Projektname"</string>
<string name="common_saved_changes">"Gespeicherte Änderungen"</string>
<string name="common_saving">"Speichern"</string>
@ -242,6 +240,7 @@ @@ -242,6 +240,7 @@
<string name="error_failed_loading_messages">"Fehler beim Laden der Nachrichten"</string>
<string name="error_failed_locating_user">"%1$s konnte nicht auf deinen Standort zugreifen. Bitte versuche es später erneut."</string>
<string name="error_failed_uploading_voice_message">"Fehler beim Hochladen der Sprachnachricht."</string>
<string name="error_message_not_found">"Nachricht nicht gefunden"</string>
<string name="error_missing_location_auth_android">"%1$s hat keine Erlaubnis, auf deinen Standort zuzugreifen. Du kannst den Zugriff in den Einstellungen aktivieren."</string>
<string name="error_missing_location_rationale_android">"%1$s hat keine Erlaubnis, auf deinen Standort zuzugreifen. Aktiviere unten den Zugriff."</string>
<string name="error_missing_microphone_voice_rationale_android">"%1$s hat nicht die Erlaubnis auf dein Mikrofon zuzugreifen. Aktiviere den Zugriff, um eine Sprachnachricht aufzunehmen."</string>

1
libraries/ui-strings/src/main/res/values-hu/translations.xml

@ -240,6 +240,7 @@ @@ -240,6 +240,7 @@
<string name="error_failed_loading_messages">"Nem sikerült betölteni az üzeneteket"</string>
<string name="error_failed_locating_user">"Az %1$s nem tudta elérni a tartózkodási helyét. Próbálja újra később."</string>
<string name="error_failed_uploading_voice_message">"Nem sikerült feltölteni a hangüzenetét."</string>
<string name="error_message_not_found">"Az üzenet nem található"</string>
<string name="error_missing_location_auth_android">"Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyéhez. Ezt a beállításokban engedélyezheti."</string>
<string name="error_missing_location_rationale_android">"Az %1$snek nincs engedélye, hogy hozzáférjen a tartózkodási helyéhez. Engedélyezze alább az elérését."</string>
<string name="error_missing_microphone_voice_rationale_android">"Az %1$snek nincs engedélye, hogy hozzáférjen a mikrofonhoz. Engedélyezze, hogy tudjon hangüzenetet felvenni."</string>

7
libraries/ui-strings/src/main/res/values-sv/translations.xml

@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
<string name="a11y_poll_end">"Avslutade omröstning"</string>
<string name="a11y_send_files">"Skicka filer"</string>
<string name="a11y_show_password">"Visa lösenord"</string>
<string name="a11y_start_call">"Starta ett samtal"</string>
<string name="a11y_user_menu">"Användarmeny"</string>
<string name="a11y_voice_message_record">"Spela in röstmeddelande."</string>
<string name="action_accept">"Godkänn"</string>
@ -39,6 +40,7 @@ @@ -39,6 +40,7 @@
<string name="action_edit_poll">"Redigera omröstning"</string>
<string name="action_enable">"Aktivera"</string>
<string name="action_end_poll">"Avsluta omröstning"</string>
<string name="action_enter_pin">"Ange PIN-kod"</string>
<string name="action_forgot_password">"Glömt lösenordet?"</string>
<string name="action_forward">"Vidarebefordra"</string>
<string name="action_invite">"Bjud in"</string>
@ -146,8 +148,10 @@ @@ -146,8 +148,10 @@
<string name="common_refreshing">"Uppdaterar …"</string>
<string name="common_replying_to">"Svarar till %1$s"</string>
<string name="common_report_a_bug">"Rapportera en bugg"</string>
<string name="common_report_a_problem">"Rapportera ett problem"</string>
<string name="common_report_submitted">"Rapport inskickad"</string>
<string name="common_rich_text_editor">"Riktextredigerare"</string>
<string name="common_room">"Rum"</string>
<string name="common_room_name">"Rumsnamn"</string>
<string name="common_room_name_placeholder">"t.ex. ditt projektnamn"</string>
<string name="common_screen_lock">"Skärmlås"</string>
@ -159,6 +163,7 @@ @@ -159,6 +163,7 @@
<string name="common_server_url">"Server-URL"</string>
<string name="common_settings">"Inställningar"</string>
<string name="common_shared_location">"Delade plats"</string>
<string name="common_signing_out">"Loggar ut"</string>
<string name="common_starting_chat">"Startar chatt …"</string>
<string name="common_sticker">"Dekal"</string>
<string name="common_success">"Lyckades"</string>
@ -178,9 +183,11 @@ @@ -178,9 +183,11 @@
<string name="common_username">"Användarnamn"</string>
<string name="common_verification_cancelled">"Verifiering avbruten"</string>
<string name="common_verification_complete">"Verifieringen slutförd"</string>
<string name="common_verify_device">"Verifiera enheten"</string>
<string name="common_video">"Video"</string>
<string name="common_voice_message">"Röstmeddelande"</string>
<string name="common_waiting">"Väntar …"</string>
<string name="common_waiting_for_decryption_key">"Väntar på detta meddelande"</string>
<string name="dialog_title_confirmation">"Bekräftelse"</string>
<string name="dialog_title_error">"Fel"</string>
<string name="dialog_title_success">"Lyckades"</string>

9
tools/localazy/config.json

@ -145,16 +145,21 @@ @@ -145,16 +145,21 @@
"includeRegex" : [
"screen_room_details_.*",
"screen_room_member_list_.*",
"screen_dm_details_.*",
"screen_room_notification_settings_.*",
"screen_notification_settings_edit_failed_updating_default_mode",
"screen_polls_history_title",
"screen_notification_settings_mentions_only_disclaimer",
"screen_start_chat_error_starting_chat",
"screen_room_change_.*",
"screen_room_roles_.*"
]
},
{
"name" : ":features:userprofile:shared",
"includeRegex" : [
"screen_start_chat_error_starting_chat",
"screen_dm_details_.*"
]
},
{
"name" : ":features:messages:impl",
"includeRegex" : [

Loading…
Cancel
Save