Merge branch 'develop' into feature/fga/space_members_access
This commit is contained in:
commit
3c5d50860d
215 changed files with 2349 additions and 1664 deletions
8
app/proguard-rules.pro
vendored
8
app/proguard-rules.pro
vendored
|
|
@ -66,10 +66,10 @@
|
||||||
-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo
|
-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo
|
||||||
|
|
||||||
# Also needed after AGP 8.13.1 upgrade, it seems like proguard is now more aggressive on removing unused code
|
# Also needed after AGP 8.13.1 upgrade, it seems like proguard is now more aggressive on removing unused code
|
||||||
-keep class org.matrix.rustcomponents.sdk.** { *;}
|
-keep,allowshrinking class org.matrix.rustcomponents.sdk.** { *;}
|
||||||
-keep class uniffi.** { *;}
|
-keep,allowshrinking class uniffi.** { *;}
|
||||||
-keep class io.element.android.x.di.** { *; }
|
-keep,allowshrinking class io.element.android.x.di.** { *; }
|
||||||
-keepclasseswithmembernames,allowoptimization,allowshrinking class io.element.android.** { *; }
|
-keepclasseswithmembernames,allowoptimization,allowshrinking class io.element.android.** { *; }
|
||||||
|
|
||||||
# Keep Metro classes
|
# Keep Metro classes
|
||||||
-keep class dev.zacsweers.metro.** { *; }
|
-keep,allowshrinking class dev.zacsweers.metro.** { *; }
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
testImplementation(projects.features.forward.test)
|
testImplementation(projects.features.forward.test)
|
||||||
|
testImplementation(projects.features.messages.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
testImplementation(projects.services.appnavstate.impl)
|
testImplementation(projects.services.appnavstate.impl)
|
||||||
|
|
|
||||||
|
|
@ -320,7 +320,7 @@ class RootFlowNode(
|
||||||
is ResolvedIntent.Navigation -> {
|
is ResolvedIntent.Navigation -> {
|
||||||
val openingRoomFromNotification = intent.getBooleanExtra(ROOM_OPENED_FROM_NOTIFICATION, false)
|
val openingRoomFromNotification = intent.getBooleanExtra(ROOM_OPENED_FROM_NOTIFICATION, false)
|
||||||
if (openingRoomFromNotification && resolvedIntent.deeplinkData is DeeplinkData.Room) {
|
if (openingRoomFromNotification && resolvedIntent.deeplinkData is DeeplinkData.Room) {
|
||||||
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.NotificationTapOpensTimeline)
|
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.NotificationToMessage)
|
||||||
}
|
}
|
||||||
navigateTo(resolvedIntent.deeplinkData)
|
navigateTo(resolvedIntent.deeplinkData)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ package io.element.android.appnav.di
|
||||||
|
|
||||||
import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider
|
import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider
|
||||||
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
|
import io.element.android.services.analytics.api.watchers.AnalyticsSendMessageWatcher
|
||||||
|
|
||||||
interface TimelineBindings {
|
interface TimelineBindings {
|
||||||
val timelineProvider: TimelineProvider
|
val timelineProvider: TimelineProvider
|
||||||
val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider
|
val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider
|
||||||
|
val analyticsSendMessageWatcher: AnalyticsSendMessageWatcher
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadJoinedRoomFlow
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadJoinedRoomFlow
|
||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationTapOpensTimeline
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationToMessage
|
||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
|
@ -128,7 +128,7 @@ class RoomFlowNode(
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
val parentTransaction = analyticsService.getLongRunningTransaction(NotificationTapOpensTimeline)
|
val parentTransaction = analyticsService.getLongRunningTransaction(NotificationToMessage)
|
||||||
val openRoomTransaction = analyticsService.startLongRunningTransaction(OpenRoom, parentTransaction)
|
val openRoomTransaction = analyticsService.startLongRunningTransaction(OpenRoom, parentTransaction)
|
||||||
analyticsService.startLongRunningTransaction(LoadJoinedRoomFlow, openRoomTransaction)
|
analyticsService.startLongRunningTransaction(LoadJoinedRoomFlow, openRoomTransaction)
|
||||||
resolveRoomId()
|
resolveRoomId()
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,8 @@ class JoinedRoomLoadedFlowNode(
|
||||||
private val callback: Callback = callback()
|
private val callback: Callback = callback()
|
||||||
override val graph = roomGraphFactory.create(inputs.room)
|
override val graph = roomGraphFactory.create(inputs.room)
|
||||||
|
|
||||||
|
private val sendMessageWatcher = (graph as? TimelineBindings)?.analyticsSendMessageWatcher
|
||||||
|
|
||||||
// This is an ugly hack to check activity recreation
|
// This is an ugly hack to check activity recreation
|
||||||
private var currentActivity: Activity? = null
|
private var currentActivity: Activity? = null
|
||||||
|
|
||||||
|
|
@ -109,6 +111,7 @@ class JoinedRoomLoadedFlowNode(
|
||||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||||
activeRoomsHolder.addRoom(inputs.room)
|
activeRoomsHolder.addRoom(inputs.room)
|
||||||
|
sendMessageWatcher?.start()
|
||||||
fetchRoomMembers()
|
fetchRoomMembers()
|
||||||
trackVisitedRoom()
|
trackVisitedRoom()
|
||||||
},
|
},
|
||||||
|
|
@ -120,6 +123,7 @@ class JoinedRoomLoadedFlowNode(
|
||||||
},
|
},
|
||||||
onDestroy = {
|
onDestroy = {
|
||||||
Timber.v("OnDestroy")
|
Timber.v("OnDestroy")
|
||||||
|
sendMessageWatcher?.stop()
|
||||||
// If we're just going through an activity recreation there's no need to destroy the Room object
|
// If we're just going through an activity recreation there's no need to destroy the Room object
|
||||||
// Destroying it would actually cause an issue where its methods can no longer be called
|
// Destroying it would actually cause an issue where its methods can no longer be called
|
||||||
if (currentActivity?.isChangingConfigurations != true) {
|
if (currentActivity?.isChangingConfigurations != true) {
|
||||||
|
|
|
||||||
|
|
@ -19,22 +19,29 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||||
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appnav.di.RoomGraphFactory
|
import io.element.android.appnav.di.RoomGraphFactory
|
||||||
|
import io.element.android.appnav.di.TimelineBindings
|
||||||
import io.element.android.appnav.room.RoomNavigationTarget
|
import io.element.android.appnav.room.RoomNavigationTarget
|
||||||
import io.element.android.appnav.room.joined.FakeJoinedRoomLoadedFlowNodeCallback
|
import io.element.android.appnav.room.joined.FakeJoinedRoomLoadedFlowNodeCallback
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||||
import io.element.android.features.forward.test.FakeForwardEntryPoint
|
import io.element.android.features.forward.test.FakeForwardEntryPoint
|
||||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||||
|
import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider
|
||||||
|
import io.element.android.features.messages.test.pinned.FakePinnedEventsTimelineProvider
|
||||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||||
import io.element.android.features.space.api.SpaceEntryPoint
|
import io.element.android.features.space.api.SpaceEntryPoint
|
||||||
import io.element.android.libraries.architecture.childNode
|
import io.element.android.libraries.architecture.childNode
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.FakeTimelineProvider
|
||||||
|
import io.element.android.services.analytics.api.watchers.AnalyticsSendMessageWatcher
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
|
import io.element.android.services.analytics.test.watchers.FakeAnalyticsSendMessageWatcher
|
||||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||||
|
|
@ -72,9 +79,20 @@ class JoinedRoomLoadedFlowNodeTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FakeRoomGraphFactory : RoomGraphFactory {
|
private class FakeRoomGraphFactory(
|
||||||
|
private val timelineProvider: FakeTimelineProvider = FakeTimelineProvider(),
|
||||||
|
private val pinnedEventsTimelineProvider: FakePinnedEventsTimelineProvider = FakePinnedEventsTimelineProvider(),
|
||||||
|
private val analyticsSendMessageWatcher: FakeAnalyticsSendMessageWatcher = FakeAnalyticsSendMessageWatcher(),
|
||||||
|
) : RoomGraphFactory {
|
||||||
override fun create(room: JoinedRoom): Any {
|
override fun create(room: JoinedRoom): Any {
|
||||||
return Unit
|
return object : TimelineBindings {
|
||||||
|
override val timelineProvider: TimelineProvider
|
||||||
|
get() = this@FakeRoomGraphFactory.timelineProvider
|
||||||
|
override val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider
|
||||||
|
get() = this@FakeRoomGraphFactory.pinnedEventsTimelineProvider
|
||||||
|
override val analyticsSendMessageWatcher: AnalyticsSendMessageWatcher
|
||||||
|
get() = this@FakeRoomGraphFactory.analyticsSendMessageWatcher
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ class ContributesNodeProcessor(
|
||||||
.addAnnotation(Binds::class)
|
.addAnnotation(Binds::class)
|
||||||
.addAnnotation(IntoMap::class)
|
.addAnnotation(IntoMap::class)
|
||||||
.addAnnotation(
|
.addAnnotation(
|
||||||
AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
|
AnnotationSpec.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
|
||||||
CLASS_PLACEHOLDER,
|
CLASS_PLACEHOLDER,
|
||||||
ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
||||||
).build()
|
).build()
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,6 @@ import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
|
||||||
import io.element.android.libraries.indicator.api.IndicatorService
|
import io.element.android.libraries.indicator.api.IndicatorService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
|
|
@ -48,7 +46,6 @@ class HomePresenter(
|
||||||
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
|
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
|
||||||
private val logoutPresenter: Presenter<DirectLogoutState>,
|
private val logoutPresenter: Presenter<DirectLogoutState>,
|
||||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||||
private val featureFlagService: FeatureFlagService,
|
|
||||||
private val sessionStore: SessionStore,
|
private val sessionStore: SessionStore,
|
||||||
private val announcementService: AnnouncementService,
|
private val announcementService: AnnouncementService,
|
||||||
) : Presenter<HomeState> {
|
) : Presenter<HomeState> {
|
||||||
|
|
@ -69,9 +66,6 @@ class HomePresenter(
|
||||||
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||||
val roomListState = roomListPresenter.present()
|
val roomListState = roomListPresenter.present()
|
||||||
val homeSpacesState = homeSpacesPresenter.present()
|
val homeSpacesState = homeSpacesPresenter.present()
|
||||||
val isSpaceFeatureEnabled by remember {
|
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
|
|
||||||
}.collectAsState(initial = false)
|
|
||||||
var currentHomeNavigationBarItemOrdinal by rememberSaveable { mutableIntStateOf(HomeNavigationBarItem.Chats.ordinal) }
|
var currentHomeNavigationBarItemOrdinal by rememberSaveable { mutableIntStateOf(HomeNavigationBarItem.Chats.ordinal) }
|
||||||
val currentHomeNavigationBarItem by remember {
|
val currentHomeNavigationBarItem by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
|
|
@ -117,7 +111,6 @@ class HomePresenter(
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
directLogoutState = directLogoutState,
|
directLogoutState = directLogoutState,
|
||||||
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
|
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,9 @@ data class HomeState(
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val canReportBug: Boolean,
|
val canReportBug: Boolean,
|
||||||
val directLogoutState: DirectLogoutState,
|
val directLogoutState: DirectLogoutState,
|
||||||
val isSpaceFeatureEnabled: Boolean,
|
|
||||||
val eventSink: (HomeEvents) -> Unit,
|
val eventSink: (HomeEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
|
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
|
||||||
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
|
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
|
||||||
val showNavigationBar = isSpaceFeatureEnabled && homeSpacesState.spaceRooms.isNotEmpty()
|
val showNavigationBar = homeSpacesState.spaceRooms.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ open class HomeStateProvider : PreviewParameterProvider<HomeState> {
|
||||||
aHomeState(hasNetworkConnection = false),
|
aHomeState(hasNetworkConnection = false),
|
||||||
aHomeState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
aHomeState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
||||||
aHomeState(
|
aHomeState(
|
||||||
isSpaceFeatureEnabled = true,
|
|
||||||
roomListState = aRoomListState(
|
roomListState = aRoomListState(
|
||||||
// Add more rooms to see the blur effect under the NavigationBar
|
// Add more rooms to see the blur effect under the NavigationBar
|
||||||
contentState = aRoomsContentState(
|
contentState = aRoomsContentState(
|
||||||
|
|
@ -42,7 +41,6 @@ open class HomeStateProvider : PreviewParameterProvider<HomeState> {
|
||||||
homeSpacesState = aHomeSpacesState(),
|
homeSpacesState = aHomeSpacesState(),
|
||||||
),
|
),
|
||||||
aHomeState(
|
aHomeState(
|
||||||
isSpaceFeatureEnabled = true,
|
|
||||||
currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces,
|
currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces,
|
||||||
),
|
),
|
||||||
) + RoomListStateProvider().values.map {
|
) + RoomListStateProvider().values.map {
|
||||||
|
|
@ -60,7 +58,6 @@ internal fun aHomeState(
|
||||||
roomListState: RoomListState = aRoomListState(),
|
roomListState: RoomListState = aRoomListState(),
|
||||||
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
||||||
canReportBug: Boolean = true,
|
canReportBug: Boolean = true,
|
||||||
isSpaceFeatureEnabled: Boolean = false,
|
|
||||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||||
eventSink: (HomeEvents) -> Unit = {}
|
eventSink: (HomeEvents) -> Unit = {}
|
||||||
) = HomeState(
|
) = HomeState(
|
||||||
|
|
@ -73,6 +70,5 @@ internal fun aHomeState(
|
||||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||||
roomListState = roomListState,
|
roomListState = roomListState,
|
||||||
homeSpacesState = homeSpacesState,
|
homeSpacesState = homeSpacesState,
|
||||||
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
|
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -179,14 +179,10 @@ private fun HomeScaffold(
|
||||||
displayFilters = state.displayRoomListFilters,
|
displayFilters = state.displayRoomListFilters,
|
||||||
filtersState = roomListState.filtersState,
|
filtersState = roomListState.filtersState,
|
||||||
canReportBug = state.canReportBug,
|
canReportBug = state.canReportBug,
|
||||||
modifier = if (state.isSpaceFeatureEnabled) {
|
modifier = Modifier.hazeEffect(
|
||||||
Modifier.hazeEffect(
|
state = hazeState,
|
||||||
state = hazeState,
|
style = HazeMaterials.thick(),
|
||||||
style = HazeMaterials.thick(),
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Modifier.background(ElementTheme.colors.bgCanvasDefault)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,5 @@ package io.element.android.features.home.impl.search
|
||||||
|
|
||||||
sealed interface RoomListSearchEvents {
|
sealed interface RoomListSearchEvents {
|
||||||
data object ToggleSearchVisibility : RoomListSearchEvents
|
data object ToggleSearchVisibility : RoomListSearchEvents
|
||||||
data class QueryChanged(val query: String) : RoomListSearchEvents
|
|
||||||
data object ClearQuery : RoomListSearchEvents
|
data object ClearQuery : RoomListSearchEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.search
|
package io.element.android.features.home.impl.search
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.clearText
|
||||||
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
|
@ -29,29 +31,24 @@ class RoomListSearchPresenter(
|
||||||
var isSearchActive by remember {
|
var isSearchActive by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
var searchQuery by remember {
|
val searchQuery = rememberTextFieldState()
|
||||||
mutableStateOf("")
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(isSearchActive) {
|
LaunchedEffect(isSearchActive) {
|
||||||
dataSource.setIsActive(isSearchActive)
|
dataSource.setIsActive(isSearchActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery.text) {
|
||||||
dataSource.setSearchQuery(searchQuery)
|
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvent(event: RoomListSearchEvents) {
|
fun handleEvent(event: RoomListSearchEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
RoomListSearchEvents.ClearQuery -> {
|
RoomListSearchEvents.ClearQuery -> {
|
||||||
searchQuery = ""
|
searchQuery.clearText()
|
||||||
}
|
|
||||||
is RoomListSearchEvents.QueryChanged -> {
|
|
||||||
searchQuery = event.query
|
|
||||||
}
|
}
|
||||||
RoomListSearchEvents.ToggleSearchVisibility -> {
|
RoomListSearchEvents.ToggleSearchVisibility -> {
|
||||||
isSearchActive = !isSearchActive
|
isSearchActive = !isSearchActive
|
||||||
searchQuery = ""
|
searchQuery.clearText()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,13 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.search
|
package io.element.android.features.home.impl.search
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class RoomListSearchState(
|
data class RoomListSearchState(
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val query: String,
|
val query: TextFieldState,
|
||||||
val results: ImmutableList<RoomListRoomSummary>,
|
val results: ImmutableList<RoomListRoomSummary>,
|
||||||
val eventSink: (RoomListSearchEvents) -> Unit
|
val eventSink: (RoomListSearchEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.search
|
package io.element.android.features.home.impl.search
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.roomlist.aRoomListRoomSummaryList
|
import io.element.android.features.home.impl.roomlist.aRoomListRoomSummaryList
|
||||||
|
|
@ -33,7 +34,7 @@ fun aRoomListSearchState(
|
||||||
eventSink: (RoomListSearchEvents) -> Unit = { },
|
eventSink: (RoomListSearchEvents) -> Unit = { },
|
||||||
) = RoomListSearchState(
|
) = RoomListSearchState(
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
query = query,
|
query = TextFieldState(initialText = query),
|
||||||
results = results,
|
results = results,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
|
@ -35,7 +32,6 @@ import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
|
@ -112,23 +108,14 @@ private fun RoomListSearchContent(
|
||||||
},
|
},
|
||||||
navigationIcon = { BackButton(onClick = ::onBackButtonClick) },
|
navigationIcon = { BackButton(onClick = ::onBackButtonClick) },
|
||||||
title = {
|
title = {
|
||||||
// TODO replace `state.query` with TextFieldState when it's available for M3 TextField
|
|
||||||
// The stateSaver will keep the selection state when returning to this UI
|
// The stateSaver will keep the selection state when returning to this UI
|
||||||
var value by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
|
||||||
mutableStateOf(TextFieldValue(state.query))
|
|
||||||
}
|
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
FilledTextField(
|
FilledTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
value = value,
|
state = state.query,
|
||||||
singleLine = true,
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
onValueChange = {
|
|
||||||
value = it
|
|
||||||
state.eventSink(RoomListSearchEvents.QueryChanged(it.text))
|
|
||||||
},
|
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
|
@ -138,20 +125,18 @@ private fun RoomListSearchContent(
|
||||||
disabledIndicatorColor = Color.Transparent,
|
disabledIndicatorColor = Color.Transparent,
|
||||||
errorIndicatorColor = Color.Transparent,
|
errorIndicatorColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
trailingIcon = {
|
trailingIcon = if (state.query.text.isNotEmpty()) {
|
||||||
if (value.text.isNotEmpty()) {
|
@Composable {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = { state.eventSink(RoomListSearchEvents.ClearQuery) }) {
|
||||||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
|
||||||
// Clear local state too
|
|
||||||
value = value.copy(text = "")
|
|
||||||
}) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Close(),
|
imageVector = CompoundIcons.Close(),
|
||||||
contentDescription = stringResource(CommonStrings.action_cancel)
|
contentDescription = stringResource(CommonStrings.action_cancel)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,13 @@
|
||||||
<string name="screen_roomlist_empty_title">"Još nema razgovora."</string>
|
<string name="screen_roomlist_empty_title">"Još nema razgovora."</string>
|
||||||
<string name="screen_roomlist_filter_favourites">"Favoriti"</string>
|
<string name="screen_roomlist_filter_favourites">"Favoriti"</string>
|
||||||
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Razgovor možete dodati u favorite u postavkama razgovora.
|
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Razgovor možete dodati u favorite u postavkama razgovora.
|
||||||
Zasad možete poništiti odabir filtera kako biste vidjeli ostale razgovore."</string>
|
Zasad možete poništiti odabir filtara kako biste vidjeli ostale razgovore."</string>
|
||||||
<string name="screen_roomlist_filter_favourites_empty_state_title">"Još nemate omiljenih razgovora"</string>
|
<string name="screen_roomlist_filter_favourites_empty_state_title">"Još nemate omiljenih razgovora"</string>
|
||||||
<string name="screen_roomlist_filter_invites">"Pozivnice"</string>
|
<string name="screen_roomlist_filter_invites">"Pozivnice"</string>
|
||||||
<string name="screen_roomlist_filter_invites_empty_state_title">"Nemate pozivnica na čekanju."</string>
|
<string name="screen_roomlist_filter_invites_empty_state_title">"Nemate pozivnica na čekanju."</string>
|
||||||
<string name="screen_roomlist_filter_low_priority">"Nizak prioritet"</string>
|
<string name="screen_roomlist_filter_low_priority">"Nizak prioritet"</string>
|
||||||
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Još nemate razgovora niskog prioriteta"</string>
|
<string name="screen_roomlist_filter_low_priority_empty_state_title">"Još nemate razgovora niskog prioriteta"</string>
|
||||||
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Možete poništiti odabir filtera kako biste vidjeli ostale razgovore"</string>
|
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Možete poništiti odabir filtara kako biste vidjeli ostale razgovore"</string>
|
||||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"Nemate razgovora za ovaj odabir"</string>
|
<string name="screen_roomlist_filter_mixed_empty_state_title">"Nemate razgovora za ovaj odabir"</string>
|
||||||
<string name="screen_roomlist_filter_people">"Osobe"</string>
|
<string name="screen_roomlist_filter_people">"Osobe"</string>
|
||||||
<string name="screen_roomlist_filter_people_empty_state_title">"Nemate još nijednu izravnu poruku"</string>
|
<string name="screen_roomlist_filter_people_empty_state_title">"Nemate još nijednu izravnu poruku"</string>
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,6 @@ import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
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.indicator.api.IndicatorService
|
import io.element.android.libraries.indicator.api.IndicatorService
|
||||||
import io.element.android.libraries.indicator.test.FakeIndicatorService
|
import io.element.android.libraries.indicator.test.FakeIndicatorService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
@ -35,7 +32,6 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
|
|
@ -54,8 +50,6 @@ class HomePresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
private val isSpaceEnabled = FeatureFlags.Space.defaultValue(aBuildMeta())
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - should start with no user and then load user with success`() = runTest {
|
fun `present - should start with no user and then load user with success`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
|
|
@ -79,7 +73,6 @@ class HomePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
if (isSpaceEnabled) skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(
|
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(
|
||||||
MatrixUser(A_USER_ID, null, null)
|
MatrixUser(A_USER_ID, null, null)
|
||||||
|
|
@ -91,8 +84,7 @@ class HomePresenterTest {
|
||||||
MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL)
|
MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL)
|
||||||
)
|
)
|
||||||
assertThat(withUserState.showAvatarIndicator).isFalse()
|
assertThat(withUserState.showAvatarIndicator).isFalse()
|
||||||
assertThat(withUserState.isSpaceFeatureEnabled).isEqualTo(isSpaceEnabled)
|
assertThat(withUserState.showNavigationBar).isTrue()
|
||||||
assertThat(withUserState.showNavigationBar).isEqualTo(isSpaceEnabled)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,23 +106,6 @@ class HomePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - space feature enabled`() = runTest {
|
|
||||||
val presenter = createHomePresenter(
|
|
||||||
featureFlagService = FakeFeatureFlagService(
|
|
||||||
initialState = mapOf(FeatureFlags.Space.key to true),
|
|
||||||
),
|
|
||||||
sessionStore = InMemorySessionStore(
|
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.isSpaceFeatureEnabled).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - show avatar indicator`() = runTest {
|
fun `present - show avatar indicator`() = runTest {
|
||||||
val indicatorService = FakeIndicatorService()
|
val indicatorService = FakeIndicatorService()
|
||||||
|
|
@ -143,7 +118,6 @@ class HomePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
if (isSpaceEnabled) skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.showAvatarIndicator).isFalse()
|
assertThat(initialState.showAvatarIndicator).isFalse()
|
||||||
indicatorService.setShowRoomListTopBarIndicator(true)
|
indicatorService.setShowRoomListTopBarIndicator(true)
|
||||||
|
|
@ -168,7 +142,6 @@ class HomePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
if (isSpaceEnabled) skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(MatrixUser(matrixClient.sessionId))
|
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(MatrixUser(matrixClient.sessionId))
|
||||||
// No new state is coming
|
// No new state is coming
|
||||||
|
|
@ -189,7 +162,6 @@ class HomePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
if (isSpaceEnabled) skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
||||||
initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
|
initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
|
||||||
|
|
@ -207,16 +179,12 @@ class HomePresenterTest {
|
||||||
sessionStore = InMemorySessionStore(
|
sessionStore = InMemorySessionStore(
|
||||||
updateUserProfileResult = { _, _, _ -> },
|
updateUserProfileResult = { _, _, _ -> },
|
||||||
),
|
),
|
||||||
featureFlagService = FakeFeatureFlagService(
|
|
||||||
initialState = mapOf(FeatureFlags.Space.key to true),
|
|
||||||
),
|
|
||||||
homeSpacesPresenter = homeSpacesPresenter,
|
homeSpacesPresenter = homeSpacesPresenter,
|
||||||
announcementService = FakeAnnouncementService(
|
announcementService = FakeAnnouncementService(
|
||||||
showAnnouncementResult = {},
|
showAnnouncementResult = {},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
||||||
assertThat(initialState.showNavigationBar).isTrue()
|
assertThat(initialState.showNavigationBar).isTrue()
|
||||||
|
|
@ -241,7 +209,6 @@ internal fun createHomePresenter(
|
||||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||||
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) },
|
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) },
|
||||||
indicatorService: IndicatorService = FakeIndicatorService(),
|
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
|
||||||
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
|
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
|
||||||
sessionStore: SessionStore = InMemorySessionStore(),
|
sessionStore: SessionStore = InMemorySessionStore(),
|
||||||
announcementService: AnnouncementService = FakeAnnouncementService(),
|
announcementService: AnnouncementService = FakeAnnouncementService(),
|
||||||
|
|
@ -250,11 +217,10 @@ internal fun createHomePresenter(
|
||||||
syncService = syncService,
|
syncService = syncService,
|
||||||
snackbarDispatcher = snackbarDispatcher,
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
indicatorService = indicatorService,
|
indicatorService = indicatorService,
|
||||||
logoutPresenter = { aDirectLogoutState() },
|
|
||||||
roomListPresenter = { aRoomListState() },
|
roomListPresenter = { aRoomListState() },
|
||||||
homeSpacesPresenter = homeSpacesPresenter,
|
homeSpacesPresenter = homeSpacesPresenter,
|
||||||
|
logoutPresenter = { aDirectLogoutState() },
|
||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
featureFlagService = featureFlagService,
|
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
announcementService = announcementService,
|
announcementService = announcementService,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class RoomListSearchPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.isSearchActive).isFalse()
|
assertThat(state.isSearchActive).isFalse()
|
||||||
assertThat(state.query).isEmpty()
|
assertThat(state.query.text.toString()).isEmpty()
|
||||||
assertThat(state.results).isEmpty()
|
assertThat(state.results).isEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,10 +72,10 @@ class RoomListSearchPresenterTest {
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
RoomListFilter.None
|
RoomListFilter.None
|
||||||
)
|
)
|
||||||
state.eventSink(RoomListSearchEvents.QueryChanged("Search"))
|
state.query.edit { append("Search") }
|
||||||
}
|
}
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.query).isEqualTo("Search")
|
assertThat(state.query.text).isEqualTo("Search")
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomListService.allRooms.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
|
|
@ -84,7 +84,7 @@ class RoomListSearchPresenterTest {
|
||||||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
state.eventSink(RoomListSearchEvents.ClearQuery)
|
||||||
}
|
}
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.query).isEmpty()
|
assertThat(state.query.text.toString()).isEmpty()
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomListService.allRooms.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,33 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_link_new_device_desktop_scanning_title">"Scannez le QR code"</string>
|
<string name="screen_link_new_device_desktop_scanning_title">"Scannez le QR code"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_step1">"Ouvrir %1$s sur un ordinateur"</string>
|
||||||
<string name="screen_link_new_device_desktop_step3">"Scanner le QR code avec cet appareil"</string>
|
<string name="screen_link_new_device_desktop_step3">"Scanner le QR code avec cet appareil"</string>
|
||||||
<string name="screen_link_new_device_desktop_submit">"Prêt à scanner"</string>
|
<string name="screen_link_new_device_desktop_submit">"Prêt à scanner"</string>
|
||||||
|
<string name="screen_link_new_device_desktop_title">"Ouvrir %1$s sur un ordinateur pour obtenir le code QR"</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_error_numbers_do_not_match">"Les nombres ne correspondent pas"</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_notice">"Saisissez le code à 2 chiffres"</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_subtitle">"Cela permettra de vérifier que la connexion à votre autre appareil est sécurisée."</string>
|
||||||
|
<string name="screen_link_new_device_enter_number_title">"Saisir le nombre affiché sur votre autre appareil"</string>
|
||||||
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Votre fournisseur de compte ne supporte pas %1$s."</string>
|
<string name="screen_link_new_device_error_app_not_supported_subtitle">"Votre fournisseur de compte ne supporte pas %1$s."</string>
|
||||||
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s n’est pas supporté"</string>
|
<string name="screen_link_new_device_error_app_not_supported_title">"%1$s n’est pas supporté"</string>
|
||||||
|
<string name="screen_link_new_device_error_not_supported_subtitle">"Votre fournisseur de compte ne prend pas en charge la connexion à un nouvel appareil à l’aide d’un code QR."</string>
|
||||||
<string name="screen_link_new_device_error_not_supported_title">"QR code non supporté"</string>
|
<string name="screen_link_new_device_error_not_supported_title">"QR code non supporté"</string>
|
||||||
<string name="screen_link_new_device_error_request_cancelled_subtitle">"La connexion a été annulée sur l’autre appareil."</string>
|
<string name="screen_link_new_device_error_request_cancelled_subtitle">"La connexion a été annulée sur l’autre appareil."</string>
|
||||||
<string name="screen_link_new_device_error_request_cancelled_title">"Demande de connexion annulée"</string>
|
<string name="screen_link_new_device_error_request_cancelled_title">"Demande de connexion annulée"</string>
|
||||||
<string name="screen_link_new_device_error_request_timeout_subtitle">"Connexion expirée. Veuillez essayer à nouveau."</string>
|
<string name="screen_link_new_device_error_request_timeout_subtitle">"Connexion expirée. Veuillez essayer à nouveau."</string>
|
||||||
<string name="screen_link_new_device_error_request_timeout_title">"La connexion a pris trop de temps."</string>
|
<string name="screen_link_new_device_error_request_timeout_title">"La connexion a pris trop de temps."</string>
|
||||||
|
<string name="screen_link_new_device_mobile_step1">"Ouvrez %1$s sur l’autre appareil"</string>
|
||||||
<string name="screen_link_new_device_mobile_step2">"Choisissez %1$s"</string>
|
<string name="screen_link_new_device_mobile_step2">"Choisissez %1$s"</string>
|
||||||
|
<string name="screen_link_new_device_mobile_step2_action">"« Se connecter avec un code QR »"</string>
|
||||||
|
<string name="screen_link_new_device_mobile_step3">"Scannez ce code QR avec l’autre appareil."</string>
|
||||||
|
<string name="screen_link_new_device_mobile_title">"Ouvrez %1$s sur l’autre appareil"</string>
|
||||||
|
<string name="screen_link_new_device_root_desktop_computer">"Ordinateur de bureau"</string>
|
||||||
|
<string name="screen_link_new_device_root_loading_qr_code">"Chargement du code QR…"</string>
|
||||||
|
<string name="screen_link_new_device_root_mobile_device">"Appareil mobile"</string>
|
||||||
|
<string name="screen_link_new_device_root_title">"Quel type d’appareil souhaitez-vous associer ?"</string>
|
||||||
|
<string name="screen_link_new_device_wrong_number_subtitle">"Veuillez réessayer et assurez-vous d’avoir saisi correctement le code à 2 chiffres. Si les chiffres ne correspondent toujours pas, veuillez contacter votre fournisseur de compte."</string>
|
||||||
|
<string name="screen_link_new_device_wrong_number_title">"Les nombres ne correspondent pas"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_description">"Aucune connexion sécurisée n’a pu être établie avec la nouvelle session. Vos sessions existantes sont toujours en sécurité et vous n’avez pas à vous en soucier."</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_description">"Aucune connexion sécurisée n’a pu être établie avec la nouvelle session. Vos sessions existantes sont toujours en sécurité et vous n’avez pas à vous en soucier."</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Et maintenant ?"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_header">"Et maintenant ?"</string>
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Essayez de vous connecter à nouveau à l’aide du QR code au cas où il s’agirait d’un problème réseau"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_list_item_1">"Essayez de vous connecter à nouveau à l’aide du QR code au cas où il s’agirait d’un problème réseau"</string>
|
||||||
|
|
@ -21,6 +38,8 @@
|
||||||
<string name="screen_qr_code_login_error_cancelled_title">"Demande de connexion annulée"</string>
|
<string name="screen_qr_code_login_error_cancelled_title">"Demande de connexion annulée"</string>
|
||||||
<string name="screen_qr_code_login_error_declined_subtitle">"La connexion a été refusée sur l’autre appareil."</string>
|
<string name="screen_qr_code_login_error_declined_subtitle">"La connexion a été refusée sur l’autre appareil."</string>
|
||||||
<string name="screen_qr_code_login_error_declined_title">"Connexion refusée"</string>
|
<string name="screen_qr_code_login_error_declined_title">"Connexion refusée"</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Vous n’avez rien d’autre à faire."</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Votre autre appareil est déjà connecté"</string>
|
||||||
<string name="screen_qr_code_login_error_expired_subtitle">"Connexion expirée. Veuillez essayer à nouveau."</string>
|
<string name="screen_qr_code_login_error_expired_subtitle">"Connexion expirée. Veuillez essayer à nouveau."</string>
|
||||||
<string name="screen_qr_code_login_error_expired_title">"La connexion a pris trop de temps."</string>
|
<string name="screen_qr_code_login_error_expired_title">"La connexion a pris trop de temps."</string>
|
||||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Votre autre appareil ne supporte pas la connexion à %s avec un QR code. Essayer de vous connecter manuellement, ou scanner le QR code avec un autre appareil."</string>
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Votre autre appareil ne supporte pas la connexion à %s avec un QR code. Essayer de vous connecter manuellement, ou scanner le QR code avec un autre appareil."</string>
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@
|
||||||
<string name="screen_qr_code_login_error_cancelled_title">"Demande de connexion annulée"</string>
|
<string name="screen_qr_code_login_error_cancelled_title">"Demande de connexion annulée"</string>
|
||||||
<string name="screen_qr_code_login_error_declined_subtitle">"La connexion a été refusée sur l’autre appareil."</string>
|
<string name="screen_qr_code_login_error_declined_subtitle">"La connexion a été refusée sur l’autre appareil."</string>
|
||||||
<string name="screen_qr_code_login_error_declined_title">"Connexion refusée"</string>
|
<string name="screen_qr_code_login_error_declined_title">"Connexion refusée"</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_subtitle">"Vous n’avez rien d’autre à faire."</string>
|
||||||
|
<string name="screen_qr_code_login_error_device_already_signed_in_title">"Votre autre appareil est déjà connecté"</string>
|
||||||
<string name="screen_qr_code_login_error_expired_subtitle">"Connexion expirée. Veuillez essayer à nouveau."</string>
|
<string name="screen_qr_code_login_error_expired_subtitle">"Connexion expirée. Veuillez essayer à nouveau."</string>
|
||||||
<string name="screen_qr_code_login_error_expired_title">"La connexion a pris trop de temps."</string>
|
<string name="screen_qr_code_login_error_expired_title">"La connexion a pris trop de temps."</string>
|
||||||
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Votre autre appareil ne supporte pas la connexion à %s avec un QR code. Essayer de vous connecter manuellement, ou scanner le QR code avec un autre appareil."</string>
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Votre autre appareil ne supporte pas la connexion à %s avec un QR code. Essayer de vous connecter manuellement, ou scanner le QR code avec un autre appareil."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_account_provider_change">"Promijeni davatelja računa"</string>
|
<string name="screen_account_provider_change">"Promijeni davatelja usluga računa"</string>
|
||||||
<string name="screen_account_provider_form_hint">"Adresa matičnog poslužitelja"</string>
|
<string name="screen_account_provider_form_hint">"Adresa matičnog poslužitelja"</string>
|
||||||
<string name="screen_account_provider_form_notice">"Unesite pojam za pretraživanje ili adresu domene."</string>
|
<string name="screen_account_provider_form_notice">"Unesite pojam za pretraživanje ili adresu domene."</string>
|
||||||
<string name="screen_account_provider_form_subtitle">"Potražite tvrtku, zajednicu ili privatni poslužitelj."</string>
|
<string name="screen_account_provider_form_subtitle">"Potražite tvrtku, zajednicu ili privatni poslužitelj."</string>
|
||||||
<string name="screen_account_provider_form_title">"Pronađite davatelja računa"</string>
|
<string name="screen_account_provider_form_title">"Pronađite davatelja usluga računa"</string>
|
||||||
<string name="screen_account_provider_signin_subtitle">"Ovdje će se čuvati vaši razgovori – baš kao što biste koristili davatelja usluga e-pošte za čuvanje svojih e-poruka."</string>
|
<string name="screen_account_provider_signin_subtitle">"Ovdje će se čuvati vaši razgovori – baš kao što biste koristili davatelja usluga e-pošte za čuvanje svojih e-poruka."</string>
|
||||||
<string name="screen_account_provider_signin_title">"Prijavit ćete se na %s"</string>
|
<string name="screen_account_provider_signin_title">"Prijavit ćete se na %s"</string>
|
||||||
<string name="screen_account_provider_signup_subtitle">"Ovdje će se čuvati vaši razgovori – baš kao što biste koristili davatelja usluga e-pošte za čuvanje svojih e-poruka."</string>
|
<string name="screen_account_provider_signup_subtitle">"Ovdje će se čuvati vaši razgovori – baš kao što biste koristili davatelja usluga e-pošte za čuvanje svojih e-poruka."</string>
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org velik je, besplatni poslužitelj na javnoj Matrixovoj mreži koji pruža sigurnu, decentraliziranu komunikaciju, a kojim upravlja zaklada Matrix.org."</string>
|
<string name="screen_change_account_provider_matrix_org_subtitle">"Matrix.org velik je, besplatni poslužitelj na javnoj Matrixovoj mreži koji pruža sigurnu, decentraliziranu komunikaciju, a kojim upravlja zaklada Matrix.org."</string>
|
||||||
<string name="screen_change_account_provider_other">"Ostalo"</string>
|
<string name="screen_change_account_provider_other">"Ostalo"</string>
|
||||||
<string name="screen_change_account_provider_subtitle">"Koristite drugog davatelja računa, kao što je vlastiti privatni poslužitelj ili poslovni račun."</string>
|
<string name="screen_change_account_provider_subtitle">"Koristite drugog davatelja računa, kao što je vlastiti privatni poslužitelj ili poslovni račun."</string>
|
||||||
<string name="screen_change_account_provider_title">"Promijeni davatelja računa"</string>
|
<string name="screen_change_account_provider_title">"Promijeni davatelja usluga računa"</string>
|
||||||
<string name="screen_change_server_error_element_pro_required_action_android">"Google Play"</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">"Potrebna je aplikacija Element Pro na %1$s. Molimo vas da je preuzmete iz trgovine."</string>
|
<string name="screen_change_server_error_element_pro_required_message">"Potrebna je aplikacija Element Pro na %1$s. Molimo vas da je preuzmete iz trgovine."</string>
|
||||||
<string name="screen_change_server_error_element_pro_required_title">"Potreban je Element Pro"</string>
|
<string name="screen_change_server_error_element_pro_required_title">"Potreban je Element Pro"</string>
|
||||||
|
|
@ -90,7 +90,7 @@ Pokušajte se prijaviti ručno ili skenirajte QR kod drugim uređajem."</string>
|
||||||
<string name="screen_qr_code_login_verify_code_loading">"Čekanje na vaš drugi uređaj"</string>
|
<string name="screen_qr_code_login_verify_code_loading">"Čekanje na vaš drugi uređaj"</string>
|
||||||
<string name="screen_qr_code_login_verify_code_subtitle">"Davatelj usluge računa može zatražiti sljedeći kod za potvrdu prijave."</string>
|
<string name="screen_qr_code_login_verify_code_subtitle">"Davatelj usluge računa može zatražiti sljedeći kod za potvrdu prijave."</string>
|
||||||
<string name="screen_qr_code_login_verify_code_title">"Vaš verifikacijski kod"</string>
|
<string name="screen_qr_code_login_verify_code_title">"Vaš verifikacijski kod"</string>
|
||||||
<string name="screen_server_confirmation_change_server">"Promijeni davatelja računa"</string>
|
<string name="screen_server_confirmation_change_server">"Promijeni davatelja usluga računa"</string>
|
||||||
<string name="screen_server_confirmation_message_login_element_dot_io">"Privatni poslužitelj za zaposlenike aplikacije Element."</string>
|
<string name="screen_server_confirmation_message_login_element_dot_io">"Privatni poslužitelj za zaposlenike aplikacije Element."</string>
|
||||||
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix je otvorena mreža za sigurnu, decentraliziranu komunikaciju."</string>
|
<string name="screen_server_confirmation_message_login_matrix_dot_org">"Matrix je otvorena mreža za sigurnu, decentraliziranu komunikaciju."</string>
|
||||||
<string name="screen_server_confirmation_message_register">"Ovdje će se čuvati vaši razgovori – baš kao što biste koristili davatelja usluga e-pošte za čuvanje svojih e-poruka."</string>
|
<string name="screen_server_confirmation_message_register">"Ovdje će se čuvati vaši razgovori – baš kao što biste koristili davatelja usluga e-pošte za čuvanje svojih e-poruka."</string>
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ private fun ImageOptimizationSelector(state: MediaOptimizationSelectorState) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.weight(1f).align(Alignment.CenterVertically),
|
modifier = Modifier.weight(1f).align(Alignment.CenterVertically),
|
||||||
text = stringResource(R.string.screen_media_upload_preview_optimize_image_quality_title),
|
text = stringResource(R.string.screen_media_upload_preview_optimize_image_quality_title),
|
||||||
style = ElementTheme.materialTypography.bodyLarge,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
)
|
)
|
||||||
Switch(
|
Switch(
|
||||||
modifier = Modifier.height(32.dp),
|
modifier = Modifier.height(32.dp),
|
||||||
|
|
@ -337,7 +337,7 @@ private fun VideoQualitySelectorDialog(
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
text = preset.subtitle(),
|
text = preset.subtitle(),
|
||||||
style = ElementTheme.materialTypography.bodyMedium,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
color = ElementTheme.colors.textSecondary,
|
color = ElementTheme.colors.textSecondary,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.DisplayFirstTimelineItems
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.DisplayFirstTimelineItems
|
||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationTapOpensTimeline
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationToMessage
|
||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||||
|
|
@ -205,7 +205,7 @@ class TimelinePresenter(
|
||||||
}.start()
|
}.start()
|
||||||
is TimelineEvents.OnFocusEventRender -> {
|
is TimelineEvents.OnFocusEventRender -> {
|
||||||
// If there was a pending 'notification tap opens timeline' transaction, finish it now we're focused in the required event
|
// If there was a pending 'notification tap opens timeline' transaction, finish it now we're focused in the required event
|
||||||
analyticsService.finishLongRunningTransaction(NotificationTapOpensTimeline)
|
analyticsService.finishLongRunningTransaction(NotificationToMessage)
|
||||||
|
|
||||||
focusRequestState.value = focusRequestState.value.onFocusEventRender()
|
focusRequestState.value = focusRequestState.value.onFocusEventRender()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components.event
|
package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -42,6 +43,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.PlaybackSpeedButton
|
||||||
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -51,7 +53,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent
|
||||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -64,26 +66,26 @@ fun TimelineItemVoiceView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun playPause() {
|
fun playPause() {
|
||||||
state.eventSink(VoiceMessageEvents.PlayPause)
|
state.eventSink(VoiceMessageEvent.PlayPause)
|
||||||
}
|
}
|
||||||
|
|
||||||
val a11y = stringResource(CommonStrings.common_voice_message)
|
val a11y = stringResource(CommonStrings.common_voice_message)
|
||||||
val a11yActionLabel = stringResource(
|
val a11yActionLabel = stringResource(
|
||||||
when (state.button) {
|
when (state.buttonType) {
|
||||||
VoiceMessageState.Button.Play -> CommonStrings.a11y_play
|
VoiceMessageState.ButtonType.Play -> CommonStrings.a11y_play
|
||||||
VoiceMessageState.Button.Pause -> CommonStrings.a11y_pause
|
VoiceMessageState.ButtonType.Pause -> CommonStrings.a11y_pause
|
||||||
VoiceMessageState.Button.Downloading -> CommonStrings.common_downloading
|
VoiceMessageState.ButtonType.Downloading -> CommonStrings.common_downloading
|
||||||
VoiceMessageState.Button.Retry -> CommonStrings.action_retry
|
VoiceMessageState.ButtonType.Retry -> CommonStrings.action_retry
|
||||||
VoiceMessageState.Button.Disabled -> CommonStrings.error_unknown
|
VoiceMessageState.ButtonType.Disabled -> CommonStrings.error_unknown
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clearAndSetSemantics {
|
.clearAndSetSemantics {
|
||||||
contentDescription = a11y
|
contentDescription = a11y
|
||||||
if (state.button == VoiceMessageState.Button.Disabled) {
|
if (state.buttonType == VoiceMessageState.ButtonType.Disabled) {
|
||||||
disabled()
|
disabled()
|
||||||
} else if (state.button in listOf(VoiceMessageState.Button.Play, VoiceMessageState.Button.Pause)) {
|
} else if (state.buttonType in listOf(VoiceMessageState.ButtonType.Play, VoiceMessageState.ButtonType.Pause)) {
|
||||||
onClick(label = a11yActionLabel) {
|
onClick(label = a11yActionLabel) {
|
||||||
playPause()
|
playPause()
|
||||||
true
|
true
|
||||||
|
|
@ -101,30 +103,41 @@ fun TimelineItemVoiceView(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (!isTalkbackActive()) {
|
if (!isTalkbackActive()) {
|
||||||
when (state.button) {
|
when (state.buttonType) {
|
||||||
VoiceMessageState.Button.Play -> PlayButton(onClick = ::playPause)
|
VoiceMessageState.ButtonType.Play -> PlayButton(onClick = ::playPause)
|
||||||
VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause)
|
VoiceMessageState.ButtonType.Pause -> PauseButton(onClick = ::playPause)
|
||||||
VoiceMessageState.Button.Downloading -> ProgressButton()
|
VoiceMessageState.ButtonType.Downloading -> ProgressButton()
|
||||||
VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause)
|
VoiceMessageState.ButtonType.Retry -> RetryButton(onClick = ::playPause)
|
||||||
VoiceMessageState.Button.Disabled -> PlayButton(onClick = {}, enabled = false)
|
VoiceMessageState.ButtonType.Disabled -> PlayButton(onClick = {}, enabled = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(
|
Column(
|
||||||
text = state.time,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
color = ElementTheme.colors.textSecondary,
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
style = ElementTheme.typography.fontBodySmMedium,
|
) {
|
||||||
maxLines = 1,
|
PlaybackSpeedButton(
|
||||||
overflow = TextOverflow.Ellipsis,
|
speed = state.playbackSpeed,
|
||||||
)
|
onClick = { state.eventSink(VoiceMessageEvent.ChangePlaybackSpeed) },
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = state.time,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
style = ElementTheme.typography.fontBodySmMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
WaveformPlaybackView(
|
WaveformPlaybackView(
|
||||||
showCursor = state.showCursor,
|
showCursor = state.showCursor,
|
||||||
playbackProgress = state.progress,
|
playbackProgress = state.progress,
|
||||||
waveform = content.waveform,
|
waveform = content.waveform,
|
||||||
modifier = Modifier.height(34.dp),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(34.dp),
|
||||||
seekEnabled = !isTalkbackActive(),
|
seekEnabled = !isTalkbackActive(),
|
||||||
onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) },
|
onSeek = { state.eventSink(VoiceMessageEvent.Seek(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ fun GroupHeaderView(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
// Ignore isHighlighted for now, we need a design decision on it.
|
// Ignore isHighlighted for now, we need a design decision on it.
|
||||||
val backgroundColor = Color.Companion.Transparent
|
val backgroundColor = Color.Transparent
|
||||||
val shape = RoundedCornerShape(CORNER_RADIUS)
|
val shape = RoundedCornerShape(CORNER_RADIUS)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
package io.element.android.features.messages.impl.timeline.factories.event
|
package io.element.android.features.messages.impl.timeline.factories.event
|
||||||
|
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import androidx.core.text.buildSpannedString
|
|
||||||
import androidx.core.text.getSpans
|
import androidx.core.text.getSpans
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
|
|
@ -35,11 +34,9 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
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.EmoteMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
|
||||||
|
|
@ -50,6 +47,7 @@ import io.element.android.libraries.matrix.ui.messages.toHtmlDocument
|
||||||
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
|
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -60,7 +58,7 @@ class TimelineItemContentMessageFactory(
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
private val textPillificationHelper: TextPillificationHelper,
|
private val textPillificationHelper: TextPillificationHelper,
|
||||||
) {
|
) {
|
||||||
suspend fun create(
|
fun create(
|
||||||
content: MessageContent,
|
content: MessageContent,
|
||||||
senderDisambiguatedDisplayName: String,
|
senderDisambiguatedDisplayName: String,
|
||||||
eventId: EventId?,
|
eventId: EventId?,
|
||||||
|
|
@ -68,26 +66,29 @@ class TimelineItemContentMessageFactory(
|
||||||
return when (val messageType = content.type) {
|
return when (val messageType = content.type) {
|
||||||
is EmoteMessageType -> {
|
is EmoteMessageType -> {
|
||||||
val emoteBody = "* $senderDisambiguatedDisplayName ${messageType.body.trimEnd()}"
|
val emoteBody = "* $senderDisambiguatedDisplayName ${messageType.body.trimEnd()}"
|
||||||
val formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisambiguatedDisplayName") ?: textPillificationHelper.pillify(
|
val dom = messageType.formatted?.toHtmlDocument(
|
||||||
emoteBody
|
permalinkParser = permalinkParser,
|
||||||
).safeLinkify()
|
prefix = "* $senderDisambiguatedDisplayName",
|
||||||
|
)
|
||||||
|
val formattedBody = dom?.let(::parseHtml)
|
||||||
|
?: textPillificationHelper.pillify(emoteBody).safeLinkify()
|
||||||
TimelineItemEmoteContent(
|
TimelineItemEmoteContent(
|
||||||
body = emoteBody,
|
body = emoteBody,
|
||||||
htmlDocument = messageType.formatted?.toHtmlDocument(
|
htmlDocument = dom,
|
||||||
permalinkParser = permalinkParser,
|
|
||||||
prefix = "* $senderDisambiguatedDisplayName",
|
|
||||||
),
|
|
||||||
formattedBody = formattedBody,
|
formattedBody = formattedBody,
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ImageMessageType -> {
|
is ImageMessageType -> {
|
||||||
|
val dom = messageType.formattedCaption?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||||
|
val formattedCaption = dom?.let(::parseHtml)
|
||||||
|
?: messageType.caption?.withLinks()
|
||||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||||
TimelineItemImageContent(
|
TimelineItemImageContent(
|
||||||
filename = messageType.filename,
|
filename = messageType.filename,
|
||||||
fileSize = messageType.info?.size ?: 0,
|
fileSize = messageType.info?.size ?: 0,
|
||||||
caption = messageType.caption?.trimEnd(),
|
caption = messageType.caption?.trimEnd(),
|
||||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
formattedCaption = formattedCaption,
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
|
|
@ -103,12 +104,15 @@ class TimelineItemContentMessageFactory(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is StickerMessageType -> {
|
is StickerMessageType -> {
|
||||||
|
val dom = messageType.formattedCaption?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||||
|
val formattedCaption = dom?.let(::parseHtml)
|
||||||
|
?: messageType.caption?.withLinks()
|
||||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||||
TimelineItemStickerContent(
|
TimelineItemStickerContent(
|
||||||
filename = messageType.filename,
|
filename = messageType.filename,
|
||||||
fileSize = messageType.info?.size ?: 0,
|
fileSize = messageType.info?.size ?: 0,
|
||||||
caption = messageType.caption?.trimEnd(),
|
caption = messageType.caption?.trimEnd(),
|
||||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
formattedCaption = formattedCaption,
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
|
|
@ -140,12 +144,15 @@ class TimelineItemContentMessageFactory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is VideoMessageType -> {
|
is VideoMessageType -> {
|
||||||
|
val dom = messageType.formattedCaption?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||||
|
val formattedCaption = dom?.let(::parseHtml)
|
||||||
|
?: messageType.caption?.withLinks()
|
||||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||||
TimelineItemVideoContent(
|
TimelineItemVideoContent(
|
||||||
filename = messageType.filename,
|
filename = messageType.filename,
|
||||||
fileSize = messageType.info?.size ?: 0,
|
fileSize = messageType.info?.size ?: 0,
|
||||||
caption = messageType.caption?.trimEnd(),
|
caption = messageType.caption?.trimEnd(),
|
||||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
formattedCaption = formattedCaption,
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
|
|
@ -162,11 +169,14 @@ class TimelineItemContentMessageFactory(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AudioMessageType -> {
|
is AudioMessageType -> {
|
||||||
|
val dom = messageType.formattedCaption?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||||
|
val formattedCaption = dom?.let(::parseHtml)
|
||||||
|
?: messageType.caption?.withLinks()
|
||||||
TimelineItemAudioContent(
|
TimelineItemAudioContent(
|
||||||
filename = messageType.filename,
|
filename = messageType.filename,
|
||||||
fileSize = messageType.info?.size ?: 0,
|
fileSize = messageType.info?.size ?: 0,
|
||||||
caption = messageType.caption?.trimEnd(),
|
caption = messageType.caption?.trimEnd(),
|
||||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
formattedCaption = formattedCaption,
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||||
|
|
@ -176,12 +186,15 @@ class TimelineItemContentMessageFactory(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is VoiceMessageType -> {
|
is VoiceMessageType -> {
|
||||||
|
val dom = messageType.formattedCaption?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||||
|
val formattedCaption = dom?.let(::parseHtml)
|
||||||
|
?: messageType.caption?.withLinks()
|
||||||
TimelineItemVoiceContent(
|
TimelineItemVoiceContent(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
filename = messageType.filename,
|
filename = messageType.filename,
|
||||||
fileSize = messageType.info?.size ?: 0,
|
fileSize = messageType.info?.size ?: 0,
|
||||||
caption = messageType.caption?.trimEnd(),
|
caption = messageType.caption?.trimEnd(),
|
||||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
formattedCaption = formattedCaption,
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||||
|
|
@ -192,12 +205,15 @@ class TimelineItemContentMessageFactory(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is FileMessageType -> {
|
is FileMessageType -> {
|
||||||
|
val dom = messageType.formattedCaption?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||||
|
val formattedCaption = dom?.let(::parseHtml)
|
||||||
|
?: messageType.caption?.withLinks()
|
||||||
val fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
|
val fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
|
||||||
TimelineItemFileContent(
|
TimelineItemFileContent(
|
||||||
filename = messageType.filename,
|
filename = messageType.filename,
|
||||||
fileSize = messageType.info?.size ?: 0,
|
fileSize = messageType.info?.size ?: 0,
|
||||||
caption = messageType.caption?.trimEnd(),
|
caption = messageType.caption?.trimEnd(),
|
||||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
formattedCaption = formattedCaption,
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
|
|
@ -208,9 +224,9 @@ class TimelineItemContentMessageFactory(
|
||||||
}
|
}
|
||||||
is NoticeMessageType -> {
|
is NoticeMessageType -> {
|
||||||
val body = messageType.body.trimEnd()
|
val body = messageType.body.trimEnd()
|
||||||
val formattedBody = parseHtml(messageType.formatted) ?: textPillificationHelper.pillify(
|
val dom = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||||
body
|
val formattedBody = dom?.let(::parseHtml)
|
||||||
).safeLinkify()
|
?: textPillificationHelper.pillify(body).safeLinkify()
|
||||||
val htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
|
val htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||||
TimelineItemNoticeContent(
|
TimelineItemNoticeContent(
|
||||||
body = body,
|
body = body,
|
||||||
|
|
@ -221,12 +237,13 @@ class TimelineItemContentMessageFactory(
|
||||||
}
|
}
|
||||||
is TextMessageType -> {
|
is TextMessageType -> {
|
||||||
val body = messageType.body.trimEnd()
|
val body = messageType.body.trimEnd()
|
||||||
val formattedBody = parseHtml(messageType.formatted) ?: textPillificationHelper.pillify(
|
val dom = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||||
body
|
val formattedBody = dom?.let(::parseHtml)
|
||||||
).safeLinkify()
|
?: textPillificationHelper.pillify(body).safeLinkify()
|
||||||
|
val htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||||
TimelineItemTextContent(
|
TimelineItemTextContent(
|
||||||
body = body,
|
body = body,
|
||||||
htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser),
|
htmlDocument = htmlDocument,
|
||||||
formattedBody = formattedBody,
|
formattedBody = formattedBody,
|
||||||
isEdited = content.isEdited,
|
isEdited = content.isEdited,
|
||||||
)
|
)
|
||||||
|
|
@ -253,21 +270,11 @@ class TimelineItemContentMessageFactory(
|
||||||
return result?.takeIf { it.isFinite() }
|
return result?.takeIf { it.isFinite() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseHtml(formattedBody: FormattedBody?, prefix: String? = null): CharSequence? {
|
private fun parseHtml(document: Document): CharSequence? {
|
||||||
if (formattedBody == null || formattedBody.format != MessageFormat.HTML) return null
|
return htmlConverterProvider.provide()
|
||||||
val result = htmlConverterProvider.provide()
|
.fromDocumentToSpans(document)
|
||||||
.fromHtmlToSpans(formattedBody.body.trimEnd())
|
|
||||||
.let { textPillificationHelper.pillify(it) }
|
.let { textPillificationHelper.pillify(it) }
|
||||||
.safeLinkify()
|
.safeLinkify()
|
||||||
return if (prefix != null) {
|
|
||||||
buildSpannedString {
|
|
||||||
append(prefix)
|
|
||||||
append(" ")
|
|
||||||
append(result)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractor
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
import org.junit.Assert.fail
|
import org.junit.Assert.fail
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
@ -187,7 +188,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
}
|
}
|
||||||
}.toSpannable()
|
}.toSpannable()
|
||||||
val sut = createTimelineItemContentMessageFactory(
|
val sut = createTimelineItemContentMessageFactory(
|
||||||
htmlConverterTransform = { expected }
|
domConverterTransform = { expected }
|
||||||
)
|
)
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
content = createMessageContent(
|
content = createMessageContent(
|
||||||
|
|
@ -679,7 +680,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
}
|
}
|
||||||
}.toSpannable()
|
}.toSpannable()
|
||||||
val sut = createTimelineItemContentMessageFactory(
|
val sut = createTimelineItemContentMessageFactory(
|
||||||
htmlConverterTransform = { expectedSpanned },
|
domConverterTransform = { expectedSpanned },
|
||||||
permalinkParser = FakePermalinkParser { PermalinkData.FallbackLink(Uri.EMPTY) }
|
permalinkParser = FakePermalinkParser { PermalinkData.FallbackLink(Uri.EMPTY) }
|
||||||
)
|
)
|
||||||
val result = sut.create(
|
val result = sut.create(
|
||||||
|
|
@ -765,11 +766,12 @@ class TimelineItemContentMessageFactoryTest {
|
||||||
|
|
||||||
private fun createTimelineItemContentMessageFactory(
|
private fun createTimelineItemContentMessageFactory(
|
||||||
htmlConverterTransform: (String) -> CharSequence = { it },
|
htmlConverterTransform: (String) -> CharSequence = { it },
|
||||||
|
domConverterTransform: (Document) -> CharSequence = { it.body().html() },
|
||||||
permalinkParser: FakePermalinkParser = FakePermalinkParser(),
|
permalinkParser: FakePermalinkParser = FakePermalinkParser(),
|
||||||
) = TimelineItemContentMessageFactory(
|
) = TimelineItemContentMessageFactory(
|
||||||
fileSizeFormatter = FakeFileSizeFormatter(),
|
fileSizeFormatter = FakeFileSizeFormatter(),
|
||||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||||
htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform),
|
htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform, domConverterTransform),
|
||||||
permalinkParser = permalinkParser,
|
permalinkParser = permalinkParser,
|
||||||
textPillificationHelper = FakeTextPillificationHelper(),
|
textPillificationHelper = FakeTextPillificationHelper(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations 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.messages.test.pinned
|
||||||
|
|
||||||
|
import io.element.android.features.messages.api.pinned.PinnedEventsTimelineProvider
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.matrix.test.timeline.FakeTimelineProvider
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
class FakePinnedEventsTimelineProvider(
|
||||||
|
private val fakeTimelineProvider: FakeTimelineProvider = FakeTimelineProvider(),
|
||||||
|
) : PinnedEventsTimelineProvider {
|
||||||
|
override fun activeTimelineFlow(): StateFlow<Timeline?> = fakeTimelineProvider.activeTimelineFlow()
|
||||||
|
}
|
||||||
|
|
@ -11,9 +11,11 @@ package io.element.android.features.messages.test.timeline
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
||||||
import io.element.android.wysiwyg.utils.HtmlConverter
|
import io.element.android.wysiwyg.utils.HtmlConverter
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
|
||||||
class FakeHtmlConverterProvider(
|
class FakeHtmlConverterProvider(
|
||||||
private val transform: (String) -> CharSequence = { it },
|
private val transform: (String) -> CharSequence = { it },
|
||||||
|
private val transformDom: (Document) -> CharSequence = { it.html() },
|
||||||
) : HtmlConverterProvider {
|
) : HtmlConverterProvider {
|
||||||
@Composable
|
@Composable
|
||||||
override fun Update() = Unit
|
override fun Update() = Unit
|
||||||
|
|
@ -23,6 +25,10 @@ class FakeHtmlConverterProvider(
|
||||||
override fun fromHtmlToSpans(html: String): CharSequence {
|
override fun fromHtmlToSpans(html: String): CharSequence {
|
||||||
return transform(html)
|
return transform(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun fromDocumentToSpans(dom: Document): CharSequence {
|
||||||
|
return transformDom(dom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ private fun VideoQualitySelectorDialog(
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Text(
|
||||||
text = subtitle,
|
text = subtitle,
|
||||||
style = ElementTheme.materialTypography.bodyMedium,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
color = ElementTheme.colors.textSecondary,
|
color = ElementTheme.colors.textSecondary,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -404,7 +404,7 @@ private fun RoomHeaderSection(
|
||||||
}.toImmutableList(),
|
}.toImmutableList(),
|
||||||
isTombstoned = isTombstoned,
|
isTombstoned = isTombstoned,
|
||||||
),
|
),
|
||||||
contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_room_avatar) },
|
contentDescription = stringResource(CommonStrings.a11y_room_avatar),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = avatarUrl != null,
|
enabled = avatarUrl != null,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ data class RoomMemberListState(
|
||||||
val moderationState: RoomMemberModerationState,
|
val moderationState: RoomMemberModerationState,
|
||||||
val eventSink: (RoomMemberListEvents) -> Unit,
|
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val showBannedSection: Boolean = moderationState.permissions.hasAny && roomMembers.dataOrNull()?.banned?.isNotEmpty() == true
|
val showBannedSection: Boolean = moderationState.permissions.canBan && roomMembers.dataOrNull()?.banned?.isNotEmpty() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SelectedSection {
|
enum class SelectedSection {
|
||||||
|
|
|
||||||
|
|
@ -129,8 +129,10 @@
|
||||||
<string name="screen_room_roles_and_permissions_room_details">"Détails du salon"</string>
|
<string name="screen_room_roles_and_permissions_room_details">"Détails du salon"</string>
|
||||||
<string name="screen_room_roles_and_permissions_title">"Rôles & autorisations"</string>
|
<string name="screen_room_roles_and_permissions_title">"Rôles & autorisations"</string>
|
||||||
<string name="screen_security_and_privacy_add_room_address_action">"Ajouter une adresse"</string>
|
<string name="screen_security_and_privacy_add_room_address_action">"Ajouter une adresse"</string>
|
||||||
|
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Toute personne se trouvant dans un espace autorisé peut participer, mais toutes les autres doivent demander l’accès."</string>
|
||||||
<string name="screen_security_and_privacy_ask_to_join_option_description">"Tout le monde doit demander un accès."</string>
|
<string name="screen_security_and_privacy_ask_to_join_option_description">"Tout le monde doit demander un accès."</string>
|
||||||
<string name="screen_security_and_privacy_ask_to_join_option_title">"Demander à rejoindre"</string>
|
<string name="screen_security_and_privacy_ask_to_join_option_title">"Demander à rejoindre"</string>
|
||||||
|
<string name="screen_security_and_privacy_ask_to_join_single_space_members_option_description">"Tout membre de %1$s peut rejoindre l’espace, mais les autres doivent demander un accès."</string>
|
||||||
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Oui, activer le chiffrement"</string>
|
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Oui, activer le chiffrement"</string>
|
||||||
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Une fois activé, le chiffrement d’un salon ne peut pas être désactivé. L’historique des messages ne sera visible que pour les membres depuis qu’ils ont été invités ou depuis qu’ils ont rejoint le salon.
|
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Une fois activé, le chiffrement d’un salon ne peut pas être désactivé. L’historique des messages ne sera visible que pour les membres depuis qu’ils ont été invités ou depuis qu’ils ont rejoint le salon.
|
||||||
Personne d’autre que les membres du salon ne pourra lire les messages. Cela peut empêcher les bots et les bridges de fonctionner correctement.
|
Personne d’autre que les membres du salon ne pourra lire les messages. Cela peut empêcher les bots et les bridges de fonctionner correctement.
|
||||||
|
|
@ -155,10 +157,11 @@ Nous ne recommandons pas d’activer le chiffrement pour les salons que tout le
|
||||||
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Autoriser le salon à apparaître dans les résultats de recherche dans le répertoire %1$s des salons publics"</string>
|
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Autoriser le salon à apparaître dans les résultats de recherche dans le répertoire %1$s des salons publics"</string>
|
||||||
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Permet d’être trouvé en recherchant dans l’annuaire public."</string>
|
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Permet d’être trouvé en recherchant dans l’annuaire public."</string>
|
||||||
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Visible dans l’annuaire public"</string>
|
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Visible dans l’annuaire public"</string>
|
||||||
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Tout le monde"</string>
|
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Tout le monde (l’historique est public)"</string>
|
||||||
|
<string name="screen_security_and_privacy_room_history_section_footer">"Les changements n’affecteront pas les anciens messages, seulement les nouveaux. %1$s"</string>
|
||||||
<string name="screen_security_and_privacy_room_history_section_header">"Qui peux lire l’historique"</string>
|
<string name="screen_security_and_privacy_room_history_section_header">"Qui peux lire l’historique"</string>
|
||||||
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Les membres uniquement depuis qu’ils ont été invités"</string>
|
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Seulement les membres, depuis leur invitation"</string>
|
||||||
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Les membres uniquement depuis la sélection de cette option"</string>
|
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Membres (historique complet)"</string>
|
||||||
<string name="screen_security_and_privacy_room_publishing_section_footer">"Les adresses de salon sont un moyen de trouver et d’accéder aux salons. Cela vous permet également de partager facilement votre salon avec d’autres personnes.
|
<string name="screen_security_and_privacy_room_publishing_section_footer">"Les adresses de salon sont un moyen de trouver et d’accéder aux salons. Cela vous permet également de partager facilement votre salon avec d’autres personnes.
|
||||||
Vous pouvez choisir de publier votre salon dans l’annuaire des salons publics de votre serveur d’accueil."</string>
|
Vous pouvez choisir de publier votre salon dans l’annuaire des salons publics de votre serveur d’accueil."</string>
|
||||||
<string name="screen_security_and_privacy_room_publishing_section_header">"Publication du salon"</string>
|
<string name="screen_security_and_privacy_room_publishing_section_header">"Publication du salon"</string>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ data class RoomMemberModerationPermissions(
|
||||||
val canKick: Boolean,
|
val canKick: Boolean,
|
||||||
val canBan: Boolean,
|
val canBan: Boolean,
|
||||||
) {
|
) {
|
||||||
val hasAny = canKick || canBan
|
// Unban requires both kick and ban permission instead of a dedicated unban permission
|
||||||
|
val canUnban = canBan && canKick
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val DEFAULT = RoomMemberModerationPermissions(
|
val DEFAULT = RoomMemberModerationPermissions(
|
||||||
|
|
|
||||||
|
|
@ -161,16 +161,27 @@ class RoomMemberModerationPresenter(
|
||||||
val canModerateThisUser = currentUserPowerLevel > targetMemberPowerLevel
|
val canModerateThisUser = currentUserPowerLevel > targetMemberPowerLevel
|
||||||
// Assume the member is joined when it's unknown
|
// Assume the member is joined when it's unknown
|
||||||
val membership = member?.membership ?: RoomMembershipState.JOIN
|
val membership = member?.membership ?: RoomMembershipState.JOIN
|
||||||
if (permissions.canKick) {
|
when (membership) {
|
||||||
// Unban requires kick permission instead of a dedicated unban permission
|
RoomMembershipState.BAN -> {
|
||||||
if (membership == RoomMembershipState.BAN) {
|
if (permissions.canUnban) {
|
||||||
add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser))
|
add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser))
|
||||||
} else if (membership != RoomMembershipState.LEAVE) {
|
}
|
||||||
add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = canModerateThisUser))
|
}
|
||||||
|
RoomMembershipState.INVITE,
|
||||||
|
RoomMembershipState.JOIN,
|
||||||
|
RoomMembershipState.KNOCK -> {
|
||||||
|
if (permissions.canKick) {
|
||||||
|
add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = canModerateThisUser))
|
||||||
|
}
|
||||||
|
if (permissions.canBan) {
|
||||||
|
add(ModerationActionState(action = ModerationAction.BanUser, isEnabled = canModerateThisUser))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RoomMembershipState.LEAVE -> {
|
||||||
|
if (permissions.canBan) {
|
||||||
|
add(ModerationActionState(action = ModerationAction.BanUser, isEnabled = canModerateThisUser))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (permissions.canBan && membership != RoomMembershipState.BAN) {
|
|
||||||
add(ModerationActionState(action = ModerationAction.BanUser, isEnabled = canModerateThisUser))
|
|
||||||
}
|
}
|
||||||
}.toImmutableList()
|
}.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<string name="screen_recovery_key_confirm_description">"Pazite da nitko ne vidi ovaj zaslon!"</string>
|
<string name="screen_recovery_key_confirm_description">"Pazite da nitko ne vidi ovaj zaslon!"</string>
|
||||||
<string name="screen_recovery_key_confirm_error_content">"Pokušajte ponovno potvrditi pristup pohrani ključeva."</string>
|
<string name="screen_recovery_key_confirm_error_content">"Pokušajte ponovno potvrditi pristup pohrani ključeva."</string>
|
||||||
<string name="screen_recovery_key_confirm_error_title">"Neispravan ključ za oporavak"</string>
|
<string name="screen_recovery_key_confirm_error_title">"Neispravan ključ za oporavak"</string>
|
||||||
<string name="screen_recovery_key_confirm_key_description">"Ako imate sigurnosni ključ ili sigurnosnu frazu, i ovo će funkcionirati."</string>
|
<string name="screen_recovery_key_confirm_key_description">"Ako imate sigurnosni ključ ili sigurnosni izraz, i ovo će funkcionirati."</string>
|
||||||
<string name="screen_recovery_key_confirm_key_placeholder">"Unos…"</string>
|
<string name="screen_recovery_key_confirm_key_placeholder">"Unos…"</string>
|
||||||
<string name="screen_recovery_key_confirm_lost_recovery_key">"Izgubili ste ključ za oporavak?"</string>
|
<string name="screen_recovery_key_confirm_lost_recovery_key">"Izgubili ste ključ za oporavak?"</string>
|
||||||
<string name="screen_recovery_key_confirm_success">"Ključ za oporavak je potvrđen"</string>
|
<string name="screen_recovery_key_confirm_success">"Ključ za oporavak je potvrđen"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@
|
||||||
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Autres espaces dont vous n’êtes pas membre"</string>
|
<string name="screen_manage_authorized_spaces_unknown_spaces_section_title">"Autres espaces dont vous n’êtes pas membre"</string>
|
||||||
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Vos espaces"</string>
|
<string name="screen_manage_authorized_spaces_your_spaces_section_title">"Vos espaces"</string>
|
||||||
<string name="screen_security_and_privacy_add_room_address_action">"Ajouter une adresse"</string>
|
<string name="screen_security_and_privacy_add_room_address_action">"Ajouter une adresse"</string>
|
||||||
|
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Toute personne se trouvant dans un espace autorisé peut participer, mais toutes les autres doivent demander l’accès."</string>
|
||||||
<string name="screen_security_and_privacy_ask_to_join_option_description">"Tout le monde doit demander un accès."</string>
|
<string name="screen_security_and_privacy_ask_to_join_option_description">"Tout le monde doit demander un accès."</string>
|
||||||
<string name="screen_security_and_privacy_ask_to_join_option_title">"Demander à rejoindre"</string>
|
<string name="screen_security_and_privacy_ask_to_join_option_title">"Demander à rejoindre"</string>
|
||||||
|
<string name="screen_security_and_privacy_ask_to_join_single_space_members_option_description">"Tout membre de %1$s peut rejoindre l’espace, mais les autres doivent demander un accès."</string>
|
||||||
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Oui, activer le chiffrement"</string>
|
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Oui, activer le chiffrement"</string>
|
||||||
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Une fois activé, le chiffrement d’un salon ne peut pas être désactivé. L’historique des messages ne sera visible que pour les membres depuis qu’ils ont été invités ou depuis qu’ils ont rejoint le salon.
|
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Une fois activé, le chiffrement d’un salon ne peut pas être désactivé. L’historique des messages ne sera visible que pour les membres depuis qu’ils ont été invités ou depuis qu’ils ont rejoint le salon.
|
||||||
Personne d’autre que les membres du salon ne pourra lire les messages. Cela peut empêcher les bots et les bridges de fonctionner correctement.
|
Personne d’autre que les membres du salon ne pourra lire les messages. Cela peut empêcher les bots et les bridges de fonctionner correctement.
|
||||||
|
|
@ -34,10 +36,11 @@ Nous ne recommandons pas d’activer le chiffrement pour les salons que tout le
|
||||||
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Autoriser le salon à apparaître dans les résultats de recherche dans le répertoire %1$s des salons publics"</string>
|
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Autoriser le salon à apparaître dans les résultats de recherche dans le répertoire %1$s des salons publics"</string>
|
||||||
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Permet d’être trouvé en recherchant dans l’annuaire public."</string>
|
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Permet d’être trouvé en recherchant dans l’annuaire public."</string>
|
||||||
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Visible dans l’annuaire public"</string>
|
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Visible dans l’annuaire public"</string>
|
||||||
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Tout le monde"</string>
|
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Tout le monde (l’historique est public)"</string>
|
||||||
|
<string name="screen_security_and_privacy_room_history_section_footer">"Les changements n’affecteront pas les anciens messages, seulement les nouveaux. %1$s"</string>
|
||||||
<string name="screen_security_and_privacy_room_history_section_header">"Qui peux lire l’historique"</string>
|
<string name="screen_security_and_privacy_room_history_section_header">"Qui peux lire l’historique"</string>
|
||||||
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Les membres uniquement depuis qu’ils ont été invités"</string>
|
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Seulement les membres, depuis leur invitation"</string>
|
||||||
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Les membres uniquement depuis la sélection de cette option"</string>
|
<string name="screen_security_and_privacy_room_history_since_selecting_option_title">"Membres (historique complet)"</string>
|
||||||
<string name="screen_security_and_privacy_room_publishing_section_footer">"Les adresses de salon sont un moyen de trouver et d’accéder aux salons. Cela vous permet également de partager facilement votre salon avec d’autres personnes.
|
<string name="screen_security_and_privacy_room_publishing_section_footer">"Les adresses de salon sont un moyen de trouver et d’accéder aux salons. Cela vous permet également de partager facilement votre salon avec d’autres personnes.
|
||||||
Vous pouvez choisir de publier votre salon dans l’annuaire des salons publics de votre serveur d’accueil."</string>
|
Vous pouvez choisir de publier votre salon dans l’annuaire des salons publics de votre serveur d’accueil."</string>
|
||||||
<string name="screen_security_and_privacy_room_publishing_section_header">"Publication du salon"</string>
|
<string name="screen_security_and_privacy_room_publishing_section_header">"Publication du salon"</string>
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ private fun SpaceInfoSection(
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = AvatarData(roomId.value, name, avatarUrl, AvatarSize.SpaceListItem),
|
avatarData = AvatarData(roomId.value, name, avatarUrl, AvatarSize.SpaceListItem),
|
||||||
avatarType = AvatarType.Space(),
|
avatarType = AvatarType.Space(),
|
||||||
contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_avatar) },
|
contentDescription = stringResource(CommonStrings.a11y_avatar),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(16.dp))
|
Spacer(Modifier.width(16.dp))
|
||||||
Column {
|
Column {
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||||
onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(),
|
onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
onShareSpace: () -> Unit = EnsureNeverCalled(),
|
onShareSpace: () -> Unit = EnsureNeverCalled(),
|
||||||
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onDetailsClick: () -> Unit = EnsureNeverCalled(),
|
onSettingsClick: () -> Unit = EnsureNeverCalled(),
|
||||||
onViewMembersClick: () -> Unit = EnsureNeverCalled(),
|
onViewMembersClick: () -> Unit = EnsureNeverCalled(),
|
||||||
acceptDeclineInviteView: @Composable () -> Unit = {},
|
acceptDeclineInviteView: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -151,7 +151,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||||
onRoomClick = onRoomClick,
|
onRoomClick = onRoomClick,
|
||||||
onShareSpace = onShareSpace,
|
onShareSpace = onShareSpace,
|
||||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||||
onSettingsClick = onDetailsClick,
|
onSettingsClick = onSettingsClick,
|
||||||
onViewMembersClick = onViewMembersClick,
|
onViewMembersClick = onViewMembersClick,
|
||||||
acceptDeclineInviteView = acceptDeclineInviteView,
|
acceptDeclineInviteView = acceptDeclineInviteView,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ fun UserProfileHeaderSection(
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
|
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
|
||||||
avatarType = AvatarType.User,
|
avatarType = AvatarType.User,
|
||||||
contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_user_avatar) },
|
contentDescription = stringResource(CommonStrings.a11y_user_avatar),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.clickable(
|
.clickable(
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ camera = "1.5.2"
|
||||||
work = "2.11.0"
|
work = "2.11.0"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_bom = "2025.12.00"
|
compose_bom = "2025.12.01"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
coroutines = "1.10.2"
|
coroutines = "1.10.2"
|
||||||
|
|
@ -32,7 +32,7 @@ accompanist = "0.37.3"
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
test_core = "1.7.0"
|
test_core = "1.7.0"
|
||||||
roborazzi = "1.52.0"
|
roborazzi = "1.55.0"
|
||||||
|
|
||||||
# Jetbrain
|
# Jetbrain
|
||||||
datetime = "0.7.1"
|
datetime = "0.7.1"
|
||||||
|
|
@ -44,7 +44,7 @@ coil = "3.3.0"
|
||||||
showkase = "1.0.5"
|
showkase = "1.0.5"
|
||||||
appyx = "1.7.1"
|
appyx = "1.7.1"
|
||||||
sqldelight = "2.2.1"
|
sqldelight = "2.2.1"
|
||||||
wysiwyg = "2.40.0"
|
wysiwyg = "2.41.0"
|
||||||
telephoto = "0.18.0"
|
telephoto = "0.18.0"
|
||||||
haze = "1.7.1"
|
haze = "1.7.1"
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ haze = "1.7.1"
|
||||||
dependencyAnalysis = "3.5.1"
|
dependencyAnalysis = "3.5.1"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
metro = "0.9.2"
|
metro = "0.9.3"
|
||||||
|
|
||||||
# Auto service
|
# Auto service
|
||||||
autoservice = "1.1.1"
|
autoservice = "1.1.1"
|
||||||
|
|
@ -177,7 +177,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
|
# 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
|
# 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.
|
# only fix API breaks and may add some TODOs.
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.12.19"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.1.7"
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||||
|
|
@ -201,7 +201,7 @@ sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.
|
||||||
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||||
sqlcipher = "net.zetetic:sqlcipher-android:4.12.0"
|
sqlcipher = "net.zetetic:sqlcipher-android:4.12.0"
|
||||||
sqlite = "androidx.sqlite:sqlite-ktx:2.6.2"
|
sqlite = "androidx.sqlite:sqlite-ktx:2.6.2"
|
||||||
unifiedpush = "org.unifiedpush.android:connector:3.1.2"
|
unifiedpush = "org.unifiedpush.android:connector:3.2.0"
|
||||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
||||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||||
|
|
@ -218,7 +218,7 @@ haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref =
|
||||||
color_picker = "io.mhssn:colorpicker:1.0.0"
|
color_picker = "io.mhssn:colorpicker:1.0.0"
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog:posthog-android:3.27.2"
|
posthog = "com.posthog:posthog-android:3.28.0"
|
||||||
sentry = "io.sentry:sentry-android:8.29.0"
|
sentry = "io.sentry:sentry-android:8.29.0"
|
||||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.29.2"
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,15 @@ package io.element.android.libraries.androidutils.json
|
||||||
|
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import dev.zacsweers.metro.Provider
|
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a Json instance configured to ignore unknown keys.
|
* Provides a Json instance configured to ignore unknown keys.
|
||||||
*/
|
*/
|
||||||
typealias JsonProvider = Provider<Json>
|
fun interface JsonProvider {
|
||||||
|
operator fun invoke(): Json
|
||||||
|
}
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ac84a7175c4a4897aa28eddcf722b7997c6576f612eb38fa09ffabcf7be11e00
|
oid sha256:32b12d0b26cd016a632a4cb87b71d5efcb2c0d816bf565bc90aee9963ce2d5df
|
||||||
size 119496
|
size 134117
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,14 @@ import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.compoundTypography
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TypographyPreview() = ElementTheme {
|
internal fun TypographyPreview() = ElementTheme {
|
||||||
Surface {
|
Surface {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
with(ElementTheme.materialTypography) {
|
with(compoundTypography) {
|
||||||
TypographyTokenPreview(displayLarge, "Display large")
|
TypographyTokenPreview(displayLarge, "Display large")
|
||||||
TypographyTokenPreview(displayMedium, "Display medium")
|
TypographyTokenPreview(displayMedium, "Display medium")
|
||||||
TypographyTokenPreview(displaySmall, "Display small")
|
TypographyTokenPreview(displaySmall, "Display small")
|
||||||
|
|
@ -44,6 +45,33 @@ internal fun TypographyPreview() = ElementTheme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun CompoundTypographyPreview() = ElementTheme {
|
||||||
|
Surface {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
with(ElementTheme.typography) {
|
||||||
|
TypographyTokenPreview(fontHeadingXlBold, "fontHeadingXlBold")
|
||||||
|
TypographyTokenPreview(fontHeadingXlRegular, "fontHeadingXlRegular")
|
||||||
|
TypographyTokenPreview(fontHeadingLgBold, "fontHeadingLgBold")
|
||||||
|
TypographyTokenPreview(fontHeadingLgRegular, "fontHeadingLgRegular")
|
||||||
|
TypographyTokenPreview(fontHeadingMdBold, "fontHeadingMdBold")
|
||||||
|
TypographyTokenPreview(fontHeadingMdRegular, "fontHeadingMdRegular")
|
||||||
|
TypographyTokenPreview(fontHeadingSmMedium, "fontHeadingSmMedium")
|
||||||
|
TypographyTokenPreview(fontHeadingSmRegular, "fontHeadingSmRegular")
|
||||||
|
TypographyTokenPreview(fontBodyLgMedium, "fontBodyLgMedium")
|
||||||
|
TypographyTokenPreview(fontBodyLgRegular, "fontBodyLgRegular")
|
||||||
|
TypographyTokenPreview(fontBodyMdMedium, "fontBodyMdMedium")
|
||||||
|
TypographyTokenPreview(fontBodyMdRegular, "fontBodyMdRegular")
|
||||||
|
TypographyTokenPreview(fontBodySmMedium, "fontBodySmMedium")
|
||||||
|
TypographyTokenPreview(fontBodySmRegular, "fontBodySmRegular")
|
||||||
|
TypographyTokenPreview(fontBodyXsMedium, "fontBodyXsMedium")
|
||||||
|
TypographyTokenPreview(fontBodyXsRegular, "fontBodyXsRegular")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TypographyTokenPreview(style: TextStyle, text: String) {
|
private fun TypographyTokenPreview(style: TextStyle, text: String) {
|
||||||
Text(text = text, style = style)
|
Text(text = text, style = style)
|
||||||
|
|
|
||||||
|
|
@ -62,14 +62,6 @@ object ElementTheme {
|
||||||
*/
|
*/
|
||||||
val typography: TypographyTokens = TypographyTokens
|
val typography: TypographyTokens = TypographyTokens
|
||||||
|
|
||||||
/**
|
|
||||||
* Material 3 [Typography] tokens. In Figma, these have the `M3 Typography/` prefix.
|
|
||||||
*/
|
|
||||||
val materialTypography: Typography
|
|
||||||
@Composable
|
|
||||||
@ReadOnlyComposable
|
|
||||||
get() = MaterialTheme.typography
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the theme version used is the light or the dark one.
|
* Returns whether the theme version used is the light or the dark one.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,10 @@
|
||||||
|
|
||||||
package io.element.android.compound.screenshot
|
package io.element.android.compound.screenshot
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.github.takahirom.roborazzi.captureRoboImage
|
import com.github.takahirom.roborazzi.captureRoboImage
|
||||||
|
import io.element.android.compound.previews.CompoundTypographyPreview
|
||||||
import io.element.android.compound.screenshot.utils.screenshotFile
|
import io.element.android.compound.screenshot.utils.screenshotFile
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.compound.tokens.generated.TypographyTokens
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
|
@ -32,35 +24,7 @@ class CompoundTypographyTest {
|
||||||
@Config(sdk = [35], qualifiers = "h2048dp-xxhdpi")
|
@Config(sdk = [35], qualifiers = "h2048dp-xxhdpi")
|
||||||
fun screenshots() {
|
fun screenshots() {
|
||||||
captureRoboImage(file = screenshotFile("Compound Typography.png")) {
|
captureRoboImage(file = screenshotFile("Compound Typography.png")) {
|
||||||
ElementTheme {
|
CompoundTypographyPreview()
|
||||||
Surface {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
with(TypographyTokens) {
|
|
||||||
TypographyTokenPreview(fontHeadingXlBold, "Heading XL Bold")
|
|
||||||
TypographyTokenPreview(fontHeadingXlRegular, "Heading XL Regular")
|
|
||||||
TypographyTokenPreview(fontHeadingLgBold, "Heading LG Bold")
|
|
||||||
TypographyTokenPreview(fontHeadingLgRegular, "Heading LG Regular")
|
|
||||||
TypographyTokenPreview(fontHeadingMdBold, "Heading MD Bold")
|
|
||||||
TypographyTokenPreview(fontHeadingMdRegular, "Heading MD Regular")
|
|
||||||
TypographyTokenPreview(fontHeadingSmMedium, "Heading SM Medium")
|
|
||||||
TypographyTokenPreview(fontHeadingSmRegular, "Heading SM Regular")
|
|
||||||
TypographyTokenPreview(fontBodyLgMedium, "Body LG Medium")
|
|
||||||
TypographyTokenPreview(fontBodyLgRegular, "Body LG Regular")
|
|
||||||
TypographyTokenPreview(fontBodyMdMedium, "Body MD Medium")
|
|
||||||
TypographyTokenPreview(fontBodyMdRegular, "Body MD Regular")
|
|
||||||
TypographyTokenPreview(fontBodySmMedium, "Body SM Medium")
|
|
||||||
TypographyTokenPreview(fontBodySmRegular, "Body SM Regular")
|
|
||||||
TypographyTokenPreview(fontBodyXsMedium, "Body XS Medium")
|
|
||||||
TypographyTokenPreview(fontBodyXsRegular, "Body XS Regular")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TypographyTokenPreview(style: TextStyle, text: String) {
|
|
||||||
Text(text = text, style = style)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.atomic.atoms
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.messageFromMeBackground
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaybackSpeedButton(
|
||||||
|
speed: Float,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val speedText = when (speed) {
|
||||||
|
0.5f -> "0.5×"
|
||||||
|
1.0f -> "1×"
|
||||||
|
1.5f -> "1.5×"
|
||||||
|
2.0f -> "2×"
|
||||||
|
else -> "$speed×"
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(
|
||||||
|
color = ElementTheme.colors.bgCanvasDefault,
|
||||||
|
)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = speedText,
|
||||||
|
color = ElementTheme.colors.iconSecondary,
|
||||||
|
style = ElementTheme.typography.fontBodyXsMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun PlaybackSpeedButtonPreview() = ElementPreview {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(ElementTheme.colors.messageFromMeBackground)
|
||||||
|
.padding(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
listOf(0.5f, 1.0f, 1.5f, 2.0f, 3.0f).forEach { speed ->
|
||||||
|
PlaybackSpeedButton(
|
||||||
|
speed = speed,
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ fun SelectedIndicatorAtom(
|
||||||
Icon(
|
Icon(
|
||||||
modifier = modifier.toggleable(
|
modifier = modifier.toggleable(
|
||||||
value = true,
|
value = true,
|
||||||
role = Role.Companion.Checkbox,
|
role = Role.Checkbox,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ fun DmAvatars(
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = userAvatarData,
|
avatarData = userAvatarData,
|
||||||
avatarType = AvatarType.User,
|
avatarType = AvatarType.User,
|
||||||
contentDescription = userAvatarData.url?.let { stringResource(CommonStrings.a11y_your_avatar) },
|
contentDescription = stringResource(CommonStrings.a11y_your_avatar),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomStart)
|
.align(Alignment.BottomStart)
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
|
|
@ -94,7 +94,7 @@ fun DmAvatars(
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = otherUserAvatarData,
|
avatarData = otherUserAvatarData,
|
||||||
avatarType = AvatarType.User,
|
avatarType = AvatarType.User,
|
||||||
contentDescription = otherUserAvatarData.url?.let { stringResource(CommonStrings.a11y_other_user_avatar) },
|
contentDescription = stringResource(CommonStrings.a11y_other_user_avatar),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ internal fun ImageAvatar(
|
||||||
SubcomposeAsyncImage(
|
SubcomposeAsyncImage(
|
||||||
model = avatarData,
|
model = avatarData,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
contentScale = ContentScale.Companion.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(size)
|
.size(size)
|
||||||
.clip(avatarShape)
|
.clip(avatarShape)
|
||||||
|
|
|
||||||
|
|
@ -57,14 +57,14 @@ fun ErrorDialogWithDoNotShowAgain(
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = content,
|
text = content,
|
||||||
style = ElementTheme.materialTypography.bodyMedium,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Checkbox(checked = doNotShowAgain, onCheckedChange = { doNotShowAgain = it })
|
Checkbox(checked = doNotShowAgain, onCheckedChange = { doNotShowAgain = it })
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = CommonStrings.common_do_not_show_this_again),
|
text = stringResource(id = CommonStrings.common_do_not_show_this_again),
|
||||||
style = ElementTheme.materialTypography.bodyMedium,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ fun TextFieldDialog(
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = content,
|
text = content,
|
||||||
style = ElementTheme.materialTypography.bodyMedium,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ internal fun SimpleAlertDialogContent(
|
||||||
content = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = content,
|
text = content,
|
||||||
style = ElementTheme.materialTypography.bodyMedium,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
submitText = submitText,
|
submitText = submitText,
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,15 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.text.input.InputTransformation
|
||||||
|
import androidx.compose.foundation.text.input.KeyboardActionHandler
|
||||||
|
import androidx.compose.foundation.text.input.OutputTransformation
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.TextFieldColors
|
import androidx.compose.material3.TextFieldColors
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.TextFieldLabelScope
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -135,6 +141,51 @@ fun FilledTextField(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FilledTextField(
|
||||||
|
state: TextFieldState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
readOnly: Boolean = false,
|
||||||
|
textStyle: TextStyle = LocalTextStyle.current,
|
||||||
|
label: @Composable (TextFieldLabelScope.() -> Unit)? = null,
|
||||||
|
placeholder: @Composable (() -> Unit)? = null,
|
||||||
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
trailingIcon: @Composable (() -> Unit)? = null,
|
||||||
|
supportingText: @Composable (() -> Unit)? = null,
|
||||||
|
isError: Boolean = false,
|
||||||
|
inputTransformation: InputTransformation? = null,
|
||||||
|
outputTransformation: OutputTransformation? = null,
|
||||||
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
keyboardActions: KeyboardActionHandler? = null,
|
||||||
|
lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default,
|
||||||
|
interactionSource: MutableInteractionSource? = null,
|
||||||
|
shape: Shape = TextFieldDefaults.shape,
|
||||||
|
colors: TextFieldColors = TextFieldDefaults.colors()
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.TextField(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
readOnly = readOnly,
|
||||||
|
textStyle = textStyle,
|
||||||
|
label = label,
|
||||||
|
placeholder = placeholder,
|
||||||
|
leadingIcon = leadingIcon,
|
||||||
|
trailingIcon = trailingIcon,
|
||||||
|
supportingText = supportingText,
|
||||||
|
isError = isError,
|
||||||
|
inputTransformation = inputTransformation,
|
||||||
|
outputTransformation = outputTransformation,
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
onKeyboardAction = keyboardActions,
|
||||||
|
lineLimits = lineLimits,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
shape = shape,
|
||||||
|
colors = colors,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.TextFields)
|
@Preview(group = PreviewGroup.TextFields)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun FilledTextFieldLightPreview() =
|
internal fun FilledTextFieldLightPreview() =
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ fun ListItem(
|
||||||
|
|
||||||
val decoratedHeadlineContent: @Composable () -> Unit = {
|
val decoratedHeadlineContent: @Composable () -> Unit = {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextStyle provides ElementTheme.materialTypography.bodyLarge,
|
LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular,
|
||||||
LocalContentColor provides headlineColor,
|
LocalContentColor provides headlineColor,
|
||||||
) {
|
) {
|
||||||
headlineContent()
|
headlineContent()
|
||||||
|
|
@ -123,7 +123,7 @@ fun ListItem(
|
||||||
val decoratedSupportingContent: (@Composable () -> Unit)? = supportingContent?.let { content ->
|
val decoratedSupportingContent: (@Composable () -> Unit)? = supportingContent?.let { content ->
|
||||||
{
|
{
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextStyle provides ElementTheme.materialTypography.bodyMedium,
|
LocalTextStyle provides ElementTheme.typography.fontBodyMdRegular,
|
||||||
LocalContentColor provides supportingColor,
|
LocalContentColor provides supportingColor,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,6 @@ object ElementNavigationBarItemDefaults {
|
||||||
selectedTextColor = ElementTheme.colors.textPrimary,
|
selectedTextColor = ElementTheme.colors.textPrimary,
|
||||||
unselectedIconColor = ElementTheme.colors.iconTertiary,
|
unselectedIconColor = ElementTheme.colors.iconTertiary,
|
||||||
unselectedTextColor = ElementTheme.colors.textDisabled,
|
unselectedTextColor = ElementTheme.colors.textDisabled,
|
||||||
selectedIndicatorColor = Color.Companion.Transparent,
|
selectedIndicatorColor = Color.Transparent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,12 +70,6 @@ enum class FeatureFlags(
|
||||||
defaultValue = { false },
|
defaultValue = { false },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
Space(
|
|
||||||
key = "feature.space",
|
|
||||||
title = "Spaces",
|
|
||||||
defaultValue = { true },
|
|
||||||
isFinished = true,
|
|
||||||
),
|
|
||||||
SpaceSettings(
|
SpaceSettings(
|
||||||
key = "feature.spaceSettings",
|
key = "feature.spaceSettings",
|
||||||
title = "Space settings",
|
title = "Space settings",
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,14 @@ class DefaultFeatureFlagService(
|
||||||
private val featuresProvider: FeaturesProvider,
|
private val featuresProvider: FeaturesProvider,
|
||||||
) : FeatureFlagService {
|
) : FeatureFlagService {
|
||||||
override fun isFeatureEnabledFlow(feature: Feature): Flow<Boolean> {
|
override fun isFeatureEnabledFlow(feature: Feature): Flow<Boolean> {
|
||||||
return providers.filter { it.hasFeature(feature) }
|
return if (feature.isFinished) {
|
||||||
.maxByOrNull(FeatureFlagProvider::priority)
|
flowOf(feature.defaultValue(buildMeta))
|
||||||
?.isFeatureEnabledFlow(feature)
|
} else {
|
||||||
?: flowOf(feature.defaultValue(buildMeta))
|
providers.filter { it.hasFeature(feature) }
|
||||||
|
.maxByOrNull(FeatureFlagProvider::priority)
|
||||||
|
?.isFeatureEnabledFlow(feature)
|
||||||
|
?: flowOf(feature.defaultValue(buildMeta))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean {
|
override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean {
|
||||||
|
|
|
||||||
|
|
@ -177,4 +177,9 @@ interface JoinedRoom : BaseRoom {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
suspend fun withdrawVerificationAndResend(userIds: List<UserId>, sendHandle: SendHandle): Result<Unit>
|
suspend fun withdrawVerificationAndResend(userIds: List<UserId>, sendHandle: SendHandle): Result<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to a [Flow] of [SendQueueUpdate] related to this room.
|
||||||
|
*/
|
||||||
|
fun subscribeToSendQueueUpdates(): Flow<SendQueueUpdate>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.api.room
|
||||||
|
|
||||||
|
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.media.MediaSource
|
||||||
|
|
||||||
|
sealed interface SendQueueUpdate {
|
||||||
|
data class NewLocalEvent(val transactionId: TransactionId) : SendQueueUpdate
|
||||||
|
data class CancelledLocalEvent(val transactionId: TransactionId) : SendQueueUpdate
|
||||||
|
data class ReplacedLocalEvent(val transactionId: TransactionId) : SendQueueUpdate
|
||||||
|
data class SendError(val transactionId: TransactionId) : SendQueueUpdate
|
||||||
|
data class RetrySendingEvent(val transactionId: TransactionId) : SendQueueUpdate
|
||||||
|
data class SentEvent(val transactionId: TransactionId, val eventId: EventId) : SendQueueUpdate
|
||||||
|
data class MediaUpload(val relatedTo: EventId, val file: MediaSource?, val index: Long, val progress: Float) : SendQueueUpdate
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.room.CreateTimelineParams
|
||||||
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
import io.element.android.libraries.matrix.api.room.IntentionalMention
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState
|
import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState
|
||||||
|
import io.element.android.libraries.matrix.api.room.SendQueueUpdate
|
||||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||||
|
|
@ -66,6 +67,8 @@ import org.matrix.rustcomponents.sdk.DateDividerMode
|
||||||
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
|
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
|
||||||
import org.matrix.rustcomponents.sdk.KnockRequestsListener
|
import org.matrix.rustcomponents.sdk.KnockRequestsListener
|
||||||
import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
|
import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomSendQueueUpdate
|
||||||
|
import org.matrix.rustcomponents.sdk.SendQueueListener
|
||||||
import org.matrix.rustcomponents.sdk.TimelineConfiguration
|
import org.matrix.rustcomponents.sdk.TimelineConfiguration
|
||||||
import org.matrix.rustcomponents.sdk.TimelineFilter
|
import org.matrix.rustcomponents.sdk.TimelineFilter
|
||||||
import org.matrix.rustcomponents.sdk.TimelineFocus
|
import org.matrix.rustcomponents.sdk.TimelineFocus
|
||||||
|
|
@ -486,6 +489,16 @@ class JoinedRustRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun subscribeToSendQueueUpdates(): Flow<SendQueueUpdate> {
|
||||||
|
return mxCallbackFlow {
|
||||||
|
innerRoom.subscribeToSendQueueUpdates(object : SendQueueListener {
|
||||||
|
override fun onUpdate(update: RoomSendQueueUpdate) {
|
||||||
|
trySend(update.map())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() = destroy()
|
override fun close() = destroy()
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.room
|
||||||
|
|
||||||
|
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.room.SendQueueUpdate
|
||||||
|
import io.element.android.libraries.matrix.impl.media.map
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomSendQueueUpdate
|
||||||
|
|
||||||
|
fun RoomSendQueueUpdate.map(): SendQueueUpdate = when (this) {
|
||||||
|
is RoomSendQueueUpdate.NewLocalEvent -> SendQueueUpdate.NewLocalEvent(TransactionId(transactionId))
|
||||||
|
is RoomSendQueueUpdate.CancelledLocalEvent -> SendQueueUpdate.CancelledLocalEvent(TransactionId(transactionId))
|
||||||
|
is RoomSendQueueUpdate.MediaUpload -> SendQueueUpdate.MediaUpload(
|
||||||
|
relatedTo = EventId(relatedTo),
|
||||||
|
file = file?.map(),
|
||||||
|
index = index.toLong(),
|
||||||
|
progress = progress.current.toFloat() / progress.total.toFloat(),
|
||||||
|
)
|
||||||
|
is RoomSendQueueUpdate.ReplacedLocalEvent -> SendQueueUpdate.ReplacedLocalEvent(TransactionId(transactionId))
|
||||||
|
is RoomSendQueueUpdate.RetryEvent -> SendQueueUpdate.RetrySendingEvent(TransactionId(transactionId))
|
||||||
|
is RoomSendQueueUpdate.SendError -> SendQueueUpdate.SendError(TransactionId(transactionId))
|
||||||
|
is RoomSendQueueUpdate.SentEvent -> SendQueueUpdate.SentEvent(TransactionId(transactionId), EventId(eventId))
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ import org.matrix.rustcomponents.sdk.QueueWedgeError
|
||||||
import org.matrix.rustcomponents.sdk.Reaction
|
import org.matrix.rustcomponents.sdk.Reaction
|
||||||
import org.matrix.rustcomponents.sdk.ShieldState
|
import org.matrix.rustcomponents.sdk.ShieldState
|
||||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||||
import uniffi.matrix_sdk_common.ShieldStateCode
|
import uniffi.matrix_sdk_ui.TimelineEventShieldStateCode
|
||||||
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
|
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
|
||||||
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
|
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
|
||||||
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
|
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
|
||||||
|
|
@ -58,7 +58,7 @@ class EventTimelineItemMapper(
|
||||||
content = contentMapper.map(content),
|
content = contentMapper.map(content),
|
||||||
origin = origin?.map(),
|
origin = origin?.map(),
|
||||||
timelineItemDebugInfoProvider = { lazyProvider.debugInfo().map() },
|
timelineItemDebugInfoProvider = { lazyProvider.debugInfo().map() },
|
||||||
messageShieldProvider = { strict -> lazyProvider.getShields(strict)?.map() },
|
messageShieldProvider = { strict -> lazyProvider.getShields(strict).map() },
|
||||||
sendHandleProvider = { lazyProvider.getSendHandle()?.let(::RustSendHandle) }
|
sendHandleProvider = { lazyProvider.getSendHandle()?.let(::RustSendHandle) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -182,13 +182,13 @@ private fun ShieldState?.map(): MessageShield? {
|
||||||
is ShieldState.Red -> true
|
is ShieldState.Red -> true
|
||||||
}
|
}
|
||||||
return when (shieldStateCode) {
|
return when (shieldStateCode) {
|
||||||
ShieldStateCode.AUTHENTICITY_NOT_GUARANTEED -> MessageShield.AuthenticityNotGuaranteed(isCritical)
|
TimelineEventShieldStateCode.AUTHENTICITY_NOT_GUARANTEED -> MessageShield.AuthenticityNotGuaranteed(isCritical)
|
||||||
ShieldStateCode.UNKNOWN_DEVICE -> MessageShield.UnknownDevice(isCritical)
|
TimelineEventShieldStateCode.UNKNOWN_DEVICE -> MessageShield.UnknownDevice(isCritical)
|
||||||
ShieldStateCode.UNSIGNED_DEVICE -> MessageShield.UnsignedDevice(isCritical)
|
TimelineEventShieldStateCode.UNSIGNED_DEVICE -> MessageShield.UnsignedDevice(isCritical)
|
||||||
ShieldStateCode.UNVERIFIED_IDENTITY -> MessageShield.UnverifiedIdentity(isCritical)
|
TimelineEventShieldStateCode.UNVERIFIED_IDENTITY -> MessageShield.UnverifiedIdentity(isCritical)
|
||||||
ShieldStateCode.SENT_IN_CLEAR -> MessageShield.SentInClear(isCritical)
|
TimelineEventShieldStateCode.SENT_IN_CLEAR -> MessageShield.SentInClear(isCritical)
|
||||||
ShieldStateCode.VERIFICATION_VIOLATION -> MessageShield.VerificationViolation(isCritical)
|
TimelineEventShieldStateCode.VERIFICATION_VIOLATION -> MessageShield.VerificationViolation(isCritical)
|
||||||
ShieldStateCode.MISMATCHED_SENDER -> MessageShield.MismatchedSender(isCritical)
|
TimelineEventShieldStateCode.MISMATCHED_SENDER -> MessageShield.MismatchedSender(isCritical)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,8 @@ class RustSessionVerificationService(
|
||||||
// Listen for changes in verification status and update accordingly
|
// Listen for changes in verification status and update accordingly
|
||||||
private val verificationStateListenerTaskHandle = encryptionService.verificationStateListener(object : VerificationStateListener {
|
private val verificationStateListenerTaskHandle = encryptionService.verificationStateListener(object : VerificationStateListener {
|
||||||
override fun onUpdate(status: VerificationState) {
|
override fun onUpdate(status: VerificationState) {
|
||||||
if (!isInitialized.get()) {
|
// If the status is verified, just use it. It can't be a false positive like unknown or unverified
|
||||||
|
if (!isInitialized.get() && status != VerificationState.VERIFIED) {
|
||||||
Timber.d("Discarding new verifications state: $status. E2EE is not initialised yet")
|
Timber.d("Discarding new verifications state: $status. E2EE is not initialised yet")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,22 +21,24 @@ import org.matrix.rustcomponents.sdk.ShieldState
|
||||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||||
import uniffi.matrix_sdk_ui.EventItemOrigin
|
import uniffi.matrix_sdk_ui.EventItemOrigin
|
||||||
|
|
||||||
fun aRustEventTimelineItem(
|
internal fun aRustEventTimelineItem(
|
||||||
isRemote: Boolean = true,
|
isRemote: Boolean = true,
|
||||||
eventOrTransactionId: EventOrTransactionId = EventOrTransactionId.EventId(AN_EVENT_ID.value),
|
eventOrTransactionId: EventOrTransactionId = EventOrTransactionId.EventId(AN_EVENT_ID.value),
|
||||||
sender: String = A_USER_ID.value,
|
sender: String = A_USER_ID.value,
|
||||||
senderProfile: ProfileDetails = ProfileDetails.Unavailable,
|
senderProfile: ProfileDetails = ProfileDetails.Unavailable,
|
||||||
isOwn: Boolean = true,
|
isOwn: Boolean = true,
|
||||||
isEditable: Boolean = true,
|
isEditable: Boolean = true,
|
||||||
content: TimelineItemContent = aRustTimelineItemMessageContent(),
|
content: TimelineItemContent = aRustTimelineItemContentMsgLike(),
|
||||||
timestamp: ULong = 0uL,
|
timestamp: ULong = 0uL,
|
||||||
debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(),
|
debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(),
|
||||||
localSendState: EventSendState? = null,
|
localSendState: EventSendState? = null,
|
||||||
readReceipts: Map<String, Receipt> = emptyMap(),
|
readReceipts: Map<String, Receipt> = emptyMap(),
|
||||||
origin: EventItemOrigin? = EventItemOrigin.SYNC,
|
origin: EventItemOrigin? = EventItemOrigin.SYNC,
|
||||||
canBeRepliedTo: Boolean = true,
|
canBeRepliedTo: Boolean = true,
|
||||||
shieldsState: ShieldState? = null,
|
shieldsState: ShieldState = ShieldState.None,
|
||||||
localCreatedAt: ULong? = null,
|
localCreatedAt: ULong? = null,
|
||||||
|
forwarder: String? = null,
|
||||||
|
forwarderProfile: ProfileDetails? = null,
|
||||||
) = EventTimelineItem(
|
) = EventTimelineItem(
|
||||||
isRemote = isRemote,
|
isRemote = isRemote,
|
||||||
eventOrTransactionId = eventOrTransactionId,
|
eventOrTransactionId = eventOrTransactionId,
|
||||||
|
|
@ -54,5 +56,7 @@ fun aRustEventTimelineItem(
|
||||||
lazyProvider = FakeFfiLazyTimelineItemProvider(
|
lazyProvider = FakeFfiLazyTimelineItemProvider(
|
||||||
debugInfo = debugInfo,
|
debugInfo = debugInfo,
|
||||||
shieldsState = shieldsState,
|
shieldsState = shieldsState,
|
||||||
)
|
),
|
||||||
|
forwarder = forwarder,
|
||||||
|
forwarderProfile = forwarderProfile,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.fixtures.factories
|
||||||
|
|
||||||
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo
|
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo
|
||||||
|
|
||||||
fun anEventTimelineItemDebugInfo(
|
internal fun anEventTimelineItemDebugInfo(
|
||||||
model: String = "model",
|
model: String = "model",
|
||||||
originalJson: String? = null,
|
originalJson: String? = null,
|
||||||
latestEditJson: String? = null,
|
latestEditJson: String? = null,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEv
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import org.matrix.rustcomponents.sdk.Action
|
import org.matrix.rustcomponents.sdk.Action
|
||||||
|
import org.matrix.rustcomponents.sdk.BatchNotificationResult
|
||||||
import org.matrix.rustcomponents.sdk.JoinRule
|
import org.matrix.rustcomponents.sdk.JoinRule
|
||||||
import org.matrix.rustcomponents.sdk.NotificationEvent
|
import org.matrix.rustcomponents.sdk.NotificationEvent
|
||||||
import org.matrix.rustcomponents.sdk.NotificationItem
|
import org.matrix.rustcomponents.sdk.NotificationItem
|
||||||
|
|
@ -21,7 +22,7 @@ import org.matrix.rustcomponents.sdk.NotificationSenderInfo
|
||||||
import org.matrix.rustcomponents.sdk.NotificationStatus
|
import org.matrix.rustcomponents.sdk.NotificationStatus
|
||||||
import org.matrix.rustcomponents.sdk.TimelineEvent
|
import org.matrix.rustcomponents.sdk.TimelineEvent
|
||||||
|
|
||||||
fun aRustNotificationItem(
|
internal fun aRustNotificationItem(
|
||||||
event: NotificationEvent = aRustNotificationEventTimeline(),
|
event: NotificationEvent = aRustNotificationEventTimeline(),
|
||||||
senderInfo: NotificationSenderInfo = aRustNotificationSenderInfo(),
|
senderInfo: NotificationSenderInfo = aRustNotificationSenderInfo(),
|
||||||
roomInfo: NotificationRoomInfo = aRustNotificationRoomInfo(),
|
roomInfo: NotificationRoomInfo = aRustNotificationRoomInfo(),
|
||||||
|
|
@ -39,13 +40,13 @@ fun aRustNotificationItem(
|
||||||
actions = actions,
|
actions = actions,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRustBatchNotificationResult(
|
internal fun aRustBatchNotificationResultOk(
|
||||||
notificationStatus: NotificationStatus = NotificationStatus.Event(aRustNotificationItem()),
|
notificationStatus: NotificationStatus = NotificationStatus.Event(aRustNotificationItem()),
|
||||||
) = org.matrix.rustcomponents.sdk.BatchNotificationResult.Ok(
|
) = BatchNotificationResult.Ok(
|
||||||
status = notificationStatus,
|
status = notificationStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRustNotificationSenderInfo(
|
internal fun aRustNotificationSenderInfo(
|
||||||
displayName: String? = A_USER_NAME,
|
displayName: String? = A_USER_NAME,
|
||||||
avatarUrl: String? = null,
|
avatarUrl: String? = null,
|
||||||
isNameAmbiguous: Boolean = false,
|
isNameAmbiguous: Boolean = false,
|
||||||
|
|
@ -55,7 +56,7 @@ fun aRustNotificationSenderInfo(
|
||||||
isNameAmbiguous = isNameAmbiguous,
|
isNameAmbiguous = isNameAmbiguous,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRustNotificationRoomInfo(
|
internal fun aRustNotificationRoomInfo(
|
||||||
displayName: String = A_ROOM_NAME,
|
displayName: String = A_ROOM_NAME,
|
||||||
avatarUrl: String? = null,
|
avatarUrl: String? = null,
|
||||||
canonicalAlias: String? = null,
|
canonicalAlias: String? = null,
|
||||||
|
|
@ -77,7 +78,7 @@ fun aRustNotificationRoomInfo(
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRustNotificationEventTimeline(
|
internal fun aRustNotificationEventTimeline(
|
||||||
event: TimelineEvent = FakeFfiTimelineEvent(),
|
event: TimelineEvent = FakeFfiTimelineEvent(),
|
||||||
) = NotificationEvent.Timeline(
|
) = NotificationEvent.Timeline(
|
||||||
event = event,
|
event = event,
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,13 @@ internal fun aRustRoomDescription(
|
||||||
joinRule: PublicRoomJoinRule = PublicRoomJoinRule.PUBLIC,
|
joinRule: PublicRoomJoinRule = PublicRoomJoinRule.PUBLIC,
|
||||||
isWorldReadable: Boolean = true,
|
isWorldReadable: Boolean = true,
|
||||||
joinedMembers: ULong = 2u,
|
joinedMembers: ULong = 2u,
|
||||||
): RoomDescription {
|
) = RoomDescription(
|
||||||
return RoomDescription(
|
roomId = roomId,
|
||||||
roomId = roomId,
|
name = name,
|
||||||
name = name,
|
topic = topic,
|
||||||
topic = topic,
|
alias = alias,
|
||||||
alias = alias,
|
avatarUrl = avatarUrl,
|
||||||
avatarUrl = avatarUrl,
|
joinRule = joinRule,
|
||||||
joinRule = joinRule,
|
isWorldReadable = isWorldReadable,
|
||||||
isWorldReadable = isWorldReadable,
|
joinedMembers = joinedMembers,
|
||||||
joinedMembers = joinedMembers,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,8 @@ import org.matrix.rustcomponents.sdk.RoomHero
|
||||||
|
|
||||||
internal fun aRustRoomHero(
|
internal fun aRustRoomHero(
|
||||||
userId: UserId = A_USER_ID,
|
userId: UserId = A_USER_ID,
|
||||||
): RoomHero {
|
) = RoomHero(
|
||||||
return RoomHero(
|
userId = userId.value,
|
||||||
userId = userId.value,
|
displayName = "displayName",
|
||||||
displayName = "displayName",
|
avatarUrl = "avatarUrl",
|
||||||
avatarUrl = "avatarUrl",
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import org.matrix.rustcomponents.sdk.RoomPowerLevels
|
||||||
import org.matrix.rustcomponents.sdk.SuccessorRoom
|
import org.matrix.rustcomponents.sdk.SuccessorRoom
|
||||||
import uniffi.matrix_sdk_base.EncryptionState
|
import uniffi.matrix_sdk_base.EncryptionState
|
||||||
|
|
||||||
fun aRustRoomInfo(
|
internal fun aRustRoomInfo(
|
||||||
id: String = A_ROOM_ID.value,
|
id: String = A_ROOM_ID.value,
|
||||||
displayName: String? = A_ROOM_NAME,
|
displayName: String? = A_ROOM_NAME,
|
||||||
rawName: String? = A_ROOM_NAME,
|
rawName: String? = A_ROOM_NAME,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import org.matrix.rustcomponents.sdk.PowerLevel
|
||||||
import org.matrix.rustcomponents.sdk.RoomMember
|
import org.matrix.rustcomponents.sdk.RoomMember
|
||||||
import uniffi.matrix_sdk.RoomMemberRole
|
import uniffi.matrix_sdk.RoomMemberRole
|
||||||
|
|
||||||
fun aRustRoomMember(
|
internal fun aRustRoomMember(
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
displayName: String? = null,
|
displayName: String? = null,
|
||||||
avatarUrl: String? = null,
|
avatarUrl: String? = null,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.impl.fixtures.factories
|
||||||
import org.matrix.rustcomponents.sdk.RoomNotificationMode
|
import org.matrix.rustcomponents.sdk.RoomNotificationMode
|
||||||
import org.matrix.rustcomponents.sdk.RoomNotificationSettings
|
import org.matrix.rustcomponents.sdk.RoomNotificationSettings
|
||||||
|
|
||||||
fun aRustRoomNotificationSettings(
|
internal fun aRustRoomNotificationSettings(
|
||||||
mode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
mode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
||||||
isDefault: Boolean = true,
|
isDefault: Boolean = true,
|
||||||
) = RoomNotificationSettings(
|
) = RoomNotificationSettings(
|
||||||
|
|
|
||||||
|
|
@ -19,20 +19,18 @@ internal fun aRustRoomPreviewInfo(
|
||||||
canonicalAlias: String? = A_ROOM_ALIAS.value,
|
canonicalAlias: String? = A_ROOM_ALIAS.value,
|
||||||
membership: Membership? = Membership.JOINED,
|
membership: Membership? = Membership.JOINED,
|
||||||
joinRule: JoinRule = JoinRule.Public,
|
joinRule: JoinRule = JoinRule.Public,
|
||||||
): RoomPreviewInfo {
|
) = RoomPreviewInfo(
|
||||||
return RoomPreviewInfo(
|
roomId = A_ROOM_ID.value,
|
||||||
roomId = A_ROOM_ID.value,
|
canonicalAlias = canonicalAlias,
|
||||||
canonicalAlias = canonicalAlias,
|
name = "name",
|
||||||
name = "name",
|
topic = "topic",
|
||||||
topic = "topic",
|
avatarUrl = "avatarUrl",
|
||||||
avatarUrl = "avatarUrl",
|
numJoinedMembers = 1u,
|
||||||
numJoinedMembers = 1u,
|
numActiveMembers = 1u,
|
||||||
numActiveMembers = 1u,
|
isDirect = false,
|
||||||
isDirect = false,
|
roomType = RoomType.Room,
|
||||||
roomType = RoomType.Room,
|
isHistoryWorldReadable = true,
|
||||||
isHistoryWorldReadable = true,
|
membership = membership,
|
||||||
membership = membership,
|
joinRule = joinRule,
|
||||||
joinRule = joinRule,
|
heroes = null,
|
||||||
heroes = null,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,12 @@ internal fun aRustSession(
|
||||||
proxy: SlidingSyncVersion = SlidingSyncVersion.NONE,
|
proxy: SlidingSyncVersion = SlidingSyncVersion.NONE,
|
||||||
accessToken: String = "accessToken",
|
accessToken: String = "accessToken",
|
||||||
refreshToken: String = "refreshToken",
|
refreshToken: String = "refreshToken",
|
||||||
): Session {
|
) = Session(
|
||||||
return Session(
|
accessToken = accessToken,
|
||||||
accessToken = accessToken,
|
refreshToken = refreshToken,
|
||||||
refreshToken = refreshToken,
|
userId = A_USER_ID.value,
|
||||||
userId = A_USER_ID.value,
|
deviceId = A_DEVICE_ID.value,
|
||||||
deviceId = A_DEVICE_ID.value,
|
homeserverUrl = A_HOMESERVER_URL,
|
||||||
homeserverUrl = A_HOMESERVER_URL,
|
oidcData = null,
|
||||||
oidcData = null,
|
slidingSyncVersion = proxy,
|
||||||
slidingSyncVersion = proxy,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import org.matrix.rustcomponents.sdk.RoomHero
|
||||||
import org.matrix.rustcomponents.sdk.RoomType
|
import org.matrix.rustcomponents.sdk.RoomType
|
||||||
import org.matrix.rustcomponents.sdk.SpaceRoom
|
import org.matrix.rustcomponents.sdk.SpaceRoom
|
||||||
|
|
||||||
fun aRustSpaceRoom(
|
internal fun aRustSpaceRoom(
|
||||||
roomId: RoomId = A_ROOM_ID,
|
roomId: RoomId = A_ROOM_ID,
|
||||||
isDirect: Boolean = false,
|
isDirect: Boolean = false,
|
||||||
canonicalAlias: String? = null,
|
canonicalAlias: String? = null,
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,13 @@ import org.matrix.rustcomponents.sdk.MessageType
|
||||||
import org.matrix.rustcomponents.sdk.TextMessageContent
|
import org.matrix.rustcomponents.sdk.TextMessageContent
|
||||||
import org.matrix.rustcomponents.sdk.TimelineEventContent
|
import org.matrix.rustcomponents.sdk.TimelineEventContent
|
||||||
|
|
||||||
fun aRustTimelineEventContentMessageLike(
|
internal fun aRustTimelineEventContentMessageLike(
|
||||||
content: MessageLikeEventContent = aRustMessageLikeEventContentRoomMessage(),
|
content: MessageLikeEventContent = aRustMessageLikeEventContentRoomMessage(),
|
||||||
): TimelineEventContent.MessageLike {
|
) = TimelineEventContent.MessageLike(
|
||||||
return TimelineEventContent.MessageLike(
|
content = content,
|
||||||
content = content,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun aRustMessageLikeEventContentRoomMessage(
|
internal fun aRustMessageLikeEventContentRoomMessage(
|
||||||
messageType: MessageType = aRustMessageTypeText(),
|
messageType: MessageType = aRustMessageTypeText(),
|
||||||
inReplyToEventId: String? = null,
|
inReplyToEventId: String? = null,
|
||||||
) = MessageLikeEventContent.RoomMessage(
|
) = MessageLikeEventContent.RoomMessage(
|
||||||
|
|
@ -31,13 +29,13 @@ fun aRustMessageLikeEventContentRoomMessage(
|
||||||
inReplyToEventId = inReplyToEventId,
|
inReplyToEventId = inReplyToEventId,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRustMessageTypeText(
|
internal fun aRustMessageTypeText(
|
||||||
content: TextMessageContent = aRustTextMessageContent(),
|
content: TextMessageContent = aRustTextMessageContent(),
|
||||||
) = MessageType.Text(
|
) = MessageType.Text(
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRustTextMessageContent(
|
internal fun aRustTextMessageContent(
|
||||||
body: String = A_MESSAGE,
|
body: String = A_MESSAGE,
|
||||||
formatted: FormattedBody? = null,
|
formatted: FormattedBody? = null,
|
||||||
) = TextMessageContent(
|
) = TextMessageContent(
|
||||||
|
|
@ -15,7 +15,9 @@ import org.matrix.rustcomponents.sdk.MsgLikeKind
|
||||||
import org.matrix.rustcomponents.sdk.TextMessageContent
|
import org.matrix.rustcomponents.sdk.TextMessageContent
|
||||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||||
|
|
||||||
fun aRustTimelineItemMessageContent(body: String = "Hello") = TimelineItemContent.MsgLike(
|
internal fun aRustTimelineItemContentMsgLike(
|
||||||
|
body: String = "Hello",
|
||||||
|
) = TimelineItemContent.MsgLike(
|
||||||
content = MsgLikeContent(
|
content = MsgLikeContent(
|
||||||
kind = MsgLikeKind.Message(
|
kind = MsgLikeKind.Message(
|
||||||
content = MessageContent(
|
content = MessageContent(
|
||||||
|
|
@ -19,14 +19,12 @@ internal fun aRustUnableToDecryptInfo(
|
||||||
userTrustsOwnIdentity: Boolean = false,
|
userTrustsOwnIdentity: Boolean = false,
|
||||||
senderHomeserver: String = "",
|
senderHomeserver: String = "",
|
||||||
ownHomeserver: String = "",
|
ownHomeserver: String = "",
|
||||||
): UnableToDecryptInfo {
|
) = UnableToDecryptInfo(
|
||||||
return UnableToDecryptInfo(
|
eventId = eventId,
|
||||||
eventId = eventId,
|
timeToDecryptMs = timeToDecryptMs,
|
||||||
timeToDecryptMs = timeToDecryptMs,
|
cause = cause,
|
||||||
cause = cause,
|
eventLocalAgeMillis = eventLocalAgeMillis,
|
||||||
eventLocalAgeMillis = eventLocalAgeMillis,
|
userTrustsOwnIdentity = userTrustsOwnIdentity,
|
||||||
userTrustsOwnIdentity = userTrustsOwnIdentity,
|
senderHomeserver = senderHomeserver,
|
||||||
senderHomeserver = senderHomeserver,
|
ownHomeserver = ownHomeserver,
|
||||||
ownHomeserver = ownHomeserver,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.impl.fixtures.factories
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import org.matrix.rustcomponents.sdk.UserProfile
|
import org.matrix.rustcomponents.sdk.UserProfile
|
||||||
|
|
||||||
fun aRustUserProfile(
|
internal fun aRustUserProfile(
|
||||||
userId: String = A_USER_ID.value,
|
userId: String = A_USER_ID.value,
|
||||||
displayName: String = "displayName",
|
displayName: String = "displayName",
|
||||||
avatarUrl: String = "avatarUrl",
|
avatarUrl: String = "avatarUrl",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import org.matrix.rustcomponents.sdk.ShieldState
|
||||||
|
|
||||||
class FakeFfiLazyTimelineItemProvider(
|
class FakeFfiLazyTimelineItemProvider(
|
||||||
private val debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(),
|
private val debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(),
|
||||||
private val shieldsState: ShieldState? = null,
|
private val shieldsState: ShieldState = ShieldState.None,
|
||||||
) : LazyTimelineItemProvider(NoHandle) {
|
) : LazyTimelineItemProvider(NoHandle) {
|
||||||
override fun getShields(strict: Boolean) = shieldsState
|
override fun getShields(strict: Boolean) = shieldsState
|
||||||
override fun debugInfo() = debugInfo
|
override fun debugInfo() = debugInfo
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.api.exception.NotificationResolverException
|
import io.element.android.libraries.matrix.api.exception.NotificationResolverException
|
||||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustBatchNotificationResult
|
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustBatchNotificationResultOk
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustNotificationEventTimeline
|
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustNotificationEventTimeline
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustNotificationItem
|
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustNotificationItem
|
||||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiNotificationClient
|
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiNotificationClient
|
||||||
|
|
@ -38,7 +38,7 @@ class RustNotificationServiceTest {
|
||||||
@Test
|
@Test
|
||||||
fun test() = runTest {
|
fun test() = runTest {
|
||||||
val notificationClient = FakeFfiNotificationClient(
|
val notificationClient = FakeFfiNotificationClient(
|
||||||
notificationItemResult = mapOf(AN_EVENT_ID.value to aRustBatchNotificationResult()),
|
notificationItemResult = mapOf(AN_EVENT_ID.value to aRustBatchNotificationResultOk()),
|
||||||
)
|
)
|
||||||
val sut = createRustNotificationService(
|
val sut = createRustNotificationService(
|
||||||
notificationClient = notificationClient,
|
notificationClient = notificationClient,
|
||||||
|
|
@ -66,10 +66,10 @@ class RustNotificationServiceTest {
|
||||||
}
|
}
|
||||||
val notificationClient = FakeFfiNotificationClient(
|
val notificationClient = FakeFfiNotificationClient(
|
||||||
notificationItemResult = mapOf(
|
notificationItemResult = mapOf(
|
||||||
AN_EVENT_ID.value to aRustBatchNotificationResult(
|
AN_EVENT_ID.value to aRustBatchNotificationResultOk(
|
||||||
notificationStatus = NotificationStatus.Event(aRustNotificationItem(aRustNotificationEventTimeline(faultyEvent)))
|
notificationStatus = NotificationStatus.Event(aRustNotificationItem(aRustNotificationEventTimeline(faultyEvent)))
|
||||||
),
|
),
|
||||||
AN_EVENT_ID_2.value to aRustBatchNotificationResult()
|
AN_EVENT_ID_2.value to aRustBatchNotificationResultOk()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val sut = createRustNotificationService(
|
val sut = createRustNotificationService(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
import io.element.android.libraries.matrix.api.room.RoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState
|
import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState
|
||||||
|
import io.element.android.libraries.matrix.api.room.SendQueueUpdate
|
||||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||||
|
|
@ -39,6 +40,7 @@ import io.element.android.tests.testutils.simulateLongTask
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -83,6 +85,8 @@ class FakeJoinedRoom(
|
||||||
private val updateJoinRuleResult: (JoinRule) -> Result<Unit> = { lambdaError() },
|
private val updateJoinRuleResult: (JoinRule) -> Result<Unit> = { lambdaError() },
|
||||||
private val setSendQueueEnabledResult: (Boolean) -> Unit = { _: Boolean -> },
|
private val setSendQueueEnabledResult: (Boolean) -> Unit = { _: Boolean -> },
|
||||||
) : JoinedRoom, BaseRoom by baseRoom {
|
) : JoinedRoom, BaseRoom by baseRoom {
|
||||||
|
private val sendQueueUpdates = MutableSharedFlow<SendQueueUpdate>(extraBufferCapacity = 10)
|
||||||
|
|
||||||
fun givenRoomMembersState(state: RoomMembersState) {
|
fun givenRoomMembersState(state: RoomMembersState) {
|
||||||
baseRoom.givenRoomMembersState(state)
|
baseRoom.givenRoomMembersState(state)
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +223,10 @@ class FakeJoinedRoom(
|
||||||
withdrawVerificationAndResendResult(userIds, sendHandle)
|
withdrawVerificationAndResendResult(userIds, sendHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun subscribeToSendQueueUpdates(): Flow<SendQueueUpdate> {
|
||||||
|
return sendQueueUpdates
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) {
|
private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) {
|
||||||
progressCallbackValues.forEach { (current, total) ->
|
progressCallbackValues.forEach { (current, total) ->
|
||||||
progressCallback?.onProgress(current, total)
|
progressCallback?.onProgress(current, total)
|
||||||
|
|
@ -229,4 +237,8 @@ class FakeJoinedRoom(
|
||||||
fun emitSyncUpdate() {
|
fun emitSyncUpdate() {
|
||||||
(syncUpdateFlow as MutableStateFlow).value = syncUpdateFlow.value + 1
|
(syncUpdateFlow as MutableStateFlow).value = syncUpdateFlow.value + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun givenSendQueueUpdate(sendQueueUpdate: SendQueueUpdate) {
|
||||||
|
sendQueueUpdates.emit(sendQueueUpdate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ dependencies {
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.jsoup)
|
implementation(libs.matrix.richtexteditor)
|
||||||
implementation(projects.libraries.previewutils)
|
implementation(projects.libraries.previewutils)
|
||||||
|
|
||||||
testCommonDependencies(libs, true)
|
testCommonDependencies(libs, true)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||||
import org.jsoup.Jsoup
|
import io.element.android.wysiwyg.utils.HtmlToDomParser
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -34,9 +34,9 @@ fun FormattedBody.toHtmlDocument(
|
||||||
?.trimEnd()
|
?.trimEnd()
|
||||||
?.let { formattedBody ->
|
?.let { formattedBody ->
|
||||||
val dom = if (prefix != null) {
|
val dom = if (prefix != null) {
|
||||||
Jsoup.parse("$prefix $formattedBody")
|
HtmlToDomParser.document("$prefix $formattedBody")
|
||||||
} else {
|
} else {
|
||||||
Jsoup.parse(formattedBody)
|
HtmlToDomParser.document(formattedBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepend `@` to mentions
|
// Prepend `@` to mentions
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,15 @@ private class PlainTextNodeVisitor : NodeVisitor {
|
||||||
private val builder = StringBuilder()
|
private val builder = StringBuilder()
|
||||||
|
|
||||||
override fun head(node: Node, depth: Int) {
|
override fun head(node: Node, depth: Int) {
|
||||||
if (node is TextNode && node.text().isNotBlank()) {
|
if (node is TextNode) {
|
||||||
builder.append(node.text())
|
// If the text node is blank, only add a single whitespace char if there wasn't already one
|
||||||
|
if (node.text().isBlank()) {
|
||||||
|
if (builder.lastOrNull()?.isWhitespace() == false) {
|
||||||
|
builder.append(" ")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.append(node.text())
|
||||||
|
}
|
||||||
} else if (node is Element && node.tagName() == "li") {
|
} else if (node is Element && node.tagName() == "li") {
|
||||||
val index = node.elementSiblingIndex() + 1
|
val index = node.elementSiblingIndex() + 1
|
||||||
val isOrdered = node.parent()?.nodeName()?.lowercase() == "ol"
|
val isOrdered = node.parent()?.nodeName()?.lowercase() == "ol"
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class ToPlainTextTest {
|
||||||
val formattedBody = FormattedBody(
|
val formattedBody = FormattedBody(
|
||||||
format = MessageFormat.HTML,
|
format = MessageFormat.HTML,
|
||||||
body = """
|
body = """
|
||||||
Hello world
|
Hello <strong>formatted</strong> <em>world</em>
|
||||||
<ul><li>This is an unordered list.</li></ul>
|
<ul><li>This is an unordered list.</li></ul>
|
||||||
<ol><li>This is an ordered list.</li></ol>
|
<ol><li>This is an ordered list.</li></ol>
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -53,7 +53,7 @@ class ToPlainTextTest {
|
||||||
)
|
)
|
||||||
assertThat(formattedBody.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo(
|
assertThat(formattedBody.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo(
|
||||||
"""
|
"""
|
||||||
Hello world
|
Hello formatted world
|
||||||
• This is an unordered list.
|
• This is an unordered list.
|
||||||
1. This is an ordered list.
|
1. This is an ordered list.
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,12 @@ interface MediaPlayer : AutoCloseable {
|
||||||
*/
|
*/
|
||||||
fun seekTo(positionMs: Long)
|
fun seekTo(positionMs: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the playback speed.
|
||||||
|
* @param speed The playback speed (e.g., 0.5f for half speed, 1.0f for normal, 2.0f for double speed)
|
||||||
|
*/
|
||||||
|
fun setPlaybackSpeed(speed: Float)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Releases any resources associated with this player.
|
* Releases any resources associated with this player.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,10 @@ class DefaultMediaPlayer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setPlaybackSpeed(speed: Float) {
|
||||||
|
player.setPlaybackSpeed(speed)
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
player.release()
|
player.release()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ interface SimplePlayer {
|
||||||
fun isPlaying(): Boolean
|
fun isPlaying(): Boolean
|
||||||
fun pause()
|
fun pause()
|
||||||
fun seekTo(positionMs: Long)
|
fun seekTo(positionMs: Long)
|
||||||
|
fun setPlaybackSpeed(speed: Float)
|
||||||
fun release()
|
fun release()
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onIsPlayingChanged(isPlaying: Boolean)
|
fun onIsPlayingChanged(isPlaying: Boolean)
|
||||||
|
|
@ -88,5 +89,9 @@ class DefaultSimplePlayer(
|
||||||
|
|
||||||
override fun seekTo(positionMs: Long) = p.seekTo(positionMs)
|
override fun seekTo(positionMs: Long) = p.seekTo(positionMs)
|
||||||
|
|
||||||
|
override fun setPlaybackSpeed(speed: Float) {
|
||||||
|
p.setPlaybackParameters(p.playbackParameters.withSpeed(speed))
|
||||||
|
}
|
||||||
|
|
||||||
override fun release() = p.release()
|
override fun release() = p.release()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class FakeSimplePlayer(
|
||||||
private val isPlayingLambda: () -> Boolean = { lambdaError() },
|
private val isPlayingLambda: () -> Boolean = { lambdaError() },
|
||||||
private val pauseLambda: () -> Unit = { lambdaError() },
|
private val pauseLambda: () -> Unit = { lambdaError() },
|
||||||
private val seekToLambda: (Long) -> Unit = { lambdaError() },
|
private val seekToLambda: (Long) -> Unit = { lambdaError() },
|
||||||
|
private val setPlaybackSpeedLambda: (Float) -> Unit = { lambdaError() },
|
||||||
private val releaseLambda: () -> Unit = { lambdaError() },
|
private val releaseLambda: () -> Unit = { lambdaError() },
|
||||||
) : SimplePlayer {
|
) : SimplePlayer {
|
||||||
private val listeners = mutableListOf<SimplePlayer.Listener>()
|
private val listeners = mutableListOf<SimplePlayer.Listener>()
|
||||||
|
|
@ -45,6 +46,7 @@ class FakeSimplePlayer(
|
||||||
override fun isPlaying() = isPlayingLambda()
|
override fun isPlaying() = isPlayingLambda()
|
||||||
override fun pause() = pauseLambda()
|
override fun pause() = pauseLambda()
|
||||||
override fun seekTo(positionMs: Long) = seekToLambda(positionMs)
|
override fun seekTo(positionMs: Long) = seekToLambda(positionMs)
|
||||||
|
override fun setPlaybackSpeed(speed: Float) = setPlaybackSpeedLambda(speed)
|
||||||
override fun release() = releaseLambda()
|
override fun release() = releaseLambda()
|
||||||
|
|
||||||
fun simulateIsPlayingChanged(isPlaying: Boolean) {
|
fun simulateIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,10 @@ class FakeMediaPlayer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setPlaybackSpeed(speed: Float) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
|
@ -38,6 +39,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.libraries.designsystem.atomic.atoms.PlaybackSpeedButton
|
||||||
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
||||||
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -50,7 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||||
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice
|
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent
|
||||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
|
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
|
||||||
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
|
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
|
||||||
|
|
@ -92,7 +94,7 @@ private fun VoiceInfoRow(
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
fun playPause() {
|
fun playPause() {
|
||||||
state.eventSink(VoiceMessageEvents.PlayPause)
|
state.eventSink(VoiceMessageEvent.PlayPause)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -112,21 +114,30 @@ private fun VoiceInfoRow(
|
||||||
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
when (state.button) {
|
when (state.buttonType) {
|
||||||
VoiceMessageState.Button.Play -> PlayButton(onClick = ::playPause)
|
VoiceMessageState.ButtonType.Play -> PlayButton(onClick = ::playPause)
|
||||||
VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause)
|
VoiceMessageState.ButtonType.Pause -> PauseButton(onClick = ::playPause)
|
||||||
VoiceMessageState.Button.Downloading -> ProgressButton()
|
VoiceMessageState.ButtonType.Downloading -> ProgressButton()
|
||||||
VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause)
|
VoiceMessageState.ButtonType.Retry -> RetryButton(onClick = ::playPause)
|
||||||
VoiceMessageState.Button.Disabled -> PlayButton(onClick = {}, enabled = false)
|
VoiceMessageState.ButtonType.Disabled -> PlayButton(onClick = {}, enabled = false)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text(
|
Column(
|
||||||
text = if (state.progress > 0f) state.time else voice.mediaInfo.duration ?: state.time,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
color = ElementTheme.colors.textSecondary,
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
style = ElementTheme.typography.fontBodyMdMedium,
|
) {
|
||||||
maxLines = 1,
|
PlaybackSpeedButton(
|
||||||
overflow = TextOverflow.Ellipsis,
|
speed = state.playbackSpeed,
|
||||||
)
|
onClick = { state.eventSink(VoiceMessageEvent.ChangePlaybackSpeed) },
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (state.progress > 0f) state.time else voice.mediaInfo.duration ?: state.time,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
WaveformPlaybackView(
|
WaveformPlaybackView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -136,7 +147,7 @@ private fun VoiceInfoRow(
|
||||||
playbackProgress = state.progress,
|
playbackProgress = state.progress,
|
||||||
waveform = voice.mediaInfo.waveform.orEmpty().toImmutableList(),
|
waveform = voice.mediaInfo.waveform.orEmpty().toImmutableList(),
|
||||||
onSeek = {
|
onSeek = {
|
||||||
state.eventSink(VoiceMessageEvents.Seek(it))
|
state.eventSink(VoiceMessageEvent.Seek(it))
|
||||||
},
|
},
|
||||||
seekEnabled = true,
|
seekEnabled = true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ class DefaultRoomGroupMessageCreator(
|
||||||
hasSmartReplyError = smartReplyErrors.isNotEmpty(),
|
hasSmartReplyError = smartReplyErrors.isNotEmpty(),
|
||||||
shouldBing = events.any { it.noisy },
|
shouldBing = events.any { it.noisy },
|
||||||
customSound = events.last().soundName,
|
customSound = events.last().soundName,
|
||||||
isUpdated = events.last().isUpdated,
|
isUpdated = events.last().let { it.isUpdated || it.outGoingMessage },
|
||||||
),
|
),
|
||||||
threadId = threadId,
|
threadId = threadId,
|
||||||
largeIcon = largeBitmap,
|
largeIcon = largeBitmap,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@
|
||||||
<string name="notification_room_invite_body_with_sender">"%1$s vous a invité à rejoindre le salon"</string>
|
<string name="notification_room_invite_body_with_sender">"%1$s vous a invité à rejoindre le salon"</string>
|
||||||
<string name="notification_sender_me">"Moi"</string>
|
<string name="notification_sender_me">"Moi"</string>
|
||||||
<string name="notification_sender_mention_reply">"%1$s mentionné ou en réponse"</string>
|
<string name="notification_sender_mention_reply">"%1$s mentionné ou en réponse"</string>
|
||||||
|
<string name="notification_space_invite_body">"Vous a invité à rejoindre l’espace"</string>
|
||||||
|
<string name="notification_space_invite_body_with_sender">"%1$s vous a invité à rejoindre l’espace"</string>
|
||||||
<string name="notification_test_push_notification_content">"Vous êtes en train de voir la notification ! Cliquez-moi !"</string>
|
<string name="notification_test_push_notification_content">"Vous êtes en train de voir la notification ! Cliquez-moi !"</string>
|
||||||
<string name="notification_thread_in_room">"Discussion dans %1$s"</string>
|
<string name="notification_thread_in_room">"Discussion dans %1$s"</string>
|
||||||
<string name="notification_ticker_text_dm">"%1$s : %2$s"</string>
|
<string name="notification_ticker_text_dm">"%1$s : %2$s"</string>
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,7 @@
|
||||||
<string name="action_static_map_load">"Cliquez pour charger la carte"</string>
|
<string name="action_static_map_load">"Cliquez pour charger la carte"</string>
|
||||||
<string name="action_take_photo">"Prendre une photo"</string>
|
<string name="action_take_photo">"Prendre une photo"</string>
|
||||||
<string name="action_tap_for_options">"Appuyez pour afficher les options"</string>
|
<string name="action_tap_for_options">"Appuyez pour afficher les options"</string>
|
||||||
|
<string name="action_translate">"Traduire"</string>
|
||||||
<string name="action_try_again">"Essayer à nouveau"</string>
|
<string name="action_try_again">"Essayer à nouveau"</string>
|
||||||
<string name="action_unpin">"Désépingler"</string>
|
<string name="action_unpin">"Désépingler"</string>
|
||||||
<string name="action_view">"Voir"</string>
|
<string name="action_view">"Voir"</string>
|
||||||
|
|
@ -235,6 +236,7 @@ Raison : %1$s."</string>
|
||||||
<string name="common_light">"Clair"</string>
|
<string name="common_light">"Clair"</string>
|
||||||
<string name="common_line_copied_to_clipboard">"Ligne copiée dans le presse-papiers"</string>
|
<string name="common_line_copied_to_clipboard">"Ligne copiée dans le presse-papiers"</string>
|
||||||
<string name="common_link_copied_to_clipboard">"Lien copié dans le presse-papiers"</string>
|
<string name="common_link_copied_to_clipboard">"Lien copié dans le presse-papiers"</string>
|
||||||
|
<string name="common_link_new_device">"Associer un nouvel appareil"</string>
|
||||||
<string name="common_loading">"Chargement…"</string>
|
<string name="common_loading">"Chargement…"</string>
|
||||||
<string name="common_loading_more">"Chargement…"</string>
|
<string name="common_loading_more">"Chargement…"</string>
|
||||||
<plurals name="common_many_members">
|
<plurals name="common_many_members">
|
||||||
|
|
@ -252,6 +254,7 @@ Raison : %1$s."</string>
|
||||||
<string name="common_message_removed">"Message supprimé"</string>
|
<string name="common_message_removed">"Message supprimé"</string>
|
||||||
<string name="common_modern">"Moderne"</string>
|
<string name="common_modern">"Moderne"</string>
|
||||||
<string name="common_mute">"Mettre en sourdine"</string>
|
<string name="common_mute">"Mettre en sourdine"</string>
|
||||||
|
<string name="common_name">"Nom"</string>
|
||||||
<string name="common_name_and_id">"%1$s (%2$s)"</string>
|
<string name="common_name_and_id">"%1$s (%2$s)"</string>
|
||||||
<string name="common_no_results">"Aucun résultat"</string>
|
<string name="common_no_results">"Aucun résultat"</string>
|
||||||
<string name="common_no_room_name">"Salon sans nom"</string>
|
<string name="common_no_room_name">"Salon sans nom"</string>
|
||||||
|
|
@ -326,6 +329,7 @@ Raison : %1$s."</string>
|
||||||
<string name="common_something_went_wrong">"Une erreur s’est produite"</string>
|
<string name="common_something_went_wrong">"Une erreur s’est produite"</string>
|
||||||
<string name="common_something_went_wrong_message">"Nous avons rencontré un problème. Veuillez réessayer."</string>
|
<string name="common_something_went_wrong_message">"Nous avons rencontré un problème. Veuillez réessayer."</string>
|
||||||
<string name="common_space">"Espace"</string>
|
<string name="common_space">"Espace"</string>
|
||||||
|
<string name="common_space_topic_placeholder">"Quel est le sujet de cet espace ?"</string>
|
||||||
<plurals name="common_spaces">
|
<plurals name="common_spaces">
|
||||||
<item quantity="one">"%1$d Espace"</item>
|
<item quantity="one">"%1$d Espace"</item>
|
||||||
<item quantity="other">"%1$d Espaces"</item>
|
<item quantity="other">"%1$d Espaces"</item>
|
||||||
|
|
@ -370,7 +374,7 @@ Raison : %1$s."</string>
|
||||||
<string name="common_waiting">"En attente…"</string>
|
<string name="common_waiting">"En attente…"</string>
|
||||||
<string name="common_waiting_for_decryption_key">"En attente de la clé de déchiffrement"</string>
|
<string name="common_waiting_for_decryption_key">"En attente de la clé de déchiffrement"</string>
|
||||||
<string name="common_you">"Vous"</string>
|
<string name="common_you">"Vous"</string>
|
||||||
<string name="crypto_history_visible">"Les messages que vous enverrez seront partagés avec les nouveaux membres invités dans ce salon. %1$s"</string>
|
<string name="crypto_history_visible">"Ce salon a été configuré pour que les nouveaux membres puissent lire l’historique. %1$s"</string>
|
||||||
<string name="crypto_identity_change_pin_violation">"L’identité de %1$s a été réinitialisée. %2$s"</string>
|
<string name="crypto_identity_change_pin_violation">"L’identité de %1$s a été réinitialisée. %2$s"</string>
|
||||||
<string name="crypto_identity_change_pin_violation_new">"L’identité de %1$s %2$s a été réinitialisée. %3$s"</string>
|
<string name="crypto_identity_change_pin_violation_new">"L’identité de %1$s %2$s a été réinitialisée. %3$s"</string>
|
||||||
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
|
<string name="crypto_identity_change_pin_violation_new_user_id">"(%1$s)"</string>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="a11y_edit_avatar">"Uredi avatar"</string>
|
<string name="a11y_edit_avatar">"Uredi avatar"</string>
|
||||||
<string name="a11y_edit_room_address_hint">"Potpuna adresa bit će %1$s"</string>
|
<string name="a11y_edit_room_address_hint">"Potpuna adresa bit će %1$s"</string>
|
||||||
<string name="a11y_encryption_details">"Pojedinosti šifriranja"</string>
|
<string name="a11y_encryption_details">"Pojedinosti o šifriranju"</string>
|
||||||
<string name="a11y_expand_message_text_field">"Proširi tekstno polje poruke"</string>
|
<string name="a11y_expand_message_text_field">"Proširi tekstno polje poruke"</string>
|
||||||
<string name="a11y_hide_password">"Sakrij zaporku"</string>
|
<string name="a11y_hide_password">"Sakrij zaporku"</string>
|
||||||
<string name="a11y_join_call">"Pridruži se pozivu"</string>
|
<string name="a11y_join_call">"Pridruži se pozivu"</string>
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
<string name="a11y_your_avatar">"Vaš avatar"</string>
|
<string name="a11y_your_avatar">"Vaš avatar"</string>
|
||||||
<string name="action_accept">"Prihvati"</string>
|
<string name="action_accept">"Prihvati"</string>
|
||||||
<string name="action_add_caption">"Dodaj opis"</string>
|
<string name="action_add_caption">"Dodaj opis"</string>
|
||||||
<string name="action_add_to_timeline">"Dodaj na vremensku crtu"</string>
|
<string name="action_add_to_timeline">"Dodaj na vremensku traku"</string>
|
||||||
<string name="action_back">"Natrag"</string>
|
<string name="action_back">"Natrag"</string>
|
||||||
<string name="action_call">"Poziv"</string>
|
<string name="action_call">"Poziv"</string>
|
||||||
<string name="action_cancel">"Odustani"</string>
|
<string name="action_cancel">"Odustani"</string>
|
||||||
|
|
@ -163,6 +163,7 @@
|
||||||
<string name="action_static_map_load">"Dodirnite za učitavanje karte"</string>
|
<string name="action_static_map_load">"Dodirnite za učitavanje karte"</string>
|
||||||
<string name="action_take_photo">"Uslikaj"</string>
|
<string name="action_take_photo">"Uslikaj"</string>
|
||||||
<string name="action_tap_for_options">"Dodirnite za mogućnosti"</string>
|
<string name="action_tap_for_options">"Dodirnite za mogućnosti"</string>
|
||||||
|
<string name="action_translate">"Prevedi"</string>
|
||||||
<string name="action_try_again">"Pokušajte ponovno"</string>
|
<string name="action_try_again">"Pokušajte ponovno"</string>
|
||||||
<string name="action_unpin">"Otkvači"</string>
|
<string name="action_unpin">"Otkvači"</string>
|
||||||
<string name="action_view">"Prikaz"</string>
|
<string name="action_view">"Prikaz"</string>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
|
|
||||||
package io.element.android.libraries.voiceplayer.api
|
package io.element.android.libraries.voiceplayer.api
|
||||||
|
|
||||||
sealed interface VoiceMessageEvents {
|
sealed interface VoiceMessageEvent {
|
||||||
data object PlayPause : VoiceMessageEvents
|
data object PlayPause : VoiceMessageEvent
|
||||||
data class Seek(val percentage: Float) : VoiceMessageEvents
|
data class Seek(val percentage: Float) : VoiceMessageEvent
|
||||||
|
data object ChangePlaybackSpeed : VoiceMessageEvent
|
||||||
}
|
}
|
||||||
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