Merge branch 'develop' into feature/fga/join_space
This commit is contained in:
commit
f3f19ec476
319 changed files with 2828 additions and 1838 deletions
|
|
@ -9,6 +9,7 @@
|
|||
<locale android:name="el"/>
|
||||
<locale android:name="en"/>
|
||||
<locale android:name="en_US"/>
|
||||
<locale android:name="eo"/>
|
||||
<locale android:name="es"/>
|
||||
<locale android:name="et"/>
|
||||
<locale android:name="eu"/>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
|
|
@ -111,7 +110,7 @@ class RoomFlowNode(
|
|||
data class JoinedRoom(val roomId: RoomId) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class Space(val spaceId: RoomId) : NavTarget
|
||||
data class JoinedSpace(val spaceId: RoomId) : NavTarget
|
||||
}
|
||||
|
||||
override fun onBuilt() {
|
||||
|
|
@ -149,30 +148,28 @@ class RoomFlowNode(
|
|||
.withPreviousValue()
|
||||
combine(currentMembershipFlow, isSpaceFlow) { (previousMembership, membership), isSpace ->
|
||||
Timber.d("Room membership: $membership")
|
||||
when (membership) {
|
||||
CurrentUserMembership.JOINED -> {
|
||||
if (isSpace) {
|
||||
backstack.newRoot(NavTarget.Space(spaceId = roomId))
|
||||
} else {
|
||||
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
||||
}
|
||||
if (membership == CurrentUserMembership.JOINED) {
|
||||
if (isSpace) {
|
||||
backstack.newRoot(NavTarget.JoinedSpace(spaceId = roomId))
|
||||
} else {
|
||||
backstack.newRoot(NavTarget.JoinedRoom(roomId))
|
||||
}
|
||||
else -> {
|
||||
if (membership == CurrentUserMembership.LEFT && previousMembership == CurrentUserMembership.JOINED) {
|
||||
// The user left the room in this device, remove the room from the backstack
|
||||
if (!membershipUpdateFlow.first().isUserInRoom) {
|
||||
navigateUp()
|
||||
}
|
||||
} else {
|
||||
// Was invited or the room is not known, display the join room screen
|
||||
backstack.newRoot(
|
||||
NavTarget.JoinRoom(
|
||||
roomId = roomId,
|
||||
serverNames = serverNames,
|
||||
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
|
||||
)
|
||||
} else {
|
||||
val leavingFromCurrentDevice =
|
||||
membership == CurrentUserMembership.LEFT &&
|
||||
previousMembership == CurrentUserMembership.JOINED &&
|
||||
membershipUpdateFlow.replayCache.lastOrNull()?.isUserInRoom == false
|
||||
|
||||
if (leavingFromCurrentDevice) {
|
||||
navigateUp()
|
||||
} else {
|
||||
backstack.newRoot(
|
||||
NavTarget.JoinRoom(
|
||||
roomId = roomId,
|
||||
serverNames = serverNames,
|
||||
trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
|
|
@ -214,7 +211,7 @@ class RoomFlowNode(
|
|||
)
|
||||
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
|
||||
}
|
||||
is NavTarget.Space -> {
|
||||
is NavTarget.JoinedSpace -> {
|
||||
val spaceCallback = plugins<SpaceEntryPoint.Callback>().single()
|
||||
spaceEntryPoint.nodeBuilder(this, buildContext)
|
||||
.inputs(SpaceEntryPoint.Inputs(roomId = navTarget.spaceId))
|
||||
|
|
|
|||
|
|
@ -93,6 +93,8 @@ allprojects {
|
|||
// Fix compilation warning for annotations
|
||||
// See https://youtrack.jetbrains.com/issue/KT-73255/Change-defaulting-rule-for-annotations for more details
|
||||
freeCompilerArgs.add("-Xannotation-default-target=first-only")
|
||||
// Opt-in to context receivers
|
||||
freeCompilerArgs.add("-Xcontext-parameters")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,5 +41,8 @@ data class WidgetMessage(
|
|||
|
||||
@SerialName("send_event")
|
||||
SendEvent,
|
||||
|
||||
@SerialName("content_loaded")
|
||||
ContentLoaded,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class CallScreenPresenter(
|
|||
val urlState = remember { mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized) }
|
||||
val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) }
|
||||
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
|
||||
var isJoinedCall by rememberSaveable { mutableStateOf(false) }
|
||||
var isWidgetLoaded by rememberSaveable { mutableStateOf(false) }
|
||||
var ignoreWebViewError by rememberSaveable { mutableStateOf(false) }
|
||||
var webViewError by remember { mutableStateOf<String?>(null) }
|
||||
val languageTag = languageTagProvider.provideLanguageTag()
|
||||
|
|
@ -139,8 +139,8 @@ class CallScreenPresenter(
|
|||
if (parsedMessage?.direction == WidgetMessage.Direction.FromWidget) {
|
||||
if (parsedMessage.action == WidgetMessage.Action.Close) {
|
||||
close(callWidgetDriver.value, navigator)
|
||||
} else if (parsedMessage.action == WidgetMessage.Action.Join) {
|
||||
isJoinedCall = true
|
||||
} else if (parsedMessage.action == WidgetMessage.Action.ContentLoaded) {
|
||||
isWidgetLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -151,8 +151,8 @@ class CallScreenPresenter(
|
|||
// Wait for the call to be joined, if it takes too long, we display an error
|
||||
delay(10.seconds)
|
||||
|
||||
if (!isJoinedCall) {
|
||||
Timber.w("The call took too long to be joined. Displaying an error before exiting.")
|
||||
if (!isWidgetLoaded) {
|
||||
Timber.w("The call took too long to load. Displaying an error before exiting.")
|
||||
|
||||
// This will display a simple 'Sorry, an error occurred' dialog and force the user to exit the call
|
||||
webViewError = ""
|
||||
|
|
@ -165,10 +165,10 @@ class CallScreenPresenter(
|
|||
is CallScreenEvents.Hangup -> {
|
||||
val widgetId = callWidgetDriver.value?.id
|
||||
val interceptor = messageInterceptor.value
|
||||
if (widgetId != null && interceptor != null && isJoinedCall) {
|
||||
if (widgetId != null && interceptor != null && isWidgetLoaded) {
|
||||
// If the call was joined, we need to hang up first. Then the UI will be dismissed automatically.
|
||||
sendHangupMessage(widgetId, interceptor)
|
||||
isJoinedCall = false
|
||||
isWidgetLoaded = false
|
||||
|
||||
coroutineScope.launch {
|
||||
// Wait for a couple of seconds to receive the hangup message
|
||||
|
|
@ -198,7 +198,7 @@ class CallScreenPresenter(
|
|||
urlState = urlState.value,
|
||||
webViewError = webViewError,
|
||||
userAgent = userAgent,
|
||||
isCallActive = isJoinedCall,
|
||||
isCallActive = isWidgetLoaded,
|
||||
isInWidgetMode = isInWidgetMode,
|
||||
eventSink = { handleEvents(it) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - a received 'joined' action makes the call to be active`() = runTest {
|
||||
fun `present - a received 'content loaded' action makes the call to be active`() = runTest {
|
||||
val navigator = FakeCallScreenNavigator()
|
||||
val widgetDriver = FakeMatrixWidgetDriver()
|
||||
val presenter = createCallScreenPresenter(
|
||||
|
|
@ -238,7 +238,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
messageInterceptor.givenInterceptedMessage(
|
||||
"""
|
||||
{
|
||||
"action":"io.element.join",
|
||||
"action":"content_loaded",
|
||||
"api":"fromWidget",
|
||||
"widgetId":"1",
|
||||
"requestId":"1"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_change_permissions_administrators">"Gweinyddwyr yn unig"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Gwahardd pobl"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Dileu negeseuon"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Tynnu negeseuon"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Pawb"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Gwahodd pobl a derbyn ceisiadau i ymuno"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Cymedroli aelodau"</string>
|
||||
|
|
@ -17,13 +17,17 @@
|
|||
<string name="screen_room_change_role_administrators_title">"Golygu Gweinyddwyr"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych chi\'n hyrwyddo\'r defnyddiwr i gael yr un lefel pŵer â chi."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Ychwanegu Gweinyddwr?"</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_description">"Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych yn trosglwyddo\'r berchnogaeth i\'r defnyddwyr a ddewiswyd. Unwaith y byddwch yn gadael bydd hyn yn barhaol."</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_title">"Trosglwyddo perchnogaeth?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Gostwng"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Fyddwch chi ddim yn gallu dadwneud y newid hwn gan eich bod yn israddio eich hun, os mai chi yw\'r defnyddiwr breintiedig olaf yn yr ystafell bydd yn amhosibl adennill breintiau."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Israddio eich hun?"</string>
|
||||
<string name="screen_room_change_role_invited_member_name">"%1$s (Yn aros)"</string>
|
||||
<string name="screen_room_change_role_invited_member_name_android">"Yn aros"</string>
|
||||
<string name="screen_room_change_role_moderators_admin_section_footer">"Mae gan weinyddwyr freintiau cymedrolwr yn awtomatig"</string>
|
||||
<string name="screen_room_change_role_moderators_owner_section_footer">"Mae gan berchnogion freintiau gweinyddwr yn awtomatig."</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Golygu Cymedrolwyr"</string>
|
||||
<string name="screen_room_change_role_owners_title">"Dewiswch Berchnogion"</string>
|
||||
<string name="screen_room_change_role_section_administrators">"Gweinyddwyr"</string>
|
||||
<string name="screen_room_change_role_section_moderators">"Cymedrolwyr"</string>
|
||||
<string name="screen_room_change_role_section_users">"Aelodau"</string>
|
||||
|
|
@ -48,15 +52,18 @@
|
|||
<string name="screen_room_member_list_pending_header_title">"Dan ystyriaeth"</string>
|
||||
<string name="screen_room_member_list_role_administrator">"Gweinyddwr"</string>
|
||||
<string name="screen_room_member_list_role_moderator">"Cymedrolwr"</string>
|
||||
<string name="screen_room_member_list_role_owner">"Perchennog"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Aelodau\'r ystafell"</string>
|
||||
<string name="screen_room_member_list_unbanning_user">"Dad-wahardd %1$s"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Gweinyddwyr"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins_and_owners">"Gweinyddwyr a pherchnogion"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Newid fy rôl"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Israddio aelod"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Israddio cymedrolwr"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Cymedroli aelodau"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Negeseuon a chynnwys"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Cymedrolwyr"</string>
|
||||
<string name="screen_room_roles_and_permissions_owners">"Perchnogion"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Caniatâd"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Ailosod caniatâd"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Ar ôl i chi ailosod caniatâd, byddwch yn colli\'r gosodiadau cyfredol."</string>
|
||||
|
|
|
|||
|
|
@ -16,12 +16,12 @@
|
|||
<string name="screen_room_change_permissions_send_messages">"Viestien lähettäminen"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Muokkaa ylläpitäjiä"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Et voi peruuttaa tätä toimenpidettä. Ylennät käyttäjän samalle oikeustasolle kuin sinä."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Lisää ylläpitäjä?"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Lisätäänkö ylläpitäjä?"</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_description">"Et voi kumota tätä toimintoa. Olet siirtämässä omistajuuden valituille käyttäjille. Kun poistut, muutos on pysyvä."</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_title">"Siirretäänkö omistajuus?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Alenna"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Et voi perua tätä muutosta, koska olet alentamassa itseäsi. Jos olet viimeinen oikeutettu henkilö tässä huoneessa, oikeuksia ei voi enää saada takaisin."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Alenna itsesi?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Haluatko alentaa itsesi?"</string>
|
||||
<string name="screen_room_change_role_invited_member_name">"%1$s (Kutsuttu)"</string>
|
||||
<string name="screen_room_change_role_invited_member_name_android">"(Kutsuttu)"</string>
|
||||
<string name="screen_room_change_role_moderators_admin_section_footer">"Ylläpitäjillä on automaattisesti valvojan oikeudet"</string>
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
<string name="screen_room_change_role_section_moderators">"Valvojat"</string>
|
||||
<string name="screen_room_change_role_section_users">"Jäsenet"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Sinulla on tallentamattomia muutoksia"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Tallenna muutokset?"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Tallennetaanko muutokset?"</string>
|
||||
<string name="screen_room_member_list_banned_empty">"Tässä huoneessa ei ole porttikieltoja"</string>
|
||||
<plurals name="screen_room_member_list_header_title">
|
||||
<item quantity="one">"%1$d henkilö"</item>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_deactivate_account_list_item_3">"Delete your account information from our server."</string>
|
||||
</resources>
|
||||
|
|
@ -15,9 +15,6 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
interface FtueService {
|
||||
/** The current state of the FTUE. */
|
||||
val state: StateFlow<FtueState>
|
||||
|
||||
/** Reset the FTUE state. */
|
||||
suspend fun reset()
|
||||
}
|
||||
|
||||
/** The state of the FTUE. */
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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
|
||||
|
|
@ -30,18 +29,16 @@ import io.element.android.features.ftue.impl.notifications.NotificationsOptInNod
|
|||
import io.element.android.features.ftue.impl.sessionverification.FtueSessionVerificationFlowNode
|
||||
import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||
import io.element.android.features.ftue.impl.state.FtueStep
|
||||
import io.element.android.features.ftue.impl.state.InternalFtueState
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
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.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
|
|
@ -49,9 +46,8 @@ import kotlinx.parcelize.Parcelize
|
|||
class FtueFlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val ftueState: DefaultFtueService,
|
||||
private val defaultFtueService: DefaultFtueService,
|
||||
private val analyticsEntryPoint: AnalyticsEntryPoint,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||
) : BaseFlowNode<FtueFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -80,19 +76,11 @@ class FtueFlowNode(
|
|||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
|
||||
lifecycle.subscribe(onCreate = {
|
||||
moveToNextStepIfNeeded()
|
||||
})
|
||||
|
||||
analyticsService.didAskUserConsentFlow
|
||||
.distinctUntilChanged()
|
||||
.onEach { moveToNextStepIfNeeded() }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
ftueState.isVerificationStatusKnown
|
||||
.filter { it }
|
||||
.onEach { moveToNextStepIfNeeded() }
|
||||
defaultFtueService.ftueStepStateFlow
|
||||
.filterIsInstance(InternalFtueState.Incomplete::class)
|
||||
.onEach {
|
||||
showStep(it.nextStep)
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +92,7 @@ class FtueFlowNode(
|
|||
is NavTarget.SessionVerification -> {
|
||||
val callback = object : FtueSessionVerificationFlowNode.Callback {
|
||||
override fun onDone() {
|
||||
moveToNextStepIfNeeded()
|
||||
defaultFtueService.onUserCompletedSessionVerification()
|
||||
}
|
||||
}
|
||||
createNode<FtueSessionVerificationFlowNode>(buildContext, listOf(callback))
|
||||
|
|
@ -112,7 +100,7 @@ class FtueFlowNode(
|
|||
NavTarget.NotificationsOptIn -> {
|
||||
val callback = object : NotificationsOptInNode.Callback {
|
||||
override fun onNotificationsOptInFinished() {
|
||||
moveToNextStepIfNeeded()
|
||||
defaultFtueService.updateFtueStep()
|
||||
}
|
||||
}
|
||||
createNode<NotificationsOptInNode>(buildContext, listOf(callback))
|
||||
|
|
@ -123,7 +111,7 @@ class FtueFlowNode(
|
|||
NavTarget.LockScreenSetup -> {
|
||||
val callback = object : LockScreenEntryPoint.Callback {
|
||||
override fun onSetupDone() {
|
||||
moveToNextStepIfNeeded()
|
||||
defaultFtueService.updateFtueStep()
|
||||
}
|
||||
}
|
||||
lockScreenEntryPoint.nodeBuilder(this, buildContext, LockScreenEntryPoint.Target.Setup)
|
||||
|
|
@ -133,8 +121,8 @@ class FtueFlowNode(
|
|||
}
|
||||
}
|
||||
|
||||
private fun moveToNextStepIfNeeded() = lifecycleScope.launch {
|
||||
when (ftueState.getNextStep()) {
|
||||
private fun showStep(ftueStep: FtueStep) {
|
||||
when (ftueStep) {
|
||||
FtueStep.WaitingForInitialState -> {
|
||||
backstack.newRoot(NavTarget.Placeholder)
|
||||
}
|
||||
|
|
@ -150,7 +138,6 @@ class FtueFlowNode(
|
|||
FtueStep.LockscreenSetup -> {
|
||||
backstack.newRoot(NavTarget.LockScreenSetup)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ package io.element.android.features.ftue.impl.state
|
|||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.features.ftue.api.state.FtueService
|
||||
import io.element.android.features.ftue.api.state.FtueState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
|
|
@ -26,61 +26,70 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
@SingleIn(SessionScope::class)
|
||||
@Inject
|
||||
class DefaultFtueService(
|
||||
private val sdkVersionProvider: BuildVersionSdkIntProvider,
|
||||
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val permissionStateProvider: PermissionStateProvider,
|
||||
private val lockScreenService: LockScreenService,
|
||||
private val sessionVerificationService: SessionVerificationService,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
) : FtueService {
|
||||
override val state = MutableStateFlow<FtueState>(FtueState.Unknown)
|
||||
private val userNeedsToConfirmSessionVerificationSuccess = MutableStateFlow(false)
|
||||
|
||||
/**
|
||||
* This flow emits true when the FTUE flow is ready to be displayed.
|
||||
* In this case, the FTUE flow is ready when the session verification status is known.
|
||||
*/
|
||||
val isVerificationStatusKnown = sessionVerificationService.sessionVerifiedStatus
|
||||
.map { it != SessionVerifiedStatus.Unknown }
|
||||
.distinctUntilChanged()
|
||||
val ftueStepStateFlow = MutableStateFlow<InternalFtueState>(InternalFtueState.Unknown)
|
||||
|
||||
override suspend fun reset() {
|
||||
analyticsService.reset()
|
||||
if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||
permissionStateProvider.resetPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
override val state = ftueStepStateFlow
|
||||
.mapState {
|
||||
when (it) {
|
||||
is InternalFtueState.Unknown -> FtueState.Unknown
|
||||
is InternalFtueState.Incomplete -> FtueState.Incomplete
|
||||
is InternalFtueState.Complete -> FtueState.Complete
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
combine(
|
||||
sessionVerificationService.sessionVerifiedStatus.onEach { sessionVerifiedStatus ->
|
||||
if (sessionVerifiedStatus == SessionVerifiedStatus.NotVerified) {
|
||||
// Ensure we wait for the user to confirm the session verified screen before going further
|
||||
userNeedsToConfirmSessionVerificationSuccess.value = true
|
||||
}
|
||||
},
|
||||
userNeedsToConfirmSessionVerificationSuccess,
|
||||
analyticsService.didAskUserConsentFlow.distinctUntilChanged(),
|
||||
) {
|
||||
updateFtueStep()
|
||||
}
|
||||
.launchIn(sessionCoroutineScope)
|
||||
}
|
||||
|
||||
fun updateFtueStep() = sessionCoroutineScope.launch {
|
||||
val step = getNextStep(null)
|
||||
ftueStepStateFlow.value = when (step) {
|
||||
null -> InternalFtueState.Complete
|
||||
else -> InternalFtueState.Incomplete(step)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
sessionVerificationService.sessionVerifiedStatus
|
||||
.onEach { updateState() }
|
||||
.launchIn(sessionCoroutineScope)
|
||||
|
||||
analyticsService.didAskUserConsentFlow
|
||||
.distinctUntilChanged()
|
||||
.onEach { updateState() }
|
||||
.launchIn(sessionCoroutineScope)
|
||||
}
|
||||
|
||||
suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||
when (currentStep) {
|
||||
private suspend fun getNextStep(completedStep: FtueStep? = null): FtueStep? =
|
||||
when (completedStep) {
|
||||
null -> if (!isSessionVerificationStateReady()) {
|
||||
FtueStep.WaitingForInitialState
|
||||
} else {
|
||||
getNextStep(FtueStep.WaitingForInitialState)
|
||||
}
|
||||
FtueStep.WaitingForInitialState -> if (isSessionNotVerified()) {
|
||||
FtueStep.WaitingForInitialState -> if (isSessionNotVerified() || userNeedsToConfirmSessionVerificationSuccess.value) {
|
||||
FtueStep.SessionVerification
|
||||
} else {
|
||||
getNextStep(FtueStep.SessionVerification)
|
||||
|
|
@ -108,9 +117,6 @@ class DefaultFtueService(
|
|||
}
|
||||
|
||||
private suspend fun isSessionNotVerified(): Boolean {
|
||||
// Wait until the session verification status is known
|
||||
isVerificationStatusKnown.filter { it }.first()
|
||||
|
||||
return sessionVerificationService.sessionVerifiedStatus.value == SessionVerifiedStatus.NotVerified && !canSkipVerification()
|
||||
}
|
||||
|
||||
|
|
@ -137,14 +143,8 @@ class DefaultFtueService(
|
|||
return lockScreenService.isSetupRequired().first()
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal suspend fun updateState() {
|
||||
val nextStep = getNextStep()
|
||||
state.value = when {
|
||||
// Final state, there aren't any more next steps
|
||||
nextStep == null -> FtueState.Complete
|
||||
else -> FtueState.Incomplete
|
||||
}
|
||||
fun onUserCompletedSessionVerification() {
|
||||
userNeedsToConfirmSessionVerificationSuccess.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.ftue.impl.state
|
||||
|
||||
sealed interface InternalFtueState {
|
||||
data object Unknown : InternalFtueState
|
||||
|
||||
data class Incomplete(
|
||||
val nextStep: FtueStep,
|
||||
) : InternalFtueState
|
||||
|
||||
data object Complete : InternalFtueState
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_identity_confirmation_create_new_recovery_key">"Create a new backup password"</string>
|
||||
<string name="screen_identity_confirmation_subtitle">"Confirm this device to set up secure messaging."</string>
|
||||
<string name="screen_identity_confirmation_title">"Confirm it\'s you"</string>
|
||||
<string name="screen_identity_confirmation_use_recovery_key">"Use backup password"</string>
|
||||
<string name="screen_identity_confirmed_title">"Device confirmed"</string>
|
||||
<string name="screen_session_verification_enter_recovery_key">"Enter backup password"</string>
|
||||
</resources>
|
||||
|
|
@ -14,7 +14,6 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -36,8 +35,7 @@ class DefaultFtueEntryPointTest {
|
|||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
analyticsEntryPoint = { _, _ -> lambdaError() },
|
||||
ftueState = createDefaultFtueService(),
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
defaultFtueService = createDefaultFtueService(),
|
||||
lockScreenEntryPoint = object : LockScreenEntryPoint {
|
||||
override fun nodeBuilder(
|
||||
parentNode: com.bumble.appyx.core.node.Node,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import io.element.android.features.ftue.api.state.FtueState
|
||||
import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||
import io.element.android.features.ftue.impl.state.FtueStep
|
||||
import io.element.android.features.ftue.impl.state.InternalFtueState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||
|
|
@ -26,8 +27,6 @@ import io.element.android.services.analytics.api.AnalyticsService
|
|||
import io.element.android.services.analytics.noop.NoopAnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -69,9 +68,11 @@ class DefaultFtueServiceTest {
|
|||
analyticsService.setDidAskUserConsent()
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
service.updateState()
|
||||
|
||||
assertThat(service.state.value).isEqualTo(FtueState.Complete)
|
||||
service.updateFtueStep()
|
||||
service.state.test {
|
||||
assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
|
||||
assertThat(awaitItem()).isEqualTo(FtueState.Complete)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -90,9 +91,11 @@ class DefaultFtueServiceTest {
|
|||
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
service.updateState()
|
||||
|
||||
assertThat(service.state.value).isEqualTo(FtueState.Complete)
|
||||
service.updateFtueStep()
|
||||
service.state.test {
|
||||
assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
|
||||
assertThat(awaitItem()).isEqualTo(FtueState.Complete)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -109,35 +112,30 @@ class DefaultFtueServiceTest {
|
|||
permissionStateProvider = permissionStateProvider,
|
||||
lockScreenService = lockScreenService,
|
||||
)
|
||||
val steps = mutableListOf<FtueStep?>()
|
||||
|
||||
// Session verification
|
||||
steps.add(service.getNextStep(steps.lastOrNull()))
|
||||
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||
|
||||
// Notifications opt in
|
||||
steps.add(service.getNextStep(steps.lastOrNull()))
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
|
||||
// Entering PIN code
|
||||
steps.add(service.getNextStep(steps.lastOrNull()))
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
|
||||
// Analytics opt in
|
||||
steps.add(service.getNextStep(steps.lastOrNull()))
|
||||
analyticsService.setDidAskUserConsent()
|
||||
|
||||
// Final step (null)
|
||||
steps.add(service.getNextStep(steps.lastOrNull()))
|
||||
|
||||
assertThat(steps).containsExactly(
|
||||
FtueStep.SessionVerification,
|
||||
FtueStep.NotificationsOptIn,
|
||||
FtueStep.LockscreenSetup,
|
||||
FtueStep.AnalyticsOptIn,
|
||||
// Final state
|
||||
null,
|
||||
)
|
||||
service.ftueStepStateFlow.test {
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
|
||||
// Session verification
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.SessionVerification))
|
||||
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
// User completes verification
|
||||
service.onUserCompletedSessionVerification()
|
||||
// Notifications opt in
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.NotificationsOptIn))
|
||||
permissionStateProvider.setPermissionGranted()
|
||||
// Simulate event from NotificationsOptInNode.Callback.onNotificationsOptInFinished
|
||||
service.updateFtueStep()
|
||||
// Entering PIN code
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.LockscreenSetup))
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
// Simulate event from LockScreenEntryPoint.Callback.onSetupDone()
|
||||
service.updateFtueStep()
|
||||
// Analytics opt in
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
|
||||
analyticsService.setDidAskUserConsent()
|
||||
// Final step
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -158,10 +156,13 @@ class DefaultFtueServiceTest {
|
|||
permissionStateProvider.setPermissionGranted()
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
|
||||
assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||
|
||||
analyticsService.setDidAskUserConsent()
|
||||
assertThat(service.getNextStep(null)).isNull()
|
||||
service.ftueStepStateFlow.test {
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
|
||||
// Analytics opt in
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
|
||||
analyticsService.setDidAskUserConsent()
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -180,51 +181,13 @@ class DefaultFtueServiceTest {
|
|||
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
lockScreenService.setIsPinSetup(true)
|
||||
|
||||
assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)
|
||||
|
||||
analyticsService.setDidAskUserConsent()
|
||||
assertThat(service.getNextStep(null)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reset do the expected actions S`() = runTest {
|
||||
val resetAnalyticsLambda = lambdaRecorder<Unit> { }
|
||||
val analyticsService = FakeAnalyticsService(
|
||||
resetLambda = resetAnalyticsLambda
|
||||
)
|
||||
val resetPermissionLambda = lambdaRecorder<String, Unit> { }
|
||||
val permissionStateProvider = FakePermissionStateProvider(
|
||||
resetPermissionLambda = resetPermissionLambda
|
||||
)
|
||||
val service = createDefaultFtueService(
|
||||
sdkIntVersion = Build.VERSION_CODES.S,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
service.reset()
|
||||
resetAnalyticsLambda.assertions().isCalledOnce()
|
||||
resetPermissionLambda.assertions().isNeverCalled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reset do the expected actions TIRAMISU`() = runTest {
|
||||
val resetLambda = lambdaRecorder<Unit> { }
|
||||
val analyticsService = FakeAnalyticsService(
|
||||
resetLambda = resetLambda
|
||||
)
|
||||
val resetPermissionLambda = lambdaRecorder<String, Unit> { }
|
||||
val permissionStateProvider = FakePermissionStateProvider(
|
||||
resetPermissionLambda = resetPermissionLambda
|
||||
)
|
||||
val service = createDefaultFtueService(
|
||||
sdkIntVersion = Build.VERSION_CODES.TIRAMISU,
|
||||
permissionStateProvider = permissionStateProvider,
|
||||
analyticsService = analyticsService,
|
||||
)
|
||||
service.reset()
|
||||
resetLambda.assertions().isCalledOnce()
|
||||
resetPermissionLambda.assertions().isCalledOnce()
|
||||
.with(value("android.permission.POST_NOTIFICATIONS"))
|
||||
service.ftueStepStateFlow.test {
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Unknown)
|
||||
// Analytics opt in
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Incomplete(FtueStep.AnalyticsOptIn))
|
||||
analyticsService.setDidAskUserConsent()
|
||||
assertThat(awaitItem()).isEqualTo(InternalFtueState.Complete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,18 +9,11 @@ package io.element.android.features.ftue.test
|
|||
|
||||
import io.element.android.features.ftue.api.state.FtueService
|
||||
import io.element.android.features.ftue.api.state.FtueState
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FakeFtueService(
|
||||
private val resetLambda: () -> Unit = { lambdaError() },
|
||||
) : FtueService {
|
||||
class FakeFtueService : FtueService {
|
||||
override val state: MutableStateFlow<FtueState> = MutableStateFlow(FtueState.Unknown)
|
||||
|
||||
override suspend fun reset() {
|
||||
resetLambda()
|
||||
}
|
||||
|
||||
suspend fun emitState(newState: FtueState) {
|
||||
state.emit(newState)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Inject
|
||||
|
|
@ -102,13 +103,44 @@ class RoomListDataSource(
|
|||
}
|
||||
|
||||
private suspend fun buildAndEmitAllRooms(roomSummaries: List<RoomSummary>, useCache: Boolean = true) {
|
||||
// Used to detect duplicates in the room list summaries - see comment below
|
||||
data class CacheResult(val index: Int, val fromCache: Boolean)
|
||||
val cachingResults = mutableMapOf<RoomId, MutableList<CacheResult>>()
|
||||
|
||||
val roomListRoomSummaries = diffCache.indices().mapNotNull { index ->
|
||||
if (useCache) {
|
||||
diffCache.get(index) ?: buildAndCacheItem(roomSummaries, index)
|
||||
diffCache.get(index)?.let { cachedItem ->
|
||||
// Add the cached item to the caching results
|
||||
val pairs = cachingResults.getOrDefault(cachedItem.roomId, mutableListOf())
|
||||
pairs.add(CacheResult(index, fromCache = true))
|
||||
cachingResults[cachedItem.roomId] = pairs
|
||||
cachedItem
|
||||
} ?: run {
|
||||
roomSummaries.getOrNull(index)?.roomId?.let {
|
||||
// Add the non-cached item to the caching results
|
||||
val pairs = cachingResults.getOrDefault(it, mutableListOf())
|
||||
pairs.add(CacheResult(index, fromCache = false))
|
||||
cachingResults[it] = pairs
|
||||
}
|
||||
buildAndCacheItem(roomSummaries, index)
|
||||
}
|
||||
} else {
|
||||
roomSummaries.getOrNull(index)?.roomId?.let {
|
||||
// Add the non-cached item to the caching results
|
||||
val pairs = cachingResults.getOrDefault(it, mutableListOf())
|
||||
pairs.add(CacheResult(index, fromCache = false))
|
||||
cachingResults[it] = pairs
|
||||
}
|
||||
buildAndCacheItem(roomSummaries, index)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO remove once https://github.com/element-hq/element-x-android/issues/5031 has been confirmed as fixed
|
||||
val duplicates = cachingResults.filter { (_, operations) -> operations.size > 1 }
|
||||
if (duplicates.isNotEmpty()) {
|
||||
Timber.e("Found duplicates in room summaries after an UI update: $duplicates. This could be a race condition/caching issue of some kind")
|
||||
}
|
||||
|
||||
_allRooms.emit(roomListRoomSummaries.toImmutableList())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="full_screen_intent_banner_message">"Er mwyn sicrhau fyddwch chi ddim yn colli galwad bwysig, newidiwch eich gosodiadau i ganiatáu hysbysiadau sgrin lawn pan fydd eich ffôn wedi\'i gloi."</string>
|
||||
<string name="full_screen_intent_banner_title">"Gwella profiad eich galwadau"</string>
|
||||
<string name="screen_home_tab_chats">"Sgyrsiau"</string>
|
||||
<string name="screen_home_tab_spaces">"Gofodau"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Gwrthod y gwahoddiad"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?"</string>
|
||||
|
|
@ -32,6 +33,7 @@ Am y tro, gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill"</strin
|
|||
<string name="screen_roomlist_filter_invites">"Gwahoddiadau"</string>
|
||||
<string name="screen_roomlist_filter_invites_empty_state_title">"Does gennych chi ddim gwahoddiadau yn aros."</string>
|
||||
<string name="screen_roomlist_filter_low_priority">"Blaenoriaeth Isel"</string>
|
||||
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Does gennych chi ddim sgyrsiau blaenoriaeth isel eto"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"Does gennych chi ddim sgyrsiau ar gyfer y dewis hwn"</string>
|
||||
<string name="screen_roomlist_filter_people">"Pobl"</string>
|
||||
|
|
|
|||
11
features/home/impl/src/main/res/values-eo/translations.xml
Normal file
11
features/home/impl/src/main/res/values-eo/translations.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="banner_set_up_recovery_content">"Restore your account security and message history with a backup password if you have lost all your existing devices."</string>
|
||||
<string name="banner_set_up_recovery_submit">"Set up backup"</string>
|
||||
<string name="banner_set_up_recovery_title">"Set up backup to protect your account"</string>
|
||||
<string name="confirm_recovery_key_banner_message">"Confirm your backup password to maintain access to your message backup and message history."</string>
|
||||
<string name="confirm_recovery_key_banner_primary_button_title">"Enter your backup password"</string>
|
||||
<string name="confirm_recovery_key_banner_secondary_button_title">"Forgot your backup password?"</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Your message backup is out of sync"</string>
|
||||
<string name="session_verification_banner_message">"Looks like you\'re using a new device. Confirm it with another connected device to access your encrypted messages."</string>
|
||||
</resources>
|
||||
|
|
@ -33,6 +33,7 @@ Toistaiseksi voit poistaa suodattimien valinnan, jotta näet muut keskustelut."<
|
|||
<string name="screen_roomlist_filter_invites">"Kutsut"</string>
|
||||
<string name="screen_roomlist_filter_invites_empty_state_title">"Sinulla ei ole yhtään odottavaa kutsua."</string>
|
||||
<string name="screen_roomlist_filter_low_priority">"Matala prioriteetti"</string>
|
||||
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Sinulla ei ole vielä yhtään matalan prioriteetin keskustelua"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Voit poistaa suodattimien valinnan nähdäksesi muut keskustelusi."</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"Sinulla ei ole sopivia keskusteluja tähän valintaan"</string>
|
||||
<string name="screen_roomlist_filter_people">"Ihmiset"</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="full_screen_intent_banner_message">"For å sikre at du aldri går glipp av en viktig samtale, må du endre innstillingene dine for å tillate fullskjermvarsler når telefonen er låst."</string>
|
||||
<string name="full_screen_intent_banner_title">"Forbedre samtaleopplevelsen din"</string>
|
||||
<string name="screen_home_tab_chats">"Chatter"</string>
|
||||
<string name="screen_home_tab_spaces">"Områder"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Avvis invitasjon"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Er du sikker på at du vil avslå denne private chatten med %1$s?"</string>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
<string name="screen_roomlist_filter_invites">"邀請"</string>
|
||||
<string name="screen_roomlist_filter_invites_empty_state_title">"您沒有任何擱置中的邀請。"</string>
|
||||
<string name="screen_roomlist_filter_low_priority">"低優先度"</string>
|
||||
<string name="screen_roomlist_filter_low_priority_empty_state_title">"您尚無任何低優先程度聊天"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"您可以取消選取篩選條件以檢視其他聊天"</string>
|
||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"您並無此選擇的聊天"</string>
|
||||
<string name="screen_roomlist_filter_people">"夥伴"</string>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<string name="screen_join_room_join_action">"Ymuno â\'r ystafell"</string>
|
||||
<string name="screen_join_room_join_restricted_message">"Efallai y bydd angen i chi gael eich gwahodd neu fod yn aelod o ofod er mwyn ymuno."</string>
|
||||
<string name="screen_join_room_knock_action">"Anfon cais i ymuno"</string>
|
||||
<string name="screen_join_room_knock_message_characters_count">"Nodau a ganiateir %1$d o %2$d"</string>
|
||||
<string name="screen_join_room_knock_message_description">"Neges (dewisol)"</string>
|
||||
<string name="screen_join_room_knock_sent_description">"Byddwch yn derbyn gwahoddiad i ymuno â\'r ystafell os caiff eich cais ei dderbyn."</string>
|
||||
<string name="screen_join_room_knock_sent_title">"Anfonwyd y cais i ymuno"</string>
|
||||
|
|
|
|||
|
|
@ -3,5 +3,8 @@
|
|||
<string name="leave_conversation_alert_subtitle">"Ydych chi\'n siŵr eich bod am adael y sgwrs hon? Dyw\'r sgwrs hon ddim yn gyhoeddus a fyddwch chi ddim yn gallu ailymuno heb wahoddiad."</string>
|
||||
<string name="leave_room_alert_empty_subtitle">"Ydych chi\'n siŵr eich bod am adael yr ystafell hon? Chi yw\'r unig berson yma. Os byddwch yn gadael, fydd neb yn gallu ymuno yn y dyfodol, gan gynnwys chi."</string>
|
||||
<string name="leave_room_alert_private_subtitle">"Ydych chi\'n siŵr eich bod am adael yr ystafell hon? Dyw\'r ystafell hon ddim yn gyhoeddus a fyddwch chi ddim yn gallu ailymuno heb wahoddiad."</string>
|
||||
<string name="leave_room_alert_select_new_owner_action">"Dewiswch Berchnogion"</string>
|
||||
<string name="leave_room_alert_select_new_owner_subtitle">"Chi yw unig berchennog yr ystafell hon. Mae angen i chi drosglwyddo perchnogaeth i rywun arall cyn i chi adael yr room."</string>
|
||||
<string name="leave_room_alert_select_new_owner_title">"Trosglwyddo perchnogaeth"</string>
|
||||
<string name="leave_room_alert_subtitle">"Ydych chi\'n siŵr eich bod am adael yr ystafell?"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<string name="screen_app_lock_settings_enable_biometric_unlock">"Salli biometrinen tunnistus"</string>
|
||||
<string name="screen_app_lock_settings_remove_pin">"Poista PIN-koodi"</string>
|
||||
<string name="screen_app_lock_settings_remove_pin_alert_message">"Haluatko varmasti poistaa PIN-koodin?"</string>
|
||||
<string name="screen_app_lock_settings_remove_pin_alert_title">"Poista PIN-koodi?"</string>
|
||||
<string name="screen_app_lock_settings_remove_pin_alert_title">"Poistetaanko PIN-koodi?"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_allow_title">"Salli %1$s"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_skip">"Käytän mieluummin PIN-koodia"</string>
|
||||
<string name="screen_app_lock_setup_biometric_unlock_subtitle">"Säästä aikaa ja ota käyttöön %1$s"</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
<string name="screen_change_account_provider_other">"Arall"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Defnyddiwch ddarparwr cyfrif gwahanol, fel eich gweinydd preifat eich hun neu gyfrif gwaith."</string>
|
||||
<string name="screen_change_account_provider_title">"Newid darparwr cyfrif"</string>
|
||||
<string name="screen_change_server_error_element_pro_required_action_android">"Google Play"</string>
|
||||
<string name="screen_change_server_error_element_pro_required_message">"Mae angen yr ap Element Pro ar %1$s. Llwythwch ef o\'r siop."</string>
|
||||
<string name="screen_change_server_error_element_pro_required_title">"Mae angen Element Pro"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Doedd dim modd i ni gyrraedd y gweinydd cartref hwn. Gwiriwch eich bod wedi rhoi URL y gweinydd cartref yn gywir. Os yw\'r URL yn gywir, cysylltwch â gweinyddwr eich gweinydd cartref am ragor o help."</string>
|
||||
<string name="screen_change_server_error_invalid_well_known">"Dyw cydweddu llithrig ddim ar gael oherwydd problem yn y ffeil .well-known:
|
||||
%1$s"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"A secure connection could not be made to the new device. Your existing connected devices are still safe and you don\'t need to worry about them."</string>
|
||||
</resources>
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="screen_change_account_provider_other">"Annet"</string>
|
||||
<string name="screen_change_account_provider_subtitle">"Bruk en annen kontotilbyder, for eksempel din egen private server eller en arbeidskonto."</string>
|
||||
<string name="screen_change_account_provider_title">"Bytt kontotilbyder"</string>
|
||||
<string name="screen_change_server_error_element_pro_required_action_android">"Google Play"</string>
|
||||
<string name="screen_change_server_error_element_pro_required_message">"Element Pro-appen er nødvendig på %1$s. Last den ned fra butikken."</string>
|
||||
<string name="screen_change_server_error_element_pro_required_title">"Element Pro kreves"</string>
|
||||
<string name="screen_change_server_error_invalid_homeserver">"Vi kunne ikke nå denne hjemmeserveren. Kontroller at du har skrevet inn hjemmeserverens URL riktig. Hvis URL-en er riktig, kontakt administratoren for hjemmeserveren din for å få mer hjelp."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_signout_key_backup_offline_subtitle">"Your messages were still being backed up when you went offline. Reconnect so that your messages can be backed up before signing out."</string>
|
||||
<string name="screen_signout_key_backup_offline_title">"Your messages are still being backed up"</string>
|
||||
<string name="screen_signout_key_backup_ongoing_title">"Your messages are still being backed up"</string>
|
||||
<string name="screen_signout_recovery_disabled_title">"Backup not set up"</string>
|
||||
<string name="screen_signout_save_recovery_key_title">"Have you saved your backup password?"</string>
|
||||
</resources>
|
||||
|
|
@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.components.customreact
|
|||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
|
|
@ -328,7 +329,10 @@ class MessagesPresenter(
|
|||
val displayThreads = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
|
||||
if (displayThreads) {
|
||||
// Get either the thread id this event is in, or the event id if it's not in a thread so we can start one
|
||||
val threadId = targetEvent.threadInfo.threadRootId ?: targetEvent.eventId!!.toThreadId()
|
||||
val threadId = when (targetEvent.threadInfo) {
|
||||
is TimelineItemThreadInfo.ThreadResponse -> targetEvent.threadInfo.threadRootId
|
||||
is TimelineItemThreadInfo.ThreadRoot, null -> targetEvent.eventId?.toThreadId()
|
||||
} ?: return@launch
|
||||
navigator.onOpenThread(threadId, null)
|
||||
} else {
|
||||
handleActionReply(targetEvent, composerState, timelineProtectionState)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ import io.element.android.libraries.designsystem.components.ExpandableBottomShee
|
|||
import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayoutState
|
||||
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.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
|
|
@ -95,6 +96,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
|||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.toAnnotatedString
|
||||
import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
|
@ -111,10 +113,14 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
|||
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.wysiwyg.link.Link
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
|
@ -202,7 +208,13 @@ fun MessagesView(
|
|||
Column {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
if (state.timelineState.timelineMode is Timeline.Mode.Thread) {
|
||||
ThreadTopBar(onBackClick = onBackClick)
|
||||
ThreadTopBar(
|
||||
roomName = state.roomName,
|
||||
roomAvatarData = state.roomAvatar,
|
||||
heroes = state.heroes,
|
||||
isTombstoned = state.isTombstoned,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
} else {
|
||||
MessagesViewTopBar(
|
||||
roomName = state.roomName,
|
||||
|
|
@ -573,14 +585,48 @@ private fun MessagesViewTopBar(
|
|||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ThreadTopBar(
|
||||
roomName: String?,
|
||||
roomAvatarData: AvatarData,
|
||||
heroes: ImmutableList<AvatarData>,
|
||||
isTombstoned: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopAppBar(
|
||||
modifier = modifier,
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackClick)
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(CommonStrings.common_thread))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Avatar(
|
||||
avatarData = roomAvatarData,
|
||||
avatarType = AvatarType.Room(
|
||||
heroes = heroes,
|
||||
isTombstoned = isTombstoned,
|
||||
),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp)
|
||||
.semantics {
|
||||
heading()
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.common_thread),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
)
|
||||
Text(
|
||||
text = roomName ?: stringResource(CommonStrings.common_no_room_name),
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
fontStyle = FontStyle.Italic.takeIf { roomName == null },
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -673,3 +719,58 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
|
|||
knockRequestsBannerView = {},
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ThreadTopBarPreview() {
|
||||
ElementPreview {
|
||||
val name = "Room name"
|
||||
val initialsAvatarData = AvatarData(
|
||||
id = "id",
|
||||
name = name,
|
||||
url = null,
|
||||
size = AvatarSize.TimelineRoom,
|
||||
)
|
||||
Column {
|
||||
ThreadTopBar(
|
||||
roomName = name,
|
||||
roomAvatarData = initialsAvatarData,
|
||||
heroes = persistentListOf(),
|
||||
isTombstoned = false,
|
||||
onBackClick = {},
|
||||
)
|
||||
HorizontalDivider()
|
||||
ThreadTopBar(
|
||||
roomName = name,
|
||||
roomAvatarData = initialsAvatarData,
|
||||
heroes = aMatrixUserList().map { it.getAvatarData(AvatarSize.TimelineRoom) }.toImmutableList(),
|
||||
isTombstoned = false,
|
||||
onBackClick = {},
|
||||
)
|
||||
HorizontalDivider()
|
||||
ThreadTopBar(
|
||||
roomName = null,
|
||||
roomAvatarData = initialsAvatarData,
|
||||
heroes = persistentListOf(),
|
||||
isTombstoned = false,
|
||||
onBackClick = {},
|
||||
)
|
||||
HorizontalDivider()
|
||||
ThreadTopBar(
|
||||
roomName = name,
|
||||
roomAvatarData = initialsAvatarData.copy(url = "https://some-avatar.jpg"),
|
||||
heroes = persistentListOf(),
|
||||
isTombstoned = false,
|
||||
onBackClick = {},
|
||||
)
|
||||
HorizontalDivider()
|
||||
ThreadTopBar(
|
||||
roomName = name,
|
||||
roomAvatarData = initialsAvatarData,
|
||||
heroes = persistentListOf(),
|
||||
isTombstoned = true,
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
|
|||
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
|
||||
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
||||
|
|
@ -174,7 +175,7 @@ class DefaultActionListPresenter(
|
|||
add(TimelineItemAction.ReplyInThread)
|
||||
add(TimelineItemAction.Reply)
|
||||
} else {
|
||||
if (!isThreadsEnabled && timelineItem.threadInfo.threadRootId != null) {
|
||||
if (!isThreadsEnabled && timelineItem.threadInfo is TimelineItemThreadInfo.ThreadResponse) {
|
||||
// If threads are not enabled, we can reply in a thread if the item is already in the thread
|
||||
add(TimelineItemAction.ReplyInThread)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.anAggregatedReaction
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||
|
|
@ -32,7 +33,6 @@ import io.element.android.libraries.matrix.api.core.UniqueId
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.tombstone.PredecessorRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||
|
|
@ -146,7 +146,7 @@ internal fun aTimelineItemEvent(
|
|||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||
sendState: LocalEventSendState? = null,
|
||||
inReplyTo: InReplyToDetails? = null,
|
||||
threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
|
||||
threadInfo: TimelineItemThreadInfo? = null,
|
||||
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
||||
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
|
||||
readReceiptState: TimelineItemReadReceipts = aTimelineItemReadReceipts(),
|
||||
|
|
|
|||
|
|
@ -53,8 +53,6 @@ import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
|||
private val BUBBLE_RADIUS = 12.dp
|
||||
private val avatarRadius = AvatarSize.TimelineSender.dp / 2
|
||||
|
||||
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 78% now.
|
||||
private const val BUBBLE_WIDTH_RATIO = 0.78f
|
||||
private val MIN_BUBBLE_WIDTH = 80.dp
|
||||
|
||||
@Composable
|
||||
|
|
@ -66,34 +64,6 @@ fun MessageEventBubble(
|
|||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit = {},
|
||||
) {
|
||||
fun bubbleShape(): Shape {
|
||||
val topLeftCorner = if (state.cutTopStart) 0.dp else BUBBLE_RADIUS
|
||||
return when (state.groupPosition) {
|
||||
TimelineItemGroupPosition.First -> if (state.isMine) {
|
||||
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS)
|
||||
} else {
|
||||
RoundedCornerShape(topLeftCorner, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp)
|
||||
}
|
||||
TimelineItemGroupPosition.Middle -> if (state.isMine) {
|
||||
RoundedCornerShape(BUBBLE_RADIUS, 0.dp, 0.dp, BUBBLE_RADIUS)
|
||||
} else {
|
||||
RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp)
|
||||
}
|
||||
TimelineItemGroupPosition.Last -> if (state.isMine) {
|
||||
RoundedCornerShape(BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS)
|
||||
} else {
|
||||
RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS)
|
||||
}
|
||||
TimelineItemGroupPosition.None ->
|
||||
RoundedCornerShape(
|
||||
topLeftCorner,
|
||||
BUBBLE_RADIUS,
|
||||
BUBBLE_RADIUS,
|
||||
BUBBLE_RADIUS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val clickableModifier = if (isTalkbackActive()) {
|
||||
Modifier
|
||||
} else {
|
||||
|
|
@ -108,11 +78,8 @@ fun MessageEventBubble(
|
|||
}
|
||||
|
||||
// Ignore state.isHighlighted for now, we need a design decision on it.
|
||||
val backgroundBubbleColor = when {
|
||||
state.isMine -> ElementTheme.colors.messageFromMeBackground
|
||||
else -> ElementTheme.colors.messageFromOtherBackground
|
||||
}
|
||||
val bubbleShape = bubbleShape()
|
||||
val backgroundBubbleColor = MessageEventBubbleDefaults.backgroundBubbleColor(state.isMine)
|
||||
val bubbleShape = remember(state) { MessageEventBubbleDefaults.shape(state.cutTopStart, state.groupPosition, state.isMine) }
|
||||
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
||||
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
|
|
@ -147,7 +114,7 @@ fun MessageEventBubble(
|
|||
.testTag(TestTags.messageBubble)
|
||||
.widthIn(
|
||||
min = MIN_BUBBLE_WIDTH,
|
||||
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO)
|
||||
max = (constraints.maxWidth * MessageEventBubbleDefaults.BUBBLE_WIDTH_RATIO)
|
||||
.toInt()
|
||||
.toDp()
|
||||
)
|
||||
|
|
@ -157,6 +124,48 @@ fun MessageEventBubble(
|
|||
}
|
||||
}
|
||||
|
||||
object MessageEventBubbleDefaults {
|
||||
fun shape(cutTopStart: Boolean, groupPosition: TimelineItemGroupPosition, isMine: Boolean): Shape {
|
||||
val topLeftCorner = if (cutTopStart) 0.dp else BUBBLE_RADIUS
|
||||
return when (groupPosition) {
|
||||
TimelineItemGroupPosition.First -> if (isMine) {
|
||||
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS)
|
||||
} else {
|
||||
RoundedCornerShape(topLeftCorner, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp)
|
||||
}
|
||||
TimelineItemGroupPosition.Middle -> if (isMine) {
|
||||
RoundedCornerShape(BUBBLE_RADIUS, 0.dp, 0.dp, BUBBLE_RADIUS)
|
||||
} else {
|
||||
RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp)
|
||||
}
|
||||
TimelineItemGroupPosition.Last -> if (isMine) {
|
||||
RoundedCornerShape(BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS)
|
||||
} else {
|
||||
RoundedCornerShape(0.dp, BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS)
|
||||
}
|
||||
TimelineItemGroupPosition.None ->
|
||||
RoundedCornerShape(
|
||||
topLeftCorner,
|
||||
BUBBLE_RADIUS,
|
||||
BUBBLE_RADIUS,
|
||||
BUBBLE_RADIUS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun backgroundBubbleColor(isMine: Boolean): Color {
|
||||
return if (isMine) {
|
||||
ElementTheme.colors.messageFromMeBackground
|
||||
} else {
|
||||
ElementTheme.colors.messageFromOtherBackground
|
||||
}
|
||||
}
|
||||
|
||||
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 78% now.
|
||||
const val BUBBLE_WIDTH_RATIO = 0.78f
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun MessageEventBubblePreview(@PreviewParameter(BubbleStateProvider::class) state: BubbleState) = ElementPreview {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.foundation.gestures.draggable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -23,6 +24,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -34,6 +37,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.platform.ViewConfiguration
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
|
|
@ -43,6 +47,7 @@ import androidx.compose.ui.semantics.hideFromAccessibility
|
|||
import androidx.compose.ui.semantics.isTraversalGroup
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.traversalIndex
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -61,6 +66,7 @@ import io.element.android.features.messages.impl.timeline.components.receipt.Rea
|
|||
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
|
|
@ -78,25 +84,28 @@ import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
|||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||
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.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.modifiers.niceClickable
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.swipe.SwipeableActionsState
|
||||
import io.element.android.libraries.designsystem.swipe.rememberSwipeableActionsState
|
||||
import io.element.android.libraries.designsystem.text.toPx
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.core.toThreadId
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EmbeddedEventInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||
|
|
@ -256,22 +265,22 @@ fun TimelineItemEventRow(
|
|||
)
|
||||
}
|
||||
|
||||
if (displayThreadSummaries && timelineMode !is Timeline.Mode.Thread) {
|
||||
event.threadInfo.threadSummary?.let { threadSummary ->
|
||||
val threadPart = stringResource(CommonStrings.common_thread)
|
||||
val numberOfReplies = threadSummary.numberOfReplies.toInt().let { replies ->
|
||||
pluralStringResource(CommonPlurals.common_replies, replies, replies)
|
||||
if (displayThreadSummaries && timelineMode !is Timeline.Mode.Thread && event.threadInfo is TimelineItemThreadInfo.ThreadRoot) {
|
||||
ThreadSummaryView(
|
||||
modifier = if (event.isMine) {
|
||||
Modifier.align(Alignment.End).padding(end = 16.dp)
|
||||
} else {
|
||||
if (timelineRoomInfo.isDm) Modifier else Modifier.padding(start = 16.dp)
|
||||
}.padding(top = 2.dp),
|
||||
threadSummary = event.threadInfo.summary,
|
||||
latestEventText = event.threadInfo.latestEventText,
|
||||
isOutgoing = event.isMine,
|
||||
onClick = {
|
||||
event.eventId?.let {
|
||||
eventSink(TimelineEvents.OpenThread(it.toThreadId(), null))
|
||||
}
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 2.dp)
|
||||
.align(if (event.isMine) Alignment.End else Alignment.Start),
|
||||
text = "$threadPart - $numberOfReplies",
|
||||
size = ButtonSize.Small,
|
||||
onClick = {
|
||||
eventSink(TimelineEvents.OpenThread(event.eventId!!.toThreadId(), null))
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Read receipts / Send state
|
||||
|
|
@ -288,6 +297,81 @@ fun TimelineItemEventRow(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThreadSummaryView(
|
||||
threadSummary: ThreadSummary,
|
||||
latestEventText: String?,
|
||||
isOutgoing: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.then(if (!isOutgoing) Modifier.padding(start = 16.dp) else Modifier)
|
||||
.graphicsLayer {
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
clip = true
|
||||
}
|
||||
.background(MessageEventBubbleDefaults.backgroundBubbleColor(isOutgoing))
|
||||
.niceClickable(onClick)
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp)
|
||||
.widthIn(max = (maxWidth - 24.dp) * MessageEventBubbleDefaults.BUBBLE_WIDTH_RATIO),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = CompoundIcons.ThreadsSolid(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = pluralStringResource(CommonPlurals.common_replies, threadSummary.numberOfReplies.toInt(), threadSummary.numberOfReplies),
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
threadSummary.latestEvent.dataOrNull()?.let { latestEvent ->
|
||||
val avatarData = AvatarData(
|
||||
id = latestEvent.senderId.value,
|
||||
name = latestEvent.senderProfile.getDisplayName(),
|
||||
url = latestEvent.senderProfile.getAvatarUrl(),
|
||||
size = AvatarSize.TimelineThreadLatestEventSender,
|
||||
)
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.User,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = latestEvent.senderProfile.getDisambiguatedDisplayName(latestEvent.senderId),
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
latestEventText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Impact ViewConfiguration.touchSlop by [sensitivityFactor].
|
||||
* Inspired from https://issuetracker.google.com/u/1/issues/269627294.
|
||||
|
|
@ -694,7 +778,7 @@ private fun MessageEventBubbleContent(
|
|||
else -> ContentPadding.Textual
|
||||
}
|
||||
CommonLayout(
|
||||
showThreadDecoration = timelineMode !is Timeline.Mode.Thread && event.threadInfo.threadRootId != null,
|
||||
showThreadDecoration = timelineMode !is Timeline.Mode.Thread && event.threadInfo is TimelineItemThreadInfo.ThreadResponse,
|
||||
timestampPosition = timestampPosition,
|
||||
paddingBehaviour = paddingBehaviour,
|
||||
inReplyToDetails = event.inReplyTo,
|
||||
|
|
@ -746,9 +830,27 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
|
|||
" hopefully can be manually adjusted to test different behaviors."
|
||||
),
|
||||
groupPosition = TimelineItemGroupPosition.First,
|
||||
threadInfo = EventThreadInfo(
|
||||
threadRootId = ThreadId("\$thread-root-id"),
|
||||
threadSummary = ThreadSummary(AsyncData.Uninitialized, numberOfReplies = 20L)
|
||||
threadInfo = TimelineItemThreadInfo.ThreadRoot(
|
||||
latestEventText = "This is the latest message in the thread",
|
||||
summary = ThreadSummary(AsyncData.Success(
|
||||
EmbeddedEventInfo(
|
||||
eventOrTransactionId = EventOrTransactionId.Event(EventId("\$event-id")),
|
||||
content = MessageContent(
|
||||
body = "This is the latest message in the thread",
|
||||
inReplyTo = null,
|
||||
isEdited = false,
|
||||
threadInfo = null,
|
||||
type = TextMessageType("This is the latest message in the thread", null)
|
||||
),
|
||||
senderId = UserId("@user:id"),
|
||||
senderProfile = ProfileTimelineDetails.Ready(
|
||||
displayName = "Alice",
|
||||
avatarUrl = null,
|
||||
displayNameAmbiguous = false,
|
||||
),
|
||||
timestamp = 0L,
|
||||
)
|
||||
), numberOfReplies = 20L)
|
||||
)
|
||||
),
|
||||
displayThreadSummaries = true,
|
||||
|
|
@ -756,3 +858,40 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ThreadSummaryViewPreview() {
|
||||
ElementPreview {
|
||||
val body = "This is the latest message in the thread"
|
||||
val threadSummary = ThreadSummary(
|
||||
AsyncData.Success(
|
||||
EmbeddedEventInfo(
|
||||
eventOrTransactionId = EventOrTransactionId.Event(EventId("\$event-id")),
|
||||
content = MessageContent(
|
||||
body = body,
|
||||
inReplyTo = null,
|
||||
isEdited = false,
|
||||
threadInfo = null,
|
||||
type = TextMessageType(body, null)
|
||||
),
|
||||
senderId = UserId("@user:id"),
|
||||
senderProfile = ProfileTimelineDetails.Ready(
|
||||
displayName = "Alice",
|
||||
avatarUrl = null,
|
||||
displayNameAmbiguous = true,
|
||||
),
|
||||
timestamp = 0L,
|
||||
)
|
||||
),
|
||||
numberOfReplies = 12,
|
||||
)
|
||||
|
||||
ThreadSummaryView(
|
||||
threadSummary = threadSummary,
|
||||
latestEventText = "Some event with a very long text that should get clipped",
|
||||
isOutgoing = true,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider
|
||||
|
||||
|
|
@ -58,10 +58,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(
|
|||
),
|
||||
inReplyTo = inReplyToDetails,
|
||||
displayNameAmbiguous = displayNameAmbiguous,
|
||||
threadInfo = EventThreadInfo(
|
||||
threadRootId = ThreadId("\$thread-root-id"),
|
||||
threadSummary = null,
|
||||
),
|
||||
threadInfo = TimelineItemThreadInfo.ThreadResponse(threadRootId = ThreadId("\$thread-root-id")),
|
||||
groupPosition = TimelineItemGroupPosition.Last,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
|
|
@ -20,6 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInv
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
|
|
@ -27,6 +31,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StickerConten
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||
|
||||
@Inject
|
||||
class TimelineItemContentFactory(
|
||||
|
|
@ -40,26 +45,53 @@ class TimelineItemContentFactory(
|
|||
private val stateFactory: TimelineItemContentStateFactory,
|
||||
private val failedToParseMessageFactory: TimelineItemContentFailedToParseMessageFactory,
|
||||
private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory,
|
||||
private val currentSessionIdHolder: CurrentSessionIdHolder,
|
||||
) {
|
||||
suspend fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
return when (val itemContent = eventTimelineItem.content) {
|
||||
return create(
|
||||
itemContent = eventTimelineItem.content,
|
||||
eventId = eventTimelineItem.eventId,
|
||||
isEditable = eventTimelineItem.isEditable,
|
||||
sender = eventTimelineItem.sender,
|
||||
senderProfile = eventTimelineItem.senderProfile,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun create(
|
||||
itemContent: EventContent,
|
||||
eventId: EventId?,
|
||||
isEditable: Boolean,
|
||||
sender: UserId,
|
||||
senderProfile: ProfileTimelineDetails,
|
||||
): TimelineItemEventContent {
|
||||
val isOutgoing = currentSessionIdHolder.current == sender
|
||||
return when (itemContent) {
|
||||
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
|
||||
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
|
||||
is MessageContent -> {
|
||||
val senderDisambiguatedDisplayName = eventTimelineItem.senderProfile.getDisambiguatedDisplayName(eventTimelineItem.sender)
|
||||
val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(sender)
|
||||
messageFactory.create(
|
||||
content = itemContent,
|
||||
senderDisambiguatedDisplayName = senderDisambiguatedDisplayName,
|
||||
eventId = eventTimelineItem.eventId,
|
||||
eventId = eventId,
|
||||
)
|
||||
}
|
||||
is ProfileChangeContent -> profileChangeFactory.create(eventTimelineItem)
|
||||
is ProfileChangeContent -> {
|
||||
val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(sender)
|
||||
profileChangeFactory.create(itemContent, isOutgoing, sender, senderDisambiguatedDisplayName)
|
||||
}
|
||||
is RedactedContent -> redactedMessageFactory.create(itemContent)
|
||||
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
|
||||
is RoomMembershipContent -> {
|
||||
val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(sender)
|
||||
roomMembershipFactory.create(itemContent, isOutgoing, sender, senderDisambiguatedDisplayName)
|
||||
}
|
||||
is LegacyCallInviteContent -> TimelineItemLegacyCallInviteContent
|
||||
is StateContent -> stateFactory.create(eventTimelineItem)
|
||||
is StateContent -> {
|
||||
val senderDisambiguatedDisplayName = senderProfile.getDisambiguatedDisplayName(sender)
|
||||
stateFactory.create(itemContent, isOutgoing, sender, senderDisambiguatedDisplayName)
|
||||
}
|
||||
is StickerContent -> stickerFactory.create(itemContent)
|
||||
is PollContent -> pollFactory.create(eventTimelineItem, itemContent)
|
||||
is PollContent -> pollFactory.create(eventId, isEditable, isOutgoing, itemContent)
|
||||
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
||||
is CallNotifyContent -> TimelineItemRtcNotificationContent()
|
||||
is UnknownContent -> TimelineItemUnknownContent
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import dev.zacsweers.metro.Inject
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
|
||||
@Inject
|
||||
|
|
@ -19,14 +19,16 @@ class TimelineItemContentPollFactory(
|
|||
private val pollContentStateFactory: PollContentStateFactory,
|
||||
) {
|
||||
suspend fun create(
|
||||
event: EventTimelineItem,
|
||||
eventId: EventId?,
|
||||
isEditable: Boolean,
|
||||
isOwn: Boolean,
|
||||
content: PollContent,
|
||||
): TimelineItemEventContent {
|
||||
val pollContentState = pollContentStateFactory.create(event, content)
|
||||
val pollContentState = pollContentStateFactory.create(eventId, isEditable, isOwn, content)
|
||||
return TimelineItemPollContent(
|
||||
isMine = pollContentState.isMine,
|
||||
isEditable = pollContentState.isPollEditable,
|
||||
eventId = event.eventId,
|
||||
eventId = eventId,
|
||||
question = pollContentState.question,
|
||||
answerItems = pollContentState.answerItems,
|
||||
pollKind = pollContentState.pollKind,
|
||||
|
|
|
|||
|
|
@ -12,14 +12,15 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
|
||||
@Inject
|
||||
class TimelineItemContentProfileChangeFactory(
|
||||
private val timelineEventFormatter: TimelineEventFormatter,
|
||||
) {
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventTimelineItem)
|
||||
fun create(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(content, isOutgoing, sender, senderDisambiguatedDisplayName)
|
||||
return TimelineItemProfileChangeContent(text.orEmpty().toString())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,14 +12,15 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
|
||||
@Inject
|
||||
class TimelineItemContentRoomMembershipFactory(
|
||||
private val timelineEventFormatter: TimelineEventFormatter,
|
||||
) {
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventTimelineItem)
|
||||
fun create(eventContent: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventContent, isOutgoing, sender, senderDisambiguatedDisplayName)
|
||||
return TimelineItemRoomMembershipContent(text.orEmpty().toString())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,14 +12,15 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
|
||||
@Inject
|
||||
class TimelineItemContentStateFactory(
|
||||
private val timelineEventFormatter: TimelineEventFormatter,
|
||||
) {
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventTimelineItem)
|
||||
fun create(eventContent: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): TimelineItemEventContent {
|
||||
val text = timelineEventFormatter.format(eventContent, isOutgoing, sender, senderDisambiguatedDisplayName)
|
||||
return TimelineItemStateEventContent(text.orEmpty().toString())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
|
||||
import io.element.android.libraries.architecture.map
|
||||
import io.element.android.libraries.core.bool.orTrue
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
|
|
@ -43,6 +46,7 @@ class TimelineItemEventFactory(
|
|||
private val matrixClient: MatrixClient,
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val summaryFormatter: MessageSummaryFormatter,
|
||||
) {
|
||||
@AssistedFactory
|
||||
interface Creator {
|
||||
|
|
@ -69,6 +73,29 @@ class TimelineItemEventFactory(
|
|||
url = senderProfile.getAvatarUrl(),
|
||||
size = AvatarSize.TimelineSender
|
||||
)
|
||||
val mappedThreadInfo = when (val threadInfo = currentTimelineItem.event.threadInfo()) {
|
||||
is EventThreadInfo.ThreadResponse -> {
|
||||
TimelineItemThreadInfo.ThreadResponse(threadInfo.threadRootId)
|
||||
}
|
||||
is EventThreadInfo.ThreadRoot -> {
|
||||
TimelineItemThreadInfo.ThreadRoot(
|
||||
summary = threadInfo.summary,
|
||||
latestEventText = threadInfo.summary.latestEvent.dataOrNull()
|
||||
?.let {
|
||||
contentFactory.create(
|
||||
itemContent = it.content,
|
||||
eventId = it.eventOrTransactionId.eventId,
|
||||
isEditable = false,
|
||||
sender = it.senderId,
|
||||
senderProfile = it.senderProfile,
|
||||
)
|
||||
}
|
||||
?.let(summaryFormatter::format)
|
||||
)
|
||||
}
|
||||
null -> null
|
||||
}
|
||||
|
||||
return TimelineItem.Event(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
eventId = currentTimelineItem.eventId,
|
||||
|
|
@ -87,7 +114,7 @@ class TimelineItemEventFactory(
|
|||
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers),
|
||||
localSendState = currentTimelineItem.event.localSendState,
|
||||
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser),
|
||||
threadInfo = currentTimelineItem.event.threadInfo() ?: EventThreadInfo(threadRootId = null, threadSummary = null),
|
||||
threadInfo = mappedThreadInfo,
|
||||
origin = currentTimelineItem.event.origin,
|
||||
timelineItemDebugInfoProvider = currentTimelineItem.event.timelineItemDebugInfoProvider,
|
||||
messageShieldProvider = currentTimelineItem.event.messageShieldProvider,
|
||||
|
|
|
|||
|
|
@ -17,10 +17,11 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline
|
|||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.SendHandle
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
|
|
@ -82,7 +83,7 @@ sealed interface TimelineItem {
|
|||
val readReceiptState: TimelineItemReadReceipts,
|
||||
val localSendState: LocalEventSendState?,
|
||||
val inReplyTo: InReplyToDetails?,
|
||||
val threadInfo: EventThreadInfo,
|
||||
val threadInfo: TimelineItemThreadInfo?,
|
||||
val origin: TimelineItemEventOrigin?,
|
||||
val timelineItemDebugInfoProvider: TimelineItemDebugInfoProvider,
|
||||
val messageShieldProvider: MessageShieldProvider,
|
||||
|
|
@ -130,3 +131,8 @@ sealed interface TimelineItem {
|
|||
val aggregatedReadReceipts: ImmutableList<ReadReceiptData>,
|
||||
) : TimelineItem
|
||||
}
|
||||
|
||||
sealed interface TimelineItemThreadInfo {
|
||||
data class ThreadRoot(val summary: ThreadSummary, val latestEventText: String?) : TimelineItemThreadInfo
|
||||
data class ThreadResponse(val threadRootId: ThreadId) : TimelineItemThreadInfo
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ package io.element.android.features.messages.impl.utils.messagesummary
|
|||
import android.content.Context
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
||||
|
|
@ -37,15 +37,15 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
|||
class DefaultMessageSummaryFormatter(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : MessageSummaryFormatter {
|
||||
override fun format(event: TimelineItem.Event): String {
|
||||
return when (event.content) {
|
||||
is TimelineItemTextBasedContent -> event.content.plainText
|
||||
is TimelineItemProfileChangeContent -> event.content.body
|
||||
is TimelineItemStateContent -> event.content.body
|
||||
override fun format(content: TimelineItemEventContent): String {
|
||||
return when (content) {
|
||||
is TimelineItemTextBasedContent -> content.plainText
|
||||
is TimelineItemProfileChangeContent -> content.body
|
||||
is TimelineItemStateContent -> content.body
|
||||
is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location)
|
||||
is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
|
||||
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
|
||||
is TimelineItemPollContent -> event.content.question
|
||||
is TimelineItemPollContent -> content.question
|
||||
is TimelineItemVoiceContent -> context.getString(CommonStrings.common_voice_message)
|
||||
is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event)
|
||||
is TimelineItemImageContent -> context.getString(CommonStrings.common_image)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@
|
|||
package io.element.android.features.messages.impl.utils.messagesummary
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
|
||||
interface MessageSummaryFormatter {
|
||||
fun format(event: TimelineItem.Event): String
|
||||
fun format(event: TimelineItem.Event): String {
|
||||
return format(event.content)
|
||||
}
|
||||
fun format(content: TimelineItemEventContent): String
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<string name="emoji_picker_category_objects">"Předměty"</string>
|
||||
<string name="emoji_picker_category_people">"Smajlíci a lidé"</string>
|
||||
<string name="emoji_picker_category_places">"Cestování a místa"</string>
|
||||
<string name="emoji_picker_category_recent">"Nedávné emotikony"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symboly"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Titulky nemusí být viditelné pro lidi, kteří používají starší aplikace."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Klepnutím změníte kvalitu nahrávání videa"</string>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
<string name="screen_media_upload_preview_error_failed_sending">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_message">"Maximální povolená velikost souboru je %1$s."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_title">"Soubor je pro nahrání příliš velký."</string>
|
||||
<string name="screen_media_upload_preview_item_count">"Položka %1$d z %2$d"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimalizace kvality obrazu"</string>
|
||||
<string name="screen_media_upload_preview_processing">"Probíhá zpracování…"</string>
|
||||
<string name="screen_report_content_block_user">"Zablokovat uživatele"</string>
|
||||
|
|
|
|||
|
|
@ -7,10 +7,18 @@
|
|||
<string name="emoji_picker_category_objects">"Gwrthrychau"</string>
|
||||
<string name="emoji_picker_category_people">"Wynebau Hapus a Phobl"</string>
|
||||
<string name="emoji_picker_category_places">"Teithio a Llefydd"</string>
|
||||
<string name="emoji_picker_category_recent">"Emojis diweddar"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symbolau"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Efallai na fydd capsiynau yn weladwy i bobl sy\'n defnyddio apiau hŷn."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tapiwch i newid ansawdd llwytho\'r fideo"</string>
|
||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Nid oedd modd llwytho\'r ffeil."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Wedi methu â phrosesu cyfryngau i\'w llwytho, ceisiwch eto."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Wedi methu llwytho cyfryngau, ceisiwch eto."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_message">"Y maint ffeil mwyaf a ganiateir yw %1$s ."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_title">"Mae\'r ffeil yn rhy fawr i\'w llwytho"</string>
|
||||
<string name="screen_media_upload_preview_item_count">"Eitem %1$d o %2$d"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimeiddio ansawdd delwedd"</string>
|
||||
<string name="screen_media_upload_preview_processing">"Prosesu…"</string>
|
||||
<string name="screen_report_content_block_user">"Rhwystro defnyddiwr"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Gwiriwch a ydych am guddio\'r holl negeseuon presennol ac yn y dyfodol gan y defnyddiwr hwn"</string>
|
||||
<string name="screen_report_content_explanation">"Bydd y neges hon yn cael ei hadrodd i weinyddwr eich gweinyddwr cartref. Fyddan nhw ddim yn gallu darllen unrhyw negeseuon wedi\'u hamgryptio."</string>
|
||||
|
|
@ -38,6 +46,14 @@
|
|||
<string name="screen_room_timeline_less_reactions">"Dangos llai"</string>
|
||||
<string name="screen_room_timeline_message_copied">"Neges wedi\'i chopïo"</string>
|
||||
<string name="screen_room_timeline_no_permission_to_post">"Does gennych chi ddim caniatâd i bostio i\'r ystafell hon"</string>
|
||||
<plurals name="screen_room_timeline_reaction_a11y">
|
||||
<item quantity="zero">"Ymatebodd %1$d aelodau gyda %2$s"</item>
|
||||
<item quantity="one">"Ymatebodd %1$d aelodau gyda %2$s"</item>
|
||||
<item quantity="two">"Ymatebodd %1$d aelod gyda %2$s"</item>
|
||||
<item quantity="few">"Ymatebodd %1$d aelod gyda %2$s"</item>
|
||||
<item quantity="many">"Ymatebodd %1$d aelod gyda %2$s"</item>
|
||||
<item quantity="other">"Ymatebodd %1$d aelod gyda %2$s"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_timeline_reaction_you_a11y">"Rydych chi wedi ymateb gyda %1$s"</string>
|
||||
<string name="screen_room_timeline_reactions_show_less">"Dangos llai"</string>
|
||||
<string name="screen_room_timeline_reactions_show_more">"Dangos rhagor"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_encrypted_history_banner_unverified">"Message history is unavailable in this room. Confirm this device to see your message history."</string>
|
||||
</resources>
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
<string name="emoji_picker_category_objects">"Esineet"</string>
|
||||
<string name="emoji_picker_category_people">"Hymiöt ja ihmiset"</string>
|
||||
<string name="emoji_picker_category_places">"Matkustaminen ja paikat"</string>
|
||||
<string name="emoji_picker_category_recent">"Viimeaikaiset emojit"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symbolit"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Kuvatekstit eivät välttämättä näy ihmisille, jotka käyttävät vanhempia sovelluksia."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Napauta muuttaaksesi videon lähetyslaatua"</string>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@
|
|||
<string name="emoji_picker_category_places">"旅行與景點"</string>
|
||||
<string name="emoji_picker_category_symbols">"標誌"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"使用舊應用程式的使用者可能看不到標題。"</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"輕點即可變更影片上傳品質"</string>
|
||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"無法上傳檔案。"</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"無法處理要上傳的媒體,請再試一次。"</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"無法上傳媒體檔案,請稍後再試。"</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_message">"允許的最大檔案大小為 %1$s。"</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_title">"檔案太大,無法上傳"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"最佳化影像品質"</string>
|
||||
<string name="screen_media_upload_preview_processing">"正在處理……"</string>
|
||||
<string name="screen_report_content_block_user">"封鎖使用者"</string>
|
||||
<string name="screen_report_content_block_user_hint">"檢查您是否要隱藏所有來自此使用者的目前及未來的訊息"</string>
|
||||
<string name="screen_report_content_explanation">"此訊息將會回報給您的家伺服器管理員。他們將無法讀取任何已加密的訊息。"</string>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<string name="emoji_picker_category_objects">"Objects"</string>
|
||||
<string name="emoji_picker_category_people">"Smileys & People"</string>
|
||||
<string name="emoji_picker_category_places">"Travel & Places"</string>
|
||||
<string name="emoji_picker_category_recent">"Recent emojis"</string>
|
||||
<string name="emoji_picker_category_symbols">"Symbols"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Captions might not be visible to people using older apps."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tap to change the video upload quality"</string>
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
<string name="screen_media_upload_preview_error_failed_sending">"Failed uploading media, please try again."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_message">"The maximum file size allowed is %1$s."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_title">"The file is too large to upload"</string>
|
||||
<string name="screen_media_upload_preview_item_count">"Item %1$d of %2$d"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimise image quality"</string>
|
||||
<string name="screen_media_upload_preview_processing">"Processing…"</string>
|
||||
<string name="screen_report_content_block_user">"Block user"</string>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMess
|
|||
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
|
|
@ -61,7 +62,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembersState
|
|||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
|
|
@ -1181,7 +1181,7 @@ class MessagesPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvents.HandleAction(
|
||||
action = TimelineItemAction.ReplyInThread,
|
||||
event = aMessageEvent(threadInfo = EventThreadInfo(A_THREAD_ID, null))
|
||||
event = aMessageEvent(threadInfo = TimelineItemThreadInfo.ThreadResponse(A_THREAD_ID))
|
||||
))
|
||||
awaitItem()
|
||||
openThreadLambda.assertions().isCalledOnce().with(value(A_THREAD_ID), value(null))
|
||||
|
|
@ -1204,7 +1204,7 @@ class MessagesPresenterTest {
|
|||
event = aMessageEvent(
|
||||
// The event id will be used as the thread id instead
|
||||
eventId = AN_EVENT_ID,
|
||||
threadInfo = EventThreadInfo(null, null),
|
||||
threadInfo = null,
|
||||
)
|
||||
))
|
||||
awaitItem()
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser
|
|||
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
|
||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRtcNotificationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
|
|
@ -31,7 +32,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
|||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.room.BaseRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_CAPTION
|
||||
|
|
@ -198,7 +198,7 @@ class ActionListPresenterTest {
|
|||
val messageEvent = aMessageEvent(
|
||||
isMine = false,
|
||||
isEditable = false,
|
||||
threadInfo = EventThreadInfo(threadRootId = A_THREAD_ID, threadSummary = null),
|
||||
threadInfo = TimelineItemThreadInfo.ThreadResponse(threadRootId = A_THREAD_ID),
|
||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE)
|
||||
)
|
||||
initialState.eventSink.invoke(
|
||||
|
|
@ -432,7 +432,7 @@ class ActionListPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
val messageEvent = aMessageEvent(
|
||||
isMine = true,
|
||||
threadInfo = EventThreadInfo(threadRootId = A_THREAD_ID, threadSummary = null),
|
||||
threadInfo = TimelineItemThreadInfo.ThreadResponse(threadRootId = A_THREAD_ID),
|
||||
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false, formattedBody = A_MESSAGE)
|
||||
)
|
||||
initialState.eventSink.invoke(
|
||||
|
|
@ -1264,7 +1264,7 @@ class ActionListPresenterTest {
|
|||
content = aTimelineItemVoiceContent(
|
||||
caption = null,
|
||||
),
|
||||
threadInfo = EventThreadInfo(A_THREAD_ID, null)
|
||||
threadInfo = TimelineItemThreadInfo.ThreadResponse(threadRootId = A_THREAD_ID)
|
||||
)
|
||||
initialState.eventSink.invoke(
|
||||
ActionListEvents.ComputeForMessage(
|
||||
|
|
@ -1368,7 +1368,7 @@ class ActionListPresenterTest {
|
|||
content = aTimelineItemVoiceContent(
|
||||
caption = null,
|
||||
),
|
||||
threadInfo = EventThreadInfo(A_THREAD_ID, null),
|
||||
threadInfo = TimelineItemThreadInfo.ThreadResponse(threadRootId = A_THREAD_ID),
|
||||
)
|
||||
|
||||
assertThat(messageEvent.isRemote).isTrue()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
|||
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
|
|
@ -19,7 +20,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShieldProvider
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.SendHandleProvider
|
||||
|
|
@ -41,7 +41,7 @@ internal fun aMessageEvent(
|
|||
canBeRepliedTo: Boolean = true,
|
||||
content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, formattedBody = A_MESSAGE, isEdited = false),
|
||||
inReplyTo: InReplyToDetails? = null,
|
||||
threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
|
||||
threadInfo: TimelineItemThreadInfo? = null,
|
||||
sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID),
|
||||
debugInfoProvider: TimelineItemDebugInfoProvider = TimelineItemDebugInfoProvider { aTimelineItemDebugInfo() },
|
||||
messageShieldProvider: MessageShieldProvider = MessageShieldProvider { null },
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.features.messages.impl.fixtures
|
||||
|
||||
import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter
|
||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory
|
||||
|
|
@ -30,7 +31,9 @@ import io.element.android.features.poll.test.pollcontent.FakePollContentStateFac
|
|||
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
|
||||
|
|
@ -75,11 +78,13 @@ internal fun TestScope.aTimelineItemsFactory(
|
|||
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
|
||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
|
||||
currentSessionIdHolder = CurrentSessionIdHolder(matrixClient),
|
||||
),
|
||||
matrixClient = matrixClient,
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
config = config
|
||||
config = config,
|
||||
summaryFormatter = FakeMessageSummaryFormatter(),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -95,7 +100,7 @@ internal fun TestScope.aTimelineItemsFactory(
|
|||
|
||||
internal fun aTimelineEventFormatter(): TimelineEventFormatter {
|
||||
return object : TimelineEventFormatter {
|
||||
override fun format(event: EventTimelineItem): CharSequence {
|
||||
override fun format(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): CharSequence? {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
package io.element.android.features.messages.impl.messagesummary
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
|
||||
|
||||
class FakeMessageSummaryFormatter : MessageSummaryFormatter {
|
||||
private var result = "A message"
|
||||
|
||||
override fun format(event: TimelineItem.Event): String = result
|
||||
override fun format(content: TimelineItemEventContent): String = result
|
||||
|
||||
fun givenMessageResult(value: String) {
|
||||
result = value
|
||||
|
|
|
|||
|
|
@ -750,7 +750,7 @@ class TimelineItemContentMessageFactoryTest {
|
|||
body: String = "Body",
|
||||
inReplyTo: InReplyTo? = null,
|
||||
isEdited: Boolean = false,
|
||||
threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
|
||||
threadInfo: EventThreadInfo? = null,
|
||||
type: MessageType,
|
||||
): MessageContent {
|
||||
return MessageContent(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
|
|
@ -42,7 +41,7 @@ class TimelineItemGrouperTest {
|
|||
isEditable = false,
|
||||
canBeRepliedTo = false,
|
||||
inReplyTo = null,
|
||||
threadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
|
||||
threadInfo = null,
|
||||
origin = null,
|
||||
timelineItemDebugInfoProvider = { aTimelineItemDebugInfo() },
|
||||
messageShieldProvider = { null },
|
||||
|
|
|
|||
|
|
@ -7,9 +7,18 @@
|
|||
|
||||
package io.element.android.features.poll.api.pollcontent
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
|
||||
interface PollContentStateFactory {
|
||||
suspend fun create(event: EventTimelineItem, content: PollContent): PollContentState
|
||||
suspend fun create(eventTimelineItem: EventTimelineItem, content: PollContent): PollContentState {
|
||||
return create(
|
||||
eventId = eventTimelineItem.eventId,
|
||||
isEditable = eventTimelineItem.isEditable,
|
||||
isOwn = eventTimelineItem.isOwn,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
suspend fun create(eventId: EventId?, isEditable: Boolean, isOwn: Boolean, content: PollContent): PollContentState
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="a11y_polls_percent_of_total">
|
||||
<item quantity="zero">"%1$d y cant o\'r holl bleidleisiau"</item>
|
||||
<item quantity="one">"%1$d y cant o\'r holl bleidleisiau"</item>
|
||||
<item quantity="two">"%1$d y cant o\'r holl bleidleisiau"</item>
|
||||
<item quantity="few">"%1$d y cant o\'r holl bleidleisiau"</item>
|
||||
<item quantity="many">"%1$d y cant o\'r holl bleidleisiau"</item>
|
||||
<item quantity="other">"%1$d y cant o\'r holl bleidleisiau"</item>
|
||||
</plurals>
|
||||
<string name="a11y_polls_will_remove_selection">"Bydd yn dileu\'r dewis blaenorol"</string>
|
||||
<string name="a11y_polls_winning_answer">"Dyma\'r ateb buddugol"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,12 @@ class PollHistoryItemsFactory(
|
|||
return when (timelineItem) {
|
||||
is MatrixTimelineItem.Event -> {
|
||||
val pollContent = timelineItem.event.content as? PollContent ?: return null
|
||||
val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent)
|
||||
val pollContentState = pollContentStateFactory.create(
|
||||
eventId = timelineItem.eventId,
|
||||
isEditable = timelineItem.event.isEditable,
|
||||
isOwn = timelineItem.event.isOwn,
|
||||
content = pollContent,
|
||||
)
|
||||
PollHistoryItem(
|
||||
formattedDate = dateFormatter.format(
|
||||
timestamp = timelineItem.event.timestamp,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import io.element.android.features.poll.api.pollcontent.PollContentState
|
|||
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.poll.isDisclosed
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
|
@ -25,8 +25,10 @@ class DefaultPollContentStateFactory(
|
|||
private val matrixClient: MatrixClient,
|
||||
) : PollContentStateFactory {
|
||||
override suspend fun create(
|
||||
event: EventTimelineItem,
|
||||
content: PollContent
|
||||
eventId: EventId?,
|
||||
isEditable: Boolean,
|
||||
isOwn: Boolean,
|
||||
content: PollContent,
|
||||
): PollContentState {
|
||||
val totalVoteCount = content.votes.flatMap { it.value }.size
|
||||
val myVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
|
||||
|
|
@ -59,13 +61,13 @@ class DefaultPollContentStateFactory(
|
|||
}
|
||||
|
||||
return PollContentState(
|
||||
eventId = event.eventId,
|
||||
eventId = eventId,
|
||||
question = content.question,
|
||||
answerItems = answerItems.toImmutableList(),
|
||||
pollKind = content.kind,
|
||||
isPollEditable = event.isEditable,
|
||||
isPollEditable = isEditable,
|
||||
isPollEnded = isPollEnded,
|
||||
isMine = event.isOwn,
|
||||
isMine = isOwn,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,20 +10,20 @@ package io.element.android.features.poll.test.pollcontent
|
|||
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentState
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class FakePollContentStateFactory : PollContentStateFactory {
|
||||
override suspend fun create(event: EventTimelineItem, content: PollContent): PollContentState {
|
||||
override suspend fun create(eventId: EventId?, isEditable: Boolean, isOwn: Boolean, content: PollContent): PollContentState {
|
||||
return PollContentState(
|
||||
eventId = event.eventId,
|
||||
eventId = eventId,
|
||||
question = content.question,
|
||||
answerItems = emptyList<PollAnswerItem>().toImmutableList(),
|
||||
pollKind = content.kind,
|
||||
isPollEditable = event.isEditable,
|
||||
isPollEditable = isEditable,
|
||||
isPollEnded = content.endTime != null,
|
||||
isMine = event.isOwn
|
||||
isMine = isOwn,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ dependencies {
|
|||
implementation(projects.features.rageshake.api)
|
||||
implementation(projects.features.lockscreen.api)
|
||||
implementation(projects.features.analytics.api)
|
||||
implementation(projects.features.ftue.api)
|
||||
implementation(projects.features.licenses.api)
|
||||
implementation(projects.features.logout.api)
|
||||
implementation(projects.features.deactivation.api)
|
||||
|
|
@ -101,7 +100,6 @@ dependencies {
|
|||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushstore.test)
|
||||
testImplementation(projects.features.ftue.test)
|
||||
testImplementation(projects.features.invite.test)
|
||||
testImplementation(projects.features.rageshake.test)
|
||||
testImplementation(projects.features.logout.test)
|
||||
|
|
|
|||
|
|
@ -208,6 +208,10 @@ class PreferencesFlowNode(
|
|||
navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
override fun openIgnoredUsers() {
|
||||
backstack.push(NavTarget.BlockedUsers)
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import coil3.SingletonImageLoader
|
|||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.Provider
|
||||
import io.element.android.features.ftue.api.state.FtueService
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
|
|
@ -36,7 +35,6 @@ class DefaultClearCacheUseCase(
|
|||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val defaultCacheService: DefaultCacheService,
|
||||
private val okHttpClient: Provider<OkHttpClient>,
|
||||
private val ftueService: FtueService,
|
||||
private val pushService: PushService,
|
||||
private val seenInvitesStore: SeenInvitesStore,
|
||||
private val activeRoomsHolder: ActiveRoomsHolder,
|
||||
|
|
@ -56,7 +54,6 @@ class DefaultClearCacheUseCase(
|
|||
// Clear app cache
|
||||
context.cacheDir.deleteRecursively()
|
||||
// Clear some settings
|
||||
ftueService.reset()
|
||||
seenInvitesStore.clear()
|
||||
// Ensure any error will be displayed again
|
||||
pushService.setIgnoreRegistrationError(matrixClient.sessionId, false)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@
|
|||
<string name="screen_advanced_settings_media_compression_description">"Llwythwch i fyny lluniau a fideos yn gynt a lleihau\'r defnydd o ddata"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Optimeiddio ansawdd y cyfryngau"</string>
|
||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"Cymedroli a Diogelwch"</string>
|
||||
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimeiddio delweddau\'n awtomatig ar gyfer llwytho cyflymach a meintiau ffeiliau llai."</string>
|
||||
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimeiddio ansawdd llwytho delweddau"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s Tapiwch yma i newid."</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Uchel (1080p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Isel (480c)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Safonol (720p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Ansawdd lwytho fideo"</string>
|
||||
<string name="screen_advanced_settings_push_provider_android">"Darparwr hysbysiad gwthio"</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Analluogi\'r golygydd testun cyfoethog i deipio Markdown â llaw."</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Derbynebau darllen"</string>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"Optimoi kuvat automaattisesti nopeampia lähetysnopeuksia ja pienempiä tiedostokokoja varten."</string>
|
||||
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"Optimoi kuvien lähetyslaatu"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s. Napauta tästä vaihtaaksesi."</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"Korkea (1080p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"Matala (480p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"Normaali (720p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"Videon lähetyslaatu"</string>
|
||||
<string name="screen_advanced_settings_push_provider_android">"Push-ilmoitusten tarjoaja"</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Ota rikastettu tekstieditori pois käytöstä, jotta voit kirjoittaa Markdownia manuaalisesti."</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@
|
|||
<string name="screen_advanced_settings_media_compression_description">"上傳照片與影片更快且減少資料使用量"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"最佳化媒體品質"</string>
|
||||
<string name="screen_advanced_settings_moderation_and_safety_section_title">"管理與安全"</string>
|
||||
<string name="screen_advanced_settings_optimise_image_upload_quality_description">"自動最佳化影像以提供更快的上傳速度與較小的檔案大小。"</string>
|
||||
<string name="screen_advanced_settings_optimise_image_upload_quality_title">"最佳化影像上傳品質"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_description">"%1$s。輕點此處以變更。"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_high">"高 (1080p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_low">"低 (480p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_standard">"標準 (720p)"</string>
|
||||
<string name="screen_advanced_settings_optimise_video_upload_quality_title">"視訊上傳品質"</string>
|
||||
<string name="screen_advanced_settings_push_provider_android">"推播通知提供者"</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"手動輸入 Markdown,停用格式化文字編輯器。"</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"已讀回條"</string>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ package io.element.android.features.preferences.impl.tasks
|
|||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.ftue.test.FakeFtueService
|
||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||
import io.element.android.features.preferences.impl.DefaultCacheService
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
|
@ -41,10 +40,6 @@ class DefaultClearCacheUseCaseTest {
|
|||
clearCacheLambda = clearCacheLambda,
|
||||
)
|
||||
val defaultCacheService = DefaultCacheService()
|
||||
val resetFtueLambda = lambdaRecorder<Unit> { }
|
||||
val ftueService = FakeFtueService(
|
||||
resetLambda = resetFtueLambda,
|
||||
)
|
||||
val setIgnoreRegistrationErrorLambda = lambdaRecorder<SessionId, Boolean, Unit> { _, _ -> }
|
||||
val resetBatteryOptimizationStateResult = lambdaRecorder<Unit> { }
|
||||
val pushService = FakePushService(
|
||||
|
|
@ -59,7 +54,6 @@ class DefaultClearCacheUseCaseTest {
|
|||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
defaultCacheService = defaultCacheService,
|
||||
okHttpClient = { OkHttpClient.Builder().build() },
|
||||
ftueService = ftueService,
|
||||
pushService = pushService,
|
||||
seenInvitesStore = seenInvitesStore,
|
||||
activeRoomsHolder = activeRoomsHolder,
|
||||
|
|
@ -67,7 +61,6 @@ class DefaultClearCacheUseCaseTest {
|
|||
defaultCacheService.clearedCacheEventFlow.test {
|
||||
sut.invoke()
|
||||
clearCacheLambda.assertions().isCalledOnce()
|
||||
resetFtueLambda.assertions().isCalledOnce()
|
||||
setIgnoreRegistrationErrorLambda.assertions().isCalledOnce()
|
||||
.with(value(matrixClient.sessionId), value(false))
|
||||
resetBatteryOptimizationStateResult.assertions().isCalledOnce()
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@
|
|||
<string name="screen_room_details_error_loading_notification_settings">"Při načítání nastavení oznámení došlo k chybě."</string>
|
||||
<string name="screen_room_details_error_muting">"Ztišení této místnosti se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_room_details_error_unmuting">"Nepodařilo se zrušit ztišení této místnosti, zkuste to prosím znovu."</string>
|
||||
<string name="screen_room_details_invite_people_dont_close">"Nezavírejte aplikaci, dokud neskončíte."</string>
|
||||
<string name="screen_room_details_invite_people_preparing">"Příprava pozvánek…"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Pozvat přátele"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"Opustit konverzaci"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Opustit místnost"</string>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<string name="screen_polls_history_title">"Pleidleisiau"</string>
|
||||
<string name="screen_room_change_permissions_administrators">"Gweinyddwyr yn unig"</string>
|
||||
<string name="screen_room_change_permissions_ban_people">"Gwahardd pobl"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Dileu negeseuon"</string>
|
||||
<string name="screen_room_change_permissions_delete_messages">"Tynnu negeseuon"</string>
|
||||
<string name="screen_room_change_permissions_everyone">"Pawb"</string>
|
||||
<string name="screen_room_change_permissions_invite_people">"Gwahodd pobl a derbyn ceisiadau i ymuno"</string>
|
||||
<string name="screen_room_change_permissions_member_moderation">"Cymedroli aelodau"</string>
|
||||
|
|
@ -22,13 +22,17 @@
|
|||
<string name="screen_room_change_role_administrators_title">"Golygu Gweinyddwyr"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych chi\'n hyrwyddo\'r defnyddiwr i gael yr un lefel pŵer â chi."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Ychwanegu Gweinyddwr?"</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_description">"Fyddwch chi ddim yn gallu dadwneud y weithred hon. Rydych yn trosglwyddo\'r berchnogaeth i\'r defnyddwyr a ddewiswyd. Unwaith y byddwch yn gadael bydd hyn yn barhaol."</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_title">"Trosglwyddo perchnogaeth?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Gostwng"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Fyddwch chi ddim yn gallu dadwneud y newid hwn gan eich bod yn israddio eich hun, os mai chi yw\'r defnyddiwr breintiedig olaf yn yr ystafell bydd yn amhosibl adennill breintiau."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Israddio eich hun?"</string>
|
||||
<string name="screen_room_change_role_invited_member_name">"%1$s (Yn aros)"</string>
|
||||
<string name="screen_room_change_role_invited_member_name_android">"Yn aros"</string>
|
||||
<string name="screen_room_change_role_moderators_admin_section_footer">"Mae gan weinyddwyr freintiau cymedrolwr yn awtomatig"</string>
|
||||
<string name="screen_room_change_role_moderators_owner_section_footer">"Mae gan berchnogion freintiau gweinyddwr yn awtomatig."</string>
|
||||
<string name="screen_room_change_role_moderators_title">"Golygu Cymedrolwyr"</string>
|
||||
<string name="screen_room_change_role_owners_title">"Dewiswch Berchnogion"</string>
|
||||
<string name="screen_room_change_role_section_administrators">"Gweinyddwyr"</string>
|
||||
<string name="screen_room_change_role_section_moderators">"Cymedrolwyr"</string>
|
||||
<string name="screen_room_change_role_section_users">"Aelodau"</string>
|
||||
|
|
@ -46,6 +50,8 @@
|
|||
<string name="screen_room_details_error_loading_notification_settings">"Digwyddodd gwall wrth lwytho gosodiadau hysbysu."</string>
|
||||
<string name="screen_room_details_error_muting">"Wedi methu tewi\'r ystafell hon, ceisiwch eto."</string>
|
||||
<string name="screen_room_details_error_unmuting">"Wedi methu dad-dewi\'r ystafell hon, ceisiwch eto."</string>
|
||||
<string name="screen_room_details_invite_people_dont_close">"Peidiwch â chau\'r ap nes ei fod wedi gorffen."</string>
|
||||
<string name="screen_room_details_invite_people_preparing">"Wrthi\'n paratoi gwahoddiadau…"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Gwahodd pobl"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"Gadael y sgwrs"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Gadael yr ystafell"</string>
|
||||
|
|
@ -83,6 +89,7 @@
|
|||
<string name="screen_room_member_list_pending_header_title">"Dan ystyriaeth"</string>
|
||||
<string name="screen_room_member_list_role_administrator">"Gweinyddwr"</string>
|
||||
<string name="screen_room_member_list_role_moderator">"Cymedrolwr"</string>
|
||||
<string name="screen_room_member_list_role_owner">"Perchennog"</string>
|
||||
<string name="screen_room_member_list_room_members_header_title">"Aelodau\'r ystafell"</string>
|
||||
<string name="screen_room_member_list_unbanning_user">"Dad-wahardd %1$s"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Caniatáu gosodiad personol"</string>
|
||||
|
|
@ -100,12 +107,14 @@
|
|||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Crybwylliadau ac Allweddeiriau\'n unig"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"Yn yr ystafell hon, rhowch wybod i mi am"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins">"Gweinyddwyr"</string>
|
||||
<string name="screen_room_roles_and_permissions_admins_and_owners">"Gweinyddwyr a pherchnogion"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_my_role">"Newid fy rôl"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Israddio aelod"</string>
|
||||
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Israddio cymedrolwr"</string>
|
||||
<string name="screen_room_roles_and_permissions_member_moderation">"Cymedroli aelodau"</string>
|
||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Negeseuon a chynnwys"</string>
|
||||
<string name="screen_room_roles_and_permissions_moderators">"Cymedrolwyr"</string>
|
||||
<string name="screen_room_roles_and_permissions_owners">"Perchnogion"</string>
|
||||
<string name="screen_room_roles_and_permissions_permissions_header">"Caniatâd"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset">"Ailosod caniatâd"</string>
|
||||
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Ar ôl i chi ailosod caniatâd, byddwch yn colli\'r gosodiadau cyfredol."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients can unlock them."</string>
|
||||
</resources>
|
||||
|
|
@ -21,12 +21,12 @@
|
|||
<string name="screen_room_change_permissions_send_messages">"Viestien lähettäminen"</string>
|
||||
<string name="screen_room_change_role_administrators_title">"Muokkaa ylläpitäjiä"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_description">"Et voi peruuttaa tätä toimenpidettä. Ylennät käyttäjän samalle oikeustasolle kuin sinä."</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Lisää ylläpitäjä?"</string>
|
||||
<string name="screen_room_change_role_confirm_add_admin_title">"Lisätäänkö ylläpitäjä?"</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_description">"Et voi kumota tätä toimintoa. Olet siirtämässä omistajuuden valituille käyttäjille. Kun poistut, muutos on pysyvä."</string>
|
||||
<string name="screen_room_change_role_confirm_change_owners_title">"Siirretäänkö omistajuus?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_action">"Alenna"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_description">"Et voi perua tätä muutosta, koska olet alentamassa itseäsi. Jos olet viimeinen oikeutettu henkilö tässä huoneessa, oikeuksia ei voi enää saada takaisin."</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Alenna itsesi?"</string>
|
||||
<string name="screen_room_change_role_confirm_demote_self_title">"Haluatko alentaa itsesi?"</string>
|
||||
<string name="screen_room_change_role_invited_member_name">"%1$s (Kutsuttu)"</string>
|
||||
<string name="screen_room_change_role_invited_member_name_android">"(Kutsuttu)"</string>
|
||||
<string name="screen_room_change_role_moderators_admin_section_footer">"Ylläpitäjillä on automaattisesti valvojan oikeudet"</string>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<string name="screen_room_change_role_section_moderators">"Valvojat"</string>
|
||||
<string name="screen_room_change_role_section_users">"Jäsenet"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_description">"Sinulla on tallentamattomia muutoksia"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Tallenna muutokset?"</string>
|
||||
<string name="screen_room_change_role_unsaved_changes_title">"Tallennetaanko muutokset?"</string>
|
||||
<string name="screen_room_details_add_topic_title">"Lisää aihe"</string>
|
||||
<string name="screen_room_details_badge_encrypted">"Salattu"</string>
|
||||
<string name="screen_room_details_badge_not_encrypted">"Ei salattu"</string>
|
||||
|
|
@ -50,6 +50,8 @@
|
|||
<string name="screen_room_details_error_loading_notification_settings">"Ilmoitusasetuksia ladattaessa tapahtui virhe."</string>
|
||||
<string name="screen_room_details_error_muting">"Tämän huoneen mykistäminen epäonnistui, yritä uudelleen."</string>
|
||||
<string name="screen_room_details_error_unmuting">"Tämän huoneen mykistyksen poistaminen epäonnistui, yritä uudelleen."</string>
|
||||
<string name="screen_room_details_invite_people_dont_close">"Älä sulje sovellusta ennen kuin se on valmis."</string>
|
||||
<string name="screen_room_details_invite_people_preparing">"Valmistellaan kutsuja…"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Kutsu ihmisiä"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"Poistu keskustelusta"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Poistu huoneesta"</string>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
<string name="screen_room_details_error_loading_notification_settings">"Une erreur s’est produite lors du chargement des paramètres de notification."</string>
|
||||
<string name="screen_room_details_error_muting">"Échec de la mise en sourdine de ce salon, veuillez réessayer."</string>
|
||||
<string name="screen_room_details_error_unmuting">"Échec de la désactivation de la mise en sourdine de ce salon, veuillez réessayer."</string>
|
||||
<string name="screen_room_details_invite_people_preparing">"Préparation des invitations…"</string>
|
||||
<string name="screen_room_details_invite_people_title">"Inviter des amis"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"Quitter la discussion"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Quitter le salon"</string>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@
|
|||
<string name="screen_room_details_error_loading_notification_settings">"載入通知設定時發生錯誤。"</string>
|
||||
<string name="screen_room_details_error_muting">"無法關閉聊天室通知,請再試一次。"</string>
|
||||
<string name="screen_room_details_error_unmuting">"無法開啟聊天室通知,請再試一次。"</string>
|
||||
<string name="screen_room_details_invite_people_dont_close">"完成前請勿關閉應用程式。"</string>
|
||||
<string name="screen_room_details_invite_people_preparing">"正在準備邀請……"</string>
|
||||
<string name="screen_room_details_invite_people_title">"邀請夥伴"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"離開對話"</string>
|
||||
<string name="screen_room_details_leave_room_title">"離開聊天室"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_chat_backup_key_backup_action_disable">"Delete message backup"</string>
|
||||
<string name="screen_chat_backup_key_backup_description">"Store your account security and messages securely on the server. This will allow you to view your message history on any new devices. %1$s."</string>
|
||||
<string name="screen_chat_backup_key_backup_title">"Message backup"</string>
|
||||
<string name="screen_chat_backup_key_storage_disabled_error">"Turn on message backup to set it up."</string>
|
||||
<string name="screen_chat_backup_key_storage_toggle_description">"Upload messages from this device"</string>
|
||||
<string name="screen_chat_backup_key_storage_toggle_title">"Allow message backup"</string>
|
||||
<string name="screen_chat_backup_recovery_action_change">"Change backup password"</string>
|
||||
<string name="screen_chat_backup_recovery_action_change_description">"Restore your account security and message history with a backup password if you\'ve lost all your existing devices."</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm">"Enter backup password"</string>
|
||||
<string name="screen_chat_backup_recovery_action_confirm_description">"Your message backup is currently out of sync."</string>
|
||||
<string name="screen_chat_backup_recovery_action_setup">"Set up backup"</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_3">"When asked to confirm your device, select %1$s"</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_4">"Follow the instructions to create a new backup password"</string>
|
||||
<string name="screen_create_new_recovery_key_list_item_5">"Save your new backup password in a password manager or encrypted note"</string>
|
||||
<string name="screen_encryption_reset_bullet_3">"You will need to confirm all your existing devices and verify contacts again"</string>
|
||||
<string name="screen_encryption_reset_footer">"Only start fresh if you don\'t have access to another signed-in device and you\'ve lost your backup password."</string>
|
||||
<string name="screen_encryption_reset_title">"Can\'t confirm? You\'ll need to start fresh."</string>
|
||||
<string name="screen_key_backup_disable_description">"Deleting message backup will remove your account security and messages from the server and turn off the following security features:"</string>
|
||||
<string name="screen_key_backup_disable_title">"Are you sure you want to turn off message backup and delete it?"</string>
|
||||
<string name="screen_recovery_key_change_description">"Get a new backup password if you\'ve lost your existing one. After changing your backup password, your old one will no longer work."</string>
|
||||
<string name="screen_recovery_key_change_generate_key">"Generate a new backup password"</string>
|
||||
<string name="screen_recovery_key_change_success">"Backup password changed"</string>
|
||||
<string name="screen_recovery_key_change_title">"Change backup password?"</string>
|
||||
<string name="screen_recovery_key_confirm_error_content">"Please try again to confirm access to your message backup."</string>
|
||||
<string name="screen_recovery_key_confirm_error_title">"Incorrect backup password"</string>
|
||||
<string name="screen_recovery_key_confirm_key_description">"You might have seen the terms \"recovery key\", \"security key\" or \"security phrase\" instead of \"backup password\". Don\'t worry, this is all the same."</string>
|
||||
<string name="screen_recovery_key_confirm_success">"Backup password confirmed"</string>
|
||||
<string name="screen_recovery_key_confirm_title">"Enter your backup password"</string>
|
||||
<string name="screen_recovery_key_copied_to_clipboard">"Copied backup password"</string>
|
||||
<string name="screen_recovery_key_save_action">"Save backup password"</string>
|
||||
<string name="screen_recovery_key_save_description">"Write down this backup password somewhere safe, like a password manager, encrypted note, or a physical safe."</string>
|
||||
<string name="screen_recovery_key_save_key_description">"Tap to copy backup password"</string>
|
||||
<string name="screen_recovery_key_save_title">"Save your backup password somewhere safe"</string>
|
||||
<string name="screen_recovery_key_setup_confirmation_description">"You will not be able to access your new backup password after this step."</string>
|
||||
<string name="screen_recovery_key_setup_confirmation_title">"Have you saved your backup password?"</string>
|
||||
<string name="screen_recovery_key_setup_description">"Your message backup is protected by a backup password. If you need a new backup password after setup, you can recreate it by selecting ‘Change backup password’."</string>
|
||||
<string name="screen_recovery_key_setup_generate_key">"Generate your backup password"</string>
|
||||
<string name="screen_recovery_key_setup_success">"Backup setup successful"</string>
|
||||
<string name="screen_recovery_key_setup_title">"Set up backup"</string>
|
||||
<string name="screen_reset_encryption_confirmation_alert_title">"Are you sure you want to start fresh?"</string>
|
||||
<string name="screen_reset_encryption_password_subtitle">"Confirm that you want to start fresh."</string>
|
||||
</resources>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<string name="screen_recovery_key_change_generate_key">"Luo uusi palautusavain"</string>
|
||||
<string name="screen_recovery_key_change_generate_key_description">"Älä jaa tätä kenenkään kanssa!"</string>
|
||||
<string name="screen_recovery_key_change_success">"Palautusavain vaihdettu"</string>
|
||||
<string name="screen_recovery_key_change_title">"Vaihda palautusavain?"</string>
|
||||
<string name="screen_recovery_key_change_title">"Vaihdetaanko palautusavain?"</string>
|
||||
<string name="screen_recovery_key_confirm_create_new_recovery_key">"Luo uusi palautusavain"</string>
|
||||
<string name="screen_recovery_key_confirm_description">"Varmista, ettei kukaan näe tätä ruutua!"</string>
|
||||
<string name="screen_recovery_key_confirm_error_content">"Yritä uudelleen vahvistaaksesi pääsyn avainten säilytykseen."</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_identity_confirmation_create_new_recovery_key">"Create a new backup password"</string>
|
||||
<string name="screen_identity_confirmation_subtitle">"Confirm this device to set up secure messaging."</string>
|
||||
<string name="screen_identity_confirmation_title">"Confirm it\'s you"</string>
|
||||
<string name="screen_identity_confirmation_use_recovery_key">"Use backup password"</string>
|
||||
<string name="screen_identity_confirmed_title">"Device confirmed"</string>
|
||||
<string name="screen_session_verification_complete_user_subtitle">"Now you can trust this user when sending or receiving messages."</string>
|
||||
<string name="screen_session_verification_enter_recovery_key">"Enter backup password"</string>
|
||||
<string name="screen_session_verification_request_success_title">"Device confirmed"</string>
|
||||
<string name="screen_session_verification_use_another_device_title">"Open the app on another confirmed device"</string>
|
||||
<string name="screen_session_verification_user_responder_subtitle">"For extra security, another user wants to verify you. You\'ll be shown a set of emojis to compare."</string>
|
||||
</resources>
|
||||
|
|
@ -48,7 +48,7 @@ telephoto = "0.17.0"
|
|||
haze = "1.6.10"
|
||||
|
||||
# Dependency analysis
|
||||
dependencyAnalysis = "2.19.0"
|
||||
dependencyAnalysis = "3.0.4"
|
||||
|
||||
# DI
|
||||
metro = "0.6.4"
|
||||
|
|
@ -74,7 +74,7 @@ kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlin
|
|||
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
|
||||
ksp_gradle_plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
|
||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:34.2.0"
|
||||
google_firebase_bom = "com.google.firebase:firebase-bom:34.3.0"
|
||||
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
||||
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
||||
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
|
||||
|
|
@ -166,7 +166,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
|
|||
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
|
||||
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
||||
# only fix API breaks and may add some TODOs.
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.9.18"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.9.23"
|
||||
|
||||
# Others
|
||||
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||
|
|
@ -196,7 +196,7 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
|||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
||||
maplibre = "org.maplibre.gl:android-sdk:11.13.1"
|
||||
maplibre = "org.maplibre.gl:android-sdk:11.13.5"
|
||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
|
||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
||||
opusencoder = "io.element.android:opusencoder:1.2.0"
|
||||
|
|
@ -219,7 +219,7 @@ inject = "javax.inject:javax.inject:1"
|
|||
metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" }
|
||||
|
||||
# Element Call
|
||||
element_call_embedded = "io.element.android:element-call-embedded:0.15.0"
|
||||
element_call_embedded = "io.element.android:element-call-embedded:0.16.0-rc.4"
|
||||
|
||||
# Auto services
|
||||
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||
TimelineRoom(32.dp),
|
||||
TimelineSender(32.dp),
|
||||
TimelineReadReceipt(16.dp),
|
||||
TimelineThreadLatestEventSender(24.dp),
|
||||
|
||||
ComposerAlert(32.dp),
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,19 @@
|
|||
|
||||
package io.element.android.libraries.eventformatter.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
|
||||
interface TimelineEventFormatter {
|
||||
fun format(event: EventTimelineItem): CharSequence?
|
||||
fun format(event: EventTimelineItem): CharSequence? {
|
||||
return format(
|
||||
content = event.content,
|
||||
isOutgoing = event.isOwn,
|
||||
sender = event.sender,
|
||||
senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
|
||||
)
|
||||
}
|
||||
fun format(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): CharSequence?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import io.element.android.libraries.core.meta.BuildMeta
|
|||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
|
|
@ -43,12 +45,16 @@ class DefaultTimelineEventFormatter(
|
|||
override fun format(event: EventTimelineItem): CharSequence? {
|
||||
val isOutgoing = event.isOwn
|
||||
val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
|
||||
return when (val content = event.content) {
|
||||
return format(event.content, isOutgoing, event.sender, senderDisambiguatedDisplayName)
|
||||
}
|
||||
|
||||
override fun format(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): CharSequence? {
|
||||
return when (content) {
|
||||
is RoomMembershipContent -> {
|
||||
roomMembershipContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is ProfileChangeContent -> {
|
||||
profileChangeContentFormatter.format(content, event.sender, senderDisambiguatedDisplayName, isOutgoing)
|
||||
profileChangeContentFormatter.format(content, sender, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is StateContent -> {
|
||||
stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.Timeline)
|
||||
|
|
@ -66,7 +72,7 @@ class DefaultTimelineEventFormatter(
|
|||
is FailedToParseStateContent,
|
||||
is UnknownContent -> {
|
||||
if (buildMeta.isDebuggable) {
|
||||
error("You should not use this formatter for this event: $event")
|
||||
error("You should not use this formatter for this event content: $content")
|
||||
}
|
||||
sp.getString(CommonStrings.common_unsupported_event)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import com.google.common.truth.Truth.assertWithMessage
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
|
|
@ -175,7 +174,7 @@ class DefaultBaseRoomLastMessageFormatterTest {
|
|||
) {
|
||||
val body = "Shared body"
|
||||
fun createMessageContent(type: MessageType): MessageContent {
|
||||
return MessageContent(body, null, false, EventThreadInfo(null, null), type)
|
||||
return MessageContent(body, null, false, null, type)
|
||||
}
|
||||
|
||||
val sharedContentMessagesTypes = arrayOf(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import com.google.common.truth.Truth.assertWithMessage
|
|||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
|
|
@ -130,7 +129,7 @@ class DefaultPinnedMessagesBannerFormatterTest {
|
|||
fun `Message contents`() {
|
||||
val body = "Shared body"
|
||||
fun createMessageContent(type: MessageType): MessageContent {
|
||||
return MessageContent(body, null, false, EventThreadInfo(null, null), type)
|
||||
return MessageContent(body, null, false, null, type)
|
||||
}
|
||||
|
||||
val sharedContentMessagesTypes = arrayOf(
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
|
||||
data class EventThreadInfo(
|
||||
val threadRootId: ThreadId?,
|
||||
val threadSummary: ThreadSummary?,
|
||||
)
|
||||
sealed interface EventThreadInfo {
|
||||
data class ThreadRoot(val summary: ThreadSummary) : EventThreadInfo
|
||||
data class ThreadResponse(val threadRootId: ThreadId) : EventThreadInfo
|
||||
}
|
||||
|
||||
data class ThreadSummary(
|
||||
val latestEvent: AsyncData<EmbeddedEventInfo>,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ data class MessageContent(
|
|||
val body: String,
|
||||
val inReplyTo: InReplyTo?,
|
||||
val isEdited: Boolean,
|
||||
val threadInfo: EventThreadInfo,
|
||||
val threadInfo: EventThreadInfo?,
|
||||
val type: MessageType
|
||||
) : EventContent
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ class RoomSummaryListProcessor(
|
|||
updates.forEach { update ->
|
||||
applyUpdate(update)
|
||||
}
|
||||
|
||||
// TODO remove once https://github.com/element-hq/element-x-android/issues/5031 has been confirmed as fixed
|
||||
val duplicates = groupingBy { it.roomId }.eachCount().filter { it.value > 1 }
|
||||
if (duplicates.isNotEmpty()) {
|
||||
Timber.e("Found duplicates in room summaries after a list update from the SDK: $duplicates. Updates: $updates")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class RustTimeline(
|
|||
)
|
||||
|
||||
override val forwardPaginationStatus = MutableStateFlow(
|
||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode !is Timeline.Mode.FocusedOnEvent)
|
||||
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode is Timeline.Mode.FocusedOnEvent)
|
||||
)
|
||||
|
||||
init {
|
||||
|
|
@ -221,7 +221,6 @@ class RustTimeline(
|
|||
items = items,
|
||||
hasMoreToLoadBackward = backwardPaginationStatus.hasMoreToLoad,
|
||||
hasMoreToLoadForward = forwardPaginationStatus.hasMoreToLoad,
|
||||
timelineMode = mode,
|
||||
)
|
||||
}
|
||||
.let { items ->
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ private const val MSG_TYPE_GALLERY_UNSTABLE = "dm.filament.gallery"
|
|||
class EventMessageMapper {
|
||||
private val inReplyToMapper by lazy { InReplyToMapper(TimelineEventContentMapper()) }
|
||||
|
||||
fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, threadInfo: EventThreadInfo): MessageContent = message.use {
|
||||
fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, threadInfo: EventThreadInfo?): MessageContent = message.use {
|
||||
val type = it.content.msgType.use(this::mapMessageType)
|
||||
val inReplyToEvent: InReplyTo? = inReplyTo?.use(inReplyToMapper::map)
|
||||
MessageContent(
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class TimelineEventContentMapper(
|
|||
content = map(latestEvent.content),
|
||||
senderId = UserId(latestEvent.sender),
|
||||
senderProfile = latestEvent.senderProfile.map(),
|
||||
timestamp = latestEvent.timestamp.toLong()
|
||||
timestamp = latestEvent.timestamp.toLong(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -89,10 +89,12 @@ class TimelineEventContentMapper(
|
|||
numberOfReplies = numberOfReplies,
|
||||
)
|
||||
}
|
||||
val threadInfo = EventThreadInfo(
|
||||
threadRootId = it.content.threadRoot?.let(::ThreadId),
|
||||
threadSummary = threadSummary,
|
||||
)
|
||||
val threadRootId = it.content.threadRoot?.let(::ThreadId)
|
||||
val threadInfo = when {
|
||||
threadSummary != null -> EventThreadInfo.ThreadRoot(threadSummary)
|
||||
threadRootId != null -> EventThreadInfo.ThreadResponse(threadRootId)
|
||||
else -> null
|
||||
}
|
||||
eventMessageMapper.map(kind, inReplyTo, threadInfo)
|
||||
}
|
||||
is MsgLikeKind.Redacted -> {
|
||||
|
|
|
|||
|
|
@ -18,9 +18,8 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
|
|||
items: List<MatrixTimelineItem>,
|
||||
hasMoreToLoadBackward: Boolean,
|
||||
hasMoreToLoadForward: Boolean,
|
||||
timelineMode: Timeline.Mode,
|
||||
): List<MatrixTimelineItem> {
|
||||
val shouldAddForwardLoadingIndicator = timelineMode is Timeline.Mode.Live && hasMoreToLoadForward && items.isNotEmpty()
|
||||
val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty()
|
||||
val currentTimestamp = systemClock.epochMillis()
|
||||
return buildList {
|
||||
if (hasMoreToLoadBackward) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import org.matrix.rustcomponents.sdk.SpaceRoom
|
|||
|
||||
fun aRustSpaceRoom(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
isDirect: Boolean = false,
|
||||
canonicalAlias: String? = null,
|
||||
name: String? = null,
|
||||
topic: String? = null,
|
||||
|
|
@ -31,6 +32,7 @@ fun aRustSpaceRoom(
|
|||
heroes: List<RoomHero> = emptyList(),
|
||||
) = SpaceRoom(
|
||||
roomId = roomId.value,
|
||||
isDirect = isDirect,
|
||||
canonicalAlias = canonicalAlias,
|
||||
name = name,
|
||||
topic = topic,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ class LoadingIndicatorsPostProcessorTest {
|
|||
items = listOf(messageEvent, messageEvent2),
|
||||
hasMoreToLoadBackward = true,
|
||||
hasMoreToLoadForward = false,
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
)
|
||||
assertThat(result).containsExactly(
|
||||
MatrixTimelineItem.Virtual(
|
||||
|
|
@ -47,7 +46,6 @@ class LoadingIndicatorsPostProcessorTest {
|
|||
items = listOf(messageEvent, messageEvent2),
|
||||
hasMoreToLoadBackward = false,
|
||||
hasMoreToLoadForward = true,
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
)
|
||||
assertThat(result).containsExactly(
|
||||
messageEvent,
|
||||
|
|
@ -70,7 +68,6 @@ class LoadingIndicatorsPostProcessorTest {
|
|||
items = listOf(messageEvent, messageEvent2),
|
||||
hasMoreToLoadBackward = true,
|
||||
hasMoreToLoadForward = true,
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
)
|
||||
assertThat(result).containsExactly(
|
||||
MatrixTimelineItem.Virtual(
|
||||
|
|
@ -100,7 +97,6 @@ class LoadingIndicatorsPostProcessorTest {
|
|||
items = listOf(),
|
||||
hasMoreToLoadBackward = true,
|
||||
hasMoreToLoadForward = true,
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
)
|
||||
assertThat(result).containsExactly(
|
||||
MatrixTimelineItem.Virtual(
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ fun aMessageContent(
|
|||
body: String = "body",
|
||||
inReplyTo: InReplyTo? = null,
|
||||
isEdited: Boolean = false,
|
||||
threadInfo: EventThreadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
|
||||
threadInfo: EventThreadInfo? = null,
|
||||
messageType: MessageType = TextMessageType(
|
||||
body = body,
|
||||
formatted = null
|
||||
|
|
|
|||
|
|
@ -134,10 +134,7 @@ class InReplyToDetailsOtherProvider : InReplyToDetailsProvider() {
|
|||
private fun aMessageContent(
|
||||
body: String,
|
||||
type: MessageType,
|
||||
threadInfo: EventThreadInfo = EventThreadInfo(
|
||||
threadRootId = null,
|
||||
threadSummary = null,
|
||||
),
|
||||
threadInfo: EventThreadInfo? = null,
|
||||
) = MessageContent(
|
||||
body = body,
|
||||
inReplyTo = null,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
package io.element.android.libraries.matrix.ui.messages.reply
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
|
|
@ -70,7 +69,7 @@ class InReplyToDetailTest {
|
|||
body = "**Hello!**",
|
||||
inReplyTo = null,
|
||||
isEdited = false,
|
||||
threadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
|
||||
threadInfo = null,
|
||||
type = TextMessageType(
|
||||
body = "**Hello!**",
|
||||
formatted = FormattedBody(
|
||||
|
|
@ -95,7 +94,7 @@ class InReplyToDetailTest {
|
|||
body = "**Hello!**",
|
||||
inReplyTo = null,
|
||||
isEdited = false,
|
||||
threadInfo = EventThreadInfo(threadRootId = null, threadSummary = null),
|
||||
threadInfo = null,
|
||||
type = TextMessageType(
|
||||
body = "**Hello!**",
|
||||
formatted = null,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,4 @@ interface PermissionStateProvider {
|
|||
|
||||
suspend fun setPermissionAsked(permission: String, value: Boolean)
|
||||
fun isPermissionAsked(permission: String): Flow<Boolean>
|
||||
|
||||
suspend fun resetPermission(permission: String)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,5 +43,6 @@ dependencies {
|
|||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.permissions.test)
|
||||
testImplementation(projects.libraries.troubleshoot.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue