mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Merge pull request #7517 from element-hq/feature/bma/removeUserStatusFeatureFlag
Enable user status feature and hide status text in the timeline
This commit is contained in:
@@ -30,8 +30,6 @@ import io.element.android.libraries.architecture.Presenter
|
|||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
@@ -67,7 +65,6 @@ class LoggedInPresenter(
|
|||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
||||||
private val featureFlagService: FeatureFlagService,
|
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
) : Presenter<LoggedInState> {
|
) : Presenter<LoggedInState> {
|
||||||
private val localNetworkPermissionsPresenter: PermissionsPresenter =
|
private val localNetworkPermissionsPresenter: PermissionsPresenter =
|
||||||
@@ -124,13 +121,9 @@ class LoggedInPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Keep automatic call status (m.call) in sync with the feature flag and homeserver support.
|
// Keep automatic call status (m.call) in sync with homeserver support.
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus)
|
val enabled = matrixClient.isUserStatusSupported().getOrDefault(false)
|
||||||
.onEach { isUserStatusEnabled ->
|
matrixClient.enableAutomaticCallStatus(enabled)
|
||||||
val enabled = isUserStatusEnabled && matrixClient.isUserStatusSupported().getOrDefault(false)
|
|
||||||
matrixClient.enableAutomaticCallStatus(enabled)
|
|
||||||
}
|
|
||||||
.launchIn(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val networkConnectivity by networkMonitor.connectivity.collectAsState()
|
val networkConnectivity by networkMonitor.connectivity.collectAsState()
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ import im.vector.app.features.analytics.plan.UserProperties
|
|||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
@@ -131,7 +128,6 @@ class LoggedInPresenterTest {
|
|||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||||
).test {
|
).test {
|
||||||
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
||||||
@@ -362,9 +358,6 @@ class LoggedInPresenterTest {
|
|||||||
}
|
}
|
||||||
createLoggedInPresenter(
|
createLoggedInPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
featureFlagService = FakeFeatureFlagService(
|
|
||||||
initialState = mapOf(FeatureFlags.UserStatus.key to true),
|
|
||||||
),
|
|
||||||
).test {
|
).test {
|
||||||
cancelAndConsumeRemainingEvents()
|
cancelAndConsumeRemainingEvents()
|
||||||
assert(enableAutomaticCallStatusLambda)
|
assert(enableAutomaticCallStatusLambda)
|
||||||
@@ -374,7 +367,7 @@ class LoggedInPresenterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - reacts to user status flag being toggled at runtime`() = runTest {
|
fun `present - enables automatic call status when server supports it`() = runTest {
|
||||||
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
|
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
accountManagementUrlResult = { Result.success(null) },
|
accountManagementUrlResult = { Result.success(null) },
|
||||||
@@ -382,23 +375,13 @@ class LoggedInPresenterTest {
|
|||||||
).apply {
|
).apply {
|
||||||
isUserStatusSupportedResult = Result.success(true)
|
isUserStatusSupportedResult = Result.success(true)
|
||||||
}
|
}
|
||||||
val featureFlagService = FakeFeatureFlagService(
|
|
||||||
initialState = mapOf(FeatureFlags.UserStatus.key to false),
|
|
||||||
)
|
|
||||||
createLoggedInPresenter(
|
createLoggedInPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
featureFlagService = featureFlagService,
|
|
||||||
).test {
|
).test {
|
||||||
awaitItem()
|
|
||||||
// Turning the flag on at runtime enables it.
|
|
||||||
featureFlagService.setFeatureEnabled(FeatureFlags.UserStatus, true)
|
|
||||||
cancelAndConsumeRemainingEvents()
|
cancelAndConsumeRemainingEvents()
|
||||||
assert(enableAutomaticCallStatusLambda)
|
assert(enableAutomaticCallStatusLambda)
|
||||||
.isCalledExactly(2)
|
.isCalledOnce()
|
||||||
.withSequence(
|
.with(value(true))
|
||||||
listOf(value(false)),
|
|
||||||
listOf(value(true)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +401,6 @@ class LoggedInPresenterTest {
|
|||||||
),
|
),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
|
||||||
): LoggedInPresenter {
|
): LoggedInPresenter {
|
||||||
return LoggedInPresenter(
|
return LoggedInPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
@@ -430,7 +412,6 @@ class LoggedInPresenterTest {
|
|||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
featureFlagService = featureFlagService,
|
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -76,10 +76,7 @@ class PreferencesRootPresenter(
|
|||||||
}.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
|
|
||||||
val isUserStatusSupported by produceState(false) {
|
val isUserStatusSupported by produceState(false) {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus)
|
value = matrixClient.isUserStatusSupported().getOrDefault(false)
|
||||||
.collect { isUserStatusEnabled ->
|
|
||||||
value = isUserStatusEnabled && matrixClient.isUserStatusSupported().getOrDefault(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val userStatusState = if (isUserStatusSupported) userStatusPresenter.present() else null
|
val userStatusState = if (isUserStatusSupported) userStatusPresenter.present() else null
|
||||||
|
|
||||||
|
|||||||
-7
@@ -152,13 +152,6 @@ enum class FeatureFlags(
|
|||||||
defaultValue = { false },
|
defaultValue = { false },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
UserStatus(
|
|
||||||
key = "feature.user_status",
|
|
||||||
title = "User status",
|
|
||||||
description = "Allow users to set a status (e.g. In a meeting, Away) visible to their contacts.",
|
|
||||||
defaultValue = { false },
|
|
||||||
isFinished = false,
|
|
||||||
),
|
|
||||||
MessageSearch(
|
MessageSearch(
|
||||||
key = "feature.message_search",
|
key = "feature.message_search",
|
||||||
title = "Message search",
|
title = "Message search",
|
||||||
|
|||||||
+3
-1
@@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
|
|||||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
|
||||||
@@ -163,9 +164,10 @@ fun aProfileDetailsReady(
|
|||||||
displayName: String? = USER_NAME_SENDER,
|
displayName: String? = USER_NAME_SENDER,
|
||||||
displayNameAmbiguous: Boolean = false,
|
displayNameAmbiguous: Boolean = false,
|
||||||
avatarUrl: String? = null,
|
avatarUrl: String? = null,
|
||||||
|
displayedStatus: DisplayedStatus? = null
|
||||||
) = ProfileDetails.Ready(
|
) = ProfileDetails.Ready(
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
displayNameAmbiguous = displayNameAmbiguous,
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
displayedStatus = null,
|
displayedStatus = displayedStatus,
|
||||||
)
|
)
|
||||||
|
|||||||
+28
-7
@@ -9,12 +9,15 @@
|
|||||||
package io.element.android.libraries.matrix.ui.messages.sender
|
package io.element.android.libraries.matrix.ui.messages.sender
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -24,7 +27,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
|||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||||
import io.element.android.libraries.matrix.ui.model.toText
|
import io.element.android.libraries.matrix.ui.model.toEmojiText
|
||||||
|
|
||||||
// https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0
|
// https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0
|
||||||
@Composable
|
@Composable
|
||||||
@@ -61,7 +64,7 @@ fun SenderName(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clipToBounds()
|
.clipToBounds()
|
||||||
.alignByBaseline(),
|
.alignByBaseline(),
|
||||||
text = userStatus.toText(),
|
text = userStatus.toEmojiText(),
|
||||||
style = ElementTheme.typography.fontBodyMdMedium,
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
color = ElementTheme.colors.textSecondary,
|
color = ElementTheme.colors.textSecondary,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -136,9 +139,27 @@ private fun RowScope.SecondaryText(
|
|||||||
internal fun SenderNamePreview(
|
internal fun SenderNamePreview(
|
||||||
@PreviewParameter(SenderNameDataPreviewParam::class) senderNameData: SenderNameData,
|
@PreviewParameter(SenderNameDataPreviewParam::class) senderNameData: SenderNameData,
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
SenderName(
|
Column(
|
||||||
senderId = senderNameData.userId,
|
modifier = Modifier.padding(16.dp),
|
||||||
senderProfile = senderNameData.profileDetails,
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
senderNameMode = senderNameData.senderNameMode,
|
) {
|
||||||
)
|
sequenceOf(
|
||||||
|
SenderNameMode.Timeline(mainColor = Color.Red),
|
||||||
|
SenderNameMode.Reply,
|
||||||
|
SenderNameMode.ActionList,
|
||||||
|
).forEach { mode ->
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"Rendering in mode ${mode::class.simpleName}:",
|
||||||
|
style = ElementTheme.typography.fontBodyXsRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
SenderName(
|
||||||
|
senderId = senderNameData.userId,
|
||||||
|
senderProfile = senderNameData.profileDetails,
|
||||||
|
senderNameMode = mode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-29
@@ -8,54 +8,65 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.matrix.ui.messages.sender
|
package io.element.android.libraries.matrix.ui.messages.sender
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.api.user.UserStatus
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
|
||||||
|
|
||||||
data class SenderNameData(
|
data class SenderNameData(
|
||||||
val userId: UserId,
|
val userId: UserId,
|
||||||
val profileDetails: ProfileDetails,
|
val profileDetails: ProfileDetails,
|
||||||
val senderNameMode: SenderNameMode,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
open class SenderNameDataPreviewParam : PreviewParameterProvider<SenderNameData> {
|
open class SenderNameDataPreviewParam : PreviewParameterProvider<SenderNameData> {
|
||||||
override val values: Sequence<SenderNameData>
|
override val values: Sequence<SenderNameData>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
SenderNameMode.Timeline(mainColor = Color.Red),
|
aSenderNameData(),
|
||||||
SenderNameMode.Reply,
|
aSenderNameData(
|
||||||
SenderNameMode.ActionList,
|
displayNameAmbiguous = true,
|
||||||
|
),
|
||||||
|
aSenderNameData(
|
||||||
|
profileDetails = ProfileDetails.Unavailable,
|
||||||
|
),
|
||||||
|
aSenderNameData(
|
||||||
|
displayName = null,
|
||||||
|
),
|
||||||
|
aSenderNameData(
|
||||||
|
displayedStatus = DisplayedStatus.UserSet(
|
||||||
|
status = UserStatus(
|
||||||
|
emoji = "😀",
|
||||||
|
text = "Should not be rendered",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
aSenderNameData(
|
||||||
|
displayedStatus = DisplayedStatus.InCall(0L),
|
||||||
|
),
|
||||||
|
aSenderNameData(
|
||||||
|
displayNameAmbiguous = true,
|
||||||
|
displayedStatus = DisplayedStatus.UserSet(
|
||||||
|
status = UserStatus(
|
||||||
|
emoji = "😀",
|
||||||
|
text = "Should not be rendered",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.flatMap { senderNameMode ->
|
|
||||||
sequenceOf(
|
|
||||||
aSenderNameData(
|
|
||||||
senderNameMode = senderNameMode,
|
|
||||||
),
|
|
||||||
aSenderNameData(
|
|
||||||
senderNameMode = senderNameMode,
|
|
||||||
displayNameAmbiguous = true,
|
|
||||||
),
|
|
||||||
SenderNameData(
|
|
||||||
senderNameMode = senderNameMode,
|
|
||||||
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
|
|
||||||
profileDetails = ProfileDetails.Unavailable,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aSenderNameData(
|
private fun aSenderNameData(
|
||||||
senderNameMode: SenderNameMode,
|
userId: UserId = UserId("@alice:example.com"),
|
||||||
|
displayName: String? = "Alice",
|
||||||
displayNameAmbiguous: Boolean = false,
|
displayNameAmbiguous: Boolean = false,
|
||||||
displayedStatus: DisplayedStatus? = null,
|
displayedStatus: DisplayedStatus? = null,
|
||||||
) = SenderNameData(
|
profileDetails: ProfileDetails = aProfileDetailsReady(
|
||||||
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
|
displayName = displayName,
|
||||||
profileDetails = ProfileDetails.Ready(
|
|
||||||
displayName = "Alice ${senderNameMode.javaClass.simpleName}",
|
|
||||||
displayNameAmbiguous = displayNameAmbiguous,
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
avatarUrl = null,
|
|
||||||
displayedStatus = displayedStatus,
|
displayedStatus = displayedStatus,
|
||||||
),
|
)
|
||||||
senderNameMode = senderNameMode,
|
) = SenderNameData(
|
||||||
|
userId = userId,
|
||||||
|
profileDetails = profileDetails,
|
||||||
)
|
)
|
||||||
|
|||||||
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user