Merge branch 'develop' into feature/fga/space_members_access
This commit is contained in:
8
app/proguard-rules.pro
vendored
8
app/proguard-rules.pro
vendored
@@ -66,10 +66,10 @@
|
||||
-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
|
||||
-keep class org.matrix.rustcomponents.sdk.** { *;}
|
||||
-keep class uniffi.** { *;}
|
||||
-keep class io.element.android.x.di.** { *; }
|
||||
-keep,allowshrinking class org.matrix.rustcomponents.sdk.** { *;}
|
||||
-keep,allowshrinking class uniffi.** { *;}
|
||||
-keep,allowshrinking class io.element.android.x.di.** { *; }
|
||||
-keepclasseswithmembernames,allowoptimization,allowshrinking class io.element.android.** { *; }
|
||||
|
||||
# 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.pushproviders.test)
|
||||
testImplementation(projects.features.forward.test)
|
||||
testImplementation(projects.features.messages.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.features.rageshake.test)
|
||||
testImplementation(projects.services.appnavstate.impl)
|
||||
|
||||
@@ -320,7 +320,7 @@ class RootFlowNode(
|
||||
is ResolvedIntent.Navigation -> {
|
||||
val openingRoomFromNotification = intent.getBooleanExtra(ROOM_OPENED_FROM_NOTIFICATION, false)
|
||||
if (openingRoomFromNotification && resolvedIntent.deeplinkData is DeeplinkData.Room) {
|
||||
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.NotificationTapOpensTimeline)
|
||||
analyticsService.startLongRunningTransaction(AnalyticsLongRunningTransaction.NotificationToMessage)
|
||||
}
|
||||
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.libraries.matrix.api.timeline.TimelineProvider
|
||||
import io.element.android.services.analytics.api.watchers.AnalyticsSendMessageWatcher
|
||||
|
||||
interface TimelineBindings {
|
||||
val timelineProvider: TimelineProvider
|
||||
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.ui.room.LoadingRoomState
|
||||
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.AnalyticsService
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -128,7 +128,7 @@ class RoomFlowNode(
|
||||
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
val parentTransaction = analyticsService.getLongRunningTransaction(NotificationTapOpensTimeline)
|
||||
val parentTransaction = analyticsService.getLongRunningTransaction(NotificationToMessage)
|
||||
val openRoomTransaction = analyticsService.startLongRunningTransaction(OpenRoom, parentTransaction)
|
||||
analyticsService.startLongRunningTransaction(LoadJoinedRoomFlow, openRoomTransaction)
|
||||
resolveRoomId()
|
||||
|
||||
@@ -98,6 +98,8 @@ class JoinedRoomLoadedFlowNode(
|
||||
private val callback: Callback = callback()
|
||||
override val graph = roomGraphFactory.create(inputs.room)
|
||||
|
||||
private val sendMessageWatcher = (graph as? TimelineBindings)?.analyticsSendMessageWatcher
|
||||
|
||||
// This is an ugly hack to check activity recreation
|
||||
private var currentActivity: Activity? = null
|
||||
|
||||
@@ -109,6 +111,7 @@ class JoinedRoomLoadedFlowNode(
|
||||
Timber.v("OnCreate => ${inputs.room.roomId}")
|
||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||
activeRoomsHolder.addRoom(inputs.room)
|
||||
sendMessageWatcher?.start()
|
||||
fetchRoomMembers()
|
||||
trackVisitedRoom()
|
||||
},
|
||||
@@ -120,6 +123,7 @@ class JoinedRoomLoadedFlowNode(
|
||||
},
|
||||
onDestroy = {
|
||||
Timber.v("OnDestroy")
|
||||
sendMessageWatcher?.stop()
|
||||
// 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
|
||||
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.google.common.truth.Truth.assertThat
|
||||
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.joined.FakeJoinedRoomLoadedFlowNodeCallback
|
||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||
import io.element.android.features.forward.api.ForwardEntryPoint
|
||||
import io.element.android.features.forward.test.FakeForwardEntryPoint
|
||||
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.space.api.SpaceEntryPoint
|
||||
import io.element.android.libraries.architecture.childNode
|
||||
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.FakeMatrixClient
|
||||
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.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.watchers.FakeAnalyticsSendMessageWatcher
|
||||
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
|
||||
import io.element.android.services.appnavstate.impl.DefaultActiveRoomsHolder
|
||||
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 {
|
||||
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(IntoMap::class)
|
||||
.addAnnotation(
|
||||
AnnotationSpec.Companion.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
|
||||
AnnotationSpec.builder(ClassName.bestGuess(nodeKeyFqName.asString())).addMember(
|
||||
CLASS_PLACEHOLDER,
|
||||
ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
||||
).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.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
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.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||
@@ -48,7 +46,6 @@ class HomePresenter(
|
||||
private val homeSpacesPresenter: Presenter<HomeSpacesState>,
|
||||
private val logoutPresenter: Presenter<DirectLogoutState>,
|
||||
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val sessionStore: SessionStore,
|
||||
private val announcementService: AnnouncementService,
|
||||
) : Presenter<HomeState> {
|
||||
@@ -69,9 +66,6 @@ class HomePresenter(
|
||||
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||
val roomListState = roomListPresenter.present()
|
||||
val homeSpacesState = homeSpacesPresenter.present()
|
||||
val isSpaceFeatureEnabled by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
|
||||
}.collectAsState(initial = false)
|
||||
var currentHomeNavigationBarItemOrdinal by rememberSaveable { mutableIntStateOf(HomeNavigationBarItem.Chats.ordinal) }
|
||||
val currentHomeNavigationBarItem by remember {
|
||||
derivedStateOf {
|
||||
@@ -117,7 +111,6 @@ class HomePresenter(
|
||||
snackbarMessage = snackbarMessage,
|
||||
canReportBug = canReportBug,
|
||||
directLogoutState = directLogoutState,
|
||||
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,10 +29,9 @@ data class HomeState(
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val canReportBug: Boolean,
|
||||
val directLogoutState: DirectLogoutState,
|
||||
val isSpaceFeatureEnabled: Boolean,
|
||||
val eventSink: (HomeEvents) -> Unit,
|
||||
) {
|
||||
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
|
||||
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(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
||||
aHomeState(
|
||||
isSpaceFeatureEnabled = true,
|
||||
roomListState = aRoomListState(
|
||||
// Add more rooms to see the blur effect under the NavigationBar
|
||||
contentState = aRoomsContentState(
|
||||
@@ -42,7 +41,6 @@ open class HomeStateProvider : PreviewParameterProvider<HomeState> {
|
||||
homeSpacesState = aHomeSpacesState(),
|
||||
),
|
||||
aHomeState(
|
||||
isSpaceFeatureEnabled = true,
|
||||
currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces,
|
||||
),
|
||||
) + RoomListStateProvider().values.map {
|
||||
@@ -60,7 +58,6 @@ internal fun aHomeState(
|
||||
roomListState: RoomListState = aRoomListState(),
|
||||
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
||||
canReportBug: Boolean = true,
|
||||
isSpaceFeatureEnabled: Boolean = false,
|
||||
directLogoutState: DirectLogoutState = aDirectLogoutState(),
|
||||
eventSink: (HomeEvents) -> Unit = {}
|
||||
) = HomeState(
|
||||
@@ -73,6 +70,5 @@ internal fun aHomeState(
|
||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||
roomListState = roomListState,
|
||||
homeSpacesState = homeSpacesState,
|
||||
isSpaceFeatureEnabled = isSpaceFeatureEnabled,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -179,14 +179,10 @@ private fun HomeScaffold(
|
||||
displayFilters = state.displayRoomListFilters,
|
||||
filtersState = roomListState.filtersState,
|
||||
canReportBug = state.canReportBug,
|
||||
modifier = if (state.isSpaceFeatureEnabled) {
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick(),
|
||||
)
|
||||
} else {
|
||||
Modifier.background(ElementTheme.colors.bgCanvasDefault)
|
||||
}
|
||||
modifier = Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick(),
|
||||
)
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
|
||||
@@ -10,6 +10,5 @@ package io.element.android.features.home.impl.search
|
||||
|
||||
sealed interface RoomListSearchEvents {
|
||||
data object ToggleSearchVisibility : RoomListSearchEvents
|
||||
data class QueryChanged(val query: String) : RoomListSearchEvents
|
||||
data object ClearQuery : RoomListSearchEvents
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -29,29 +31,24 @@ class RoomListSearchPresenter(
|
||||
var isSearchActive by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var searchQuery by remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val searchQuery = rememberTextFieldState()
|
||||
|
||||
LaunchedEffect(isSearchActive) {
|
||||
dataSource.setIsActive(isSearchActive)
|
||||
}
|
||||
|
||||
LaunchedEffect(searchQuery) {
|
||||
dataSource.setSearchQuery(searchQuery)
|
||||
LaunchedEffect(searchQuery.text) {
|
||||
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||
}
|
||||
|
||||
fun handleEvent(event: RoomListSearchEvents) {
|
||||
when (event) {
|
||||
RoomListSearchEvents.ClearQuery -> {
|
||||
searchQuery = ""
|
||||
}
|
||||
is RoomListSearchEvents.QueryChanged -> {
|
||||
searchQuery = event.query
|
||||
searchQuery.clearText()
|
||||
}
|
||||
RoomListSearchEvents.ToggleSearchVisibility -> {
|
||||
isSearchActive = !isSearchActive
|
||||
searchQuery = ""
|
||||
searchQuery.clearText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RoomListSearchState(
|
||||
val isSearchActive: Boolean,
|
||||
val query: String,
|
||||
val query: TextFieldState,
|
||||
val results: ImmutableList<RoomListRoomSummary>,
|
||||
val eventSink: (RoomListSearchEvents) -> Unit
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
package io.element.android.features.home.impl.search
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.home.impl.roomlist.aRoomListRoomSummaryList
|
||||
@@ -33,7 +34,7 @@ fun aRoomListSearchState(
|
||||
eventSink: (RoomListSearchEvents) -> Unit = { },
|
||||
) = RoomListSearchState(
|
||||
isSearchActive = isSearchActive,
|
||||
query = query,
|
||||
query = TextFieldState(initialText = query),
|
||||
results = results,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
@@ -18,16 +18,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.draw.drawBehind
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
@@ -112,23 +108,14 @@ private fun RoomListSearchContent(
|
||||
},
|
||||
navigationIcon = { BackButton(onClick = ::onBackButtonClick) },
|
||||
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
|
||||
var value by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(state.query))
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
FilledTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = value,
|
||||
singleLine = true,
|
||||
onValueChange = {
|
||||
value = it
|
||||
state.eventSink(RoomListSearchEvents.QueryChanged(it.text))
|
||||
},
|
||||
state = state.query,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
@@ -138,20 +125,18 @@ private fun RoomListSearchContent(
|
||||
disabledIndicatorColor = Color.Transparent,
|
||||
errorIndicatorColor = Color.Transparent,
|
||||
),
|
||||
trailingIcon = {
|
||||
if (value.text.isNotEmpty()) {
|
||||
IconButton(onClick = {
|
||||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
||||
// Clear local state too
|
||||
value = value.copy(text = "")
|
||||
}) {
|
||||
trailingIcon = if (state.query.text.isNotEmpty()) {
|
||||
@Composable {
|
||||
IconButton(onClick = { state.eventSink(RoomListSearchEvents.ClearQuery) }) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = stringResource(CommonStrings.action_cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
<string name="screen_roomlist_empty_title">"Još nema razgovora."</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.
|
||||
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_invites">"Pozivnice"</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_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_people">"Osobe"</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.libraries.architecture.Presenter
|
||||
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.test.FakeIndicatorService
|
||||
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_NAME
|
||||
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.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
@@ -54,8 +50,6 @@ class HomePresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private val isSpaceEnabled = FeatureFlags.Space.defaultValue(aBuildMeta())
|
||||
|
||||
@Test
|
||||
fun `present - should start with no user and then load user with success`() = runTest {
|
||||
val matrixClient = FakeMatrixClient(
|
||||
@@ -79,7 +73,6 @@ class HomePresenterTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
if (isSpaceEnabled) skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(
|
||||
MatrixUser(A_USER_ID, null, null)
|
||||
@@ -91,8 +84,7 @@ class HomePresenterTest {
|
||||
MatrixUser(A_USER_ID, A_USER_NAME, AN_AVATAR_URL)
|
||||
)
|
||||
assertThat(withUserState.showAvatarIndicator).isFalse()
|
||||
assertThat(withUserState.isSpaceFeatureEnabled).isEqualTo(isSpaceEnabled)
|
||||
assertThat(withUserState.showNavigationBar).isEqualTo(isSpaceEnabled)
|
||||
assertThat(withUserState.showNavigationBar).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
fun `present - show avatar indicator`() = runTest {
|
||||
val indicatorService = FakeIndicatorService()
|
||||
@@ -143,7 +118,6 @@ class HomePresenterTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
if (isSpaceEnabled) skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.showAvatarIndicator).isFalse()
|
||||
indicatorService.setShowRoomListTopBarIndicator(true)
|
||||
@@ -168,7 +142,6 @@ class HomePresenterTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
if (isSpaceEnabled) skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.currentUserAndNeighbors.first()).isEqualTo(MatrixUser(matrixClient.sessionId))
|
||||
// No new state is coming
|
||||
@@ -189,7 +162,6 @@ class HomePresenterTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
if (isSpaceEnabled) skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
||||
initialState.eventSink(HomeEvents.SelectHomeNavigationBarItem(HomeNavigationBarItem.Spaces))
|
||||
@@ -207,16 +179,12 @@ class HomePresenterTest {
|
||||
sessionStore = InMemorySessionStore(
|
||||
updateUserProfileResult = { _, _, _ -> },
|
||||
),
|
||||
featureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(FeatureFlags.Space.key to true),
|
||||
),
|
||||
homeSpacesPresenter = homeSpacesPresenter,
|
||||
announcementService = FakeAnnouncementService(
|
||||
showAnnouncementResult = {},
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.currentHomeNavigationBarItem).isEqualTo(HomeNavigationBarItem.Chats)
|
||||
assertThat(initialState.showNavigationBar).isTrue()
|
||||
@@ -241,7 +209,6 @@ internal fun createHomePresenter(
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) },
|
||||
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
homeSpacesPresenter: Presenter<HomeSpacesState> = Presenter { aHomeSpacesState() },
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
announcementService: AnnouncementService = FakeAnnouncementService(),
|
||||
@@ -250,11 +217,10 @@ internal fun createHomePresenter(
|
||||
syncService = syncService,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
indicatorService = indicatorService,
|
||||
logoutPresenter = { aDirectLogoutState() },
|
||||
roomListPresenter = { aRoomListState() },
|
||||
homeSpacesPresenter = homeSpacesPresenter,
|
||||
logoutPresenter = { aDirectLogoutState() },
|
||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||
featureFlagService = featureFlagService,
|
||||
sessionStore = sessionStore,
|
||||
announcementService = announcementService,
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ class RoomListSearchPresenterTest {
|
||||
}.test {
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.isSearchActive).isFalse()
|
||||
assertThat(state.query).isEmpty()
|
||||
assertThat(state.query.text.toString()).isEmpty()
|
||||
assertThat(state.results).isEmpty()
|
||||
}
|
||||
}
|
||||
@@ -72,10 +72,10 @@ class RoomListSearchPresenterTest {
|
||||
).isEqualTo(
|
||||
RoomListFilter.None
|
||||
)
|
||||
state.eventSink(RoomListSearchEvents.QueryChanged("Search"))
|
||||
state.query.edit { append("Search") }
|
||||
}
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.query).isEqualTo("Search")
|
||||
assertThat(state.query.text).isEqualTo("Search")
|
||||
assertThat(
|
||||
roomListService.allRooms.currentFilter.value
|
||||
).isEqualTo(
|
||||
@@ -84,7 +84,7 @@ class RoomListSearchPresenterTest {
|
||||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
||||
}
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.query).isEmpty()
|
||||
assertThat(state.query.text.toString()).isEmpty()
|
||||
assertThat(
|
||||
roomListService.allRooms.currentFilter.value
|
||||
).isEqualTo(
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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_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_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_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_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_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_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_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>
|
||||
@@ -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_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_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_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>
|
||||
|
||||
@@ -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_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_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_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>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_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_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_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>
|
||||
@@ -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_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_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_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>
|
||||
@@ -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_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_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_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>
|
||||
|
||||
@@ -231,7 +231,7 @@ private fun ImageOptimizationSelector(state: MediaOptimizationSelectorState) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f).align(Alignment.CenterVertically),
|
||||
text = stringResource(R.string.screen_media_upload_preview_optimize_image_quality_title),
|
||||
style = ElementTheme.materialTypography.bodyLarge,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
)
|
||||
Switch(
|
||||
modifier = Modifier.height(32.dp),
|
||||
@@ -337,7 +337,7 @@ private fun VideoQualitySelectorDialog(
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = preset.subtitle(),
|
||||
style = ElementTheme.materialTypography.bodyMedium,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
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.preferences.api.store.SessionPreferencesStore
|
||||
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.AnalyticsService
|
||||
import io.element.android.services.analytics.api.finishLongRunningTransaction
|
||||
@@ -205,7 +205,7 @@ class TimelinePresenter(
|
||||
}.start()
|
||||
is TimelineEvents.OnFocusEventRender -> {
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.model.event.TimelineItemVoiceContent
|
||||
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.preview.ElementPreview
|
||||
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.ui.strings.CommonStrings
|
||||
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.VoiceMessageStateProvider
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -64,26 +66,26 @@ fun TimelineItemVoiceView(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun playPause() {
|
||||
state.eventSink(VoiceMessageEvents.PlayPause)
|
||||
state.eventSink(VoiceMessageEvent.PlayPause)
|
||||
}
|
||||
|
||||
val a11y = stringResource(CommonStrings.common_voice_message)
|
||||
val a11yActionLabel = stringResource(
|
||||
when (state.button) {
|
||||
VoiceMessageState.Button.Play -> CommonStrings.a11y_play
|
||||
VoiceMessageState.Button.Pause -> CommonStrings.a11y_pause
|
||||
VoiceMessageState.Button.Downloading -> CommonStrings.common_downloading
|
||||
VoiceMessageState.Button.Retry -> CommonStrings.action_retry
|
||||
VoiceMessageState.Button.Disabled -> CommonStrings.error_unknown
|
||||
when (state.buttonType) {
|
||||
VoiceMessageState.ButtonType.Play -> CommonStrings.a11y_play
|
||||
VoiceMessageState.ButtonType.Pause -> CommonStrings.a11y_pause
|
||||
VoiceMessageState.ButtonType.Downloading -> CommonStrings.common_downloading
|
||||
VoiceMessageState.ButtonType.Retry -> CommonStrings.action_retry
|
||||
VoiceMessageState.ButtonType.Disabled -> CommonStrings.error_unknown
|
||||
}
|
||||
)
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clearAndSetSemantics {
|
||||
contentDescription = a11y
|
||||
if (state.button == VoiceMessageState.Button.Disabled) {
|
||||
if (state.buttonType == VoiceMessageState.ButtonType.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) {
|
||||
playPause()
|
||||
true
|
||||
@@ -101,30 +103,41 @@ fun TimelineItemVoiceView(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (!isTalkbackActive()) {
|
||||
when (state.button) {
|
||||
VoiceMessageState.Button.Play -> PlayButton(onClick = ::playPause)
|
||||
VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause)
|
||||
VoiceMessageState.Button.Downloading -> ProgressButton()
|
||||
VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause)
|
||||
VoiceMessageState.Button.Disabled -> PlayButton(onClick = {}, enabled = false)
|
||||
when (state.buttonType) {
|
||||
VoiceMessageState.ButtonType.Play -> PlayButton(onClick = ::playPause)
|
||||
VoiceMessageState.ButtonType.Pause -> PauseButton(onClick = ::playPause)
|
||||
VoiceMessageState.ButtonType.Downloading -> ProgressButton()
|
||||
VoiceMessageState.ButtonType.Retry -> RetryButton(onClick = ::playPause)
|
||||
VoiceMessageState.ButtonType.Disabled -> PlayButton(onClick = {}, enabled = false)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = state.time,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
PlaybackSpeedButton(
|
||||
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))
|
||||
WaveformPlaybackView(
|
||||
showCursor = state.showCursor,
|
||||
playbackProgress = state.progress,
|
||||
waveform = content.waveform,
|
||||
modifier = Modifier.height(34.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(34.dp),
|
||||
seekEnabled = !isTalkbackActive(),
|
||||
onSeek = { state.eventSink(VoiceMessageEvents.Seek(it)) },
|
||||
onSeek = { state.eventSink(VoiceMessageEvent.Seek(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ fun GroupHeaderView(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// 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)
|
||||
|
||||
Box(
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
package io.element.android.features.messages.impl.timeline.factories.event
|
||||
|
||||
import android.text.style.URLSpan
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.getSpans
|
||||
import androidx.core.text.toSpannable
|
||||
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.EmoteMessageType
|
||||
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.LocationMessageType
|
||||
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.OtherMessageType
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.jsoup.nodes.Document
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Inject
|
||||
@@ -60,7 +58,7 @@ class TimelineItemContentMessageFactory(
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val textPillificationHelper: TextPillificationHelper,
|
||||
) {
|
||||
suspend fun create(
|
||||
fun create(
|
||||
content: MessageContent,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
eventId: EventId?,
|
||||
@@ -68,26 +66,29 @@ class TimelineItemContentMessageFactory(
|
||||
return when (val messageType = content.type) {
|
||||
is EmoteMessageType -> {
|
||||
val emoteBody = "* $senderDisambiguatedDisplayName ${messageType.body.trimEnd()}"
|
||||
val formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisambiguatedDisplayName") ?: textPillificationHelper.pillify(
|
||||
emoteBody
|
||||
).safeLinkify()
|
||||
val dom = messageType.formatted?.toHtmlDocument(
|
||||
permalinkParser = permalinkParser,
|
||||
prefix = "* $senderDisambiguatedDisplayName",
|
||||
)
|
||||
val formattedBody = dom?.let(::parseHtml)
|
||||
?: textPillificationHelper.pillify(emoteBody).safeLinkify()
|
||||
TimelineItemEmoteContent(
|
||||
body = emoteBody,
|
||||
htmlDocument = messageType.formatted?.toHtmlDocument(
|
||||
permalinkParser = permalinkParser,
|
||||
prefix = "* $senderDisambiguatedDisplayName",
|
||||
),
|
||||
htmlDocument = dom,
|
||||
formattedBody = formattedBody,
|
||||
isEdited = content.isEdited,
|
||||
)
|
||||
}
|
||||
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)
|
||||
TimelineItemImageContent(
|
||||
filename = messageType.filename,
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
formattedCaption = formattedCaption,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
@@ -103,12 +104,15 @@ class TimelineItemContentMessageFactory(
|
||||
)
|
||||
}
|
||||
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)
|
||||
TimelineItemStickerContent(
|
||||
filename = messageType.filename,
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
formattedCaption = formattedCaption,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
@@ -140,12 +144,15 @@ class TimelineItemContentMessageFactory(
|
||||
}
|
||||
}
|
||||
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)
|
||||
TimelineItemVideoContent(
|
||||
filename = messageType.filename,
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
formattedCaption = formattedCaption,
|
||||
isEdited = content.isEdited,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mediaSource = messageType.source,
|
||||
@@ -162,11 +169,14 @@ class TimelineItemContentMessageFactory(
|
||||
)
|
||||
}
|
||||
is AudioMessageType -> {
|
||||
val dom = messageType.formattedCaption?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||
val formattedCaption = dom?.let(::parseHtml)
|
||||
?: messageType.caption?.withLinks()
|
||||
TimelineItemAudioContent(
|
||||
filename = messageType.filename,
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
formattedCaption = formattedCaption,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
@@ -176,12 +186,15 @@ class TimelineItemContentMessageFactory(
|
||||
)
|
||||
}
|
||||
is VoiceMessageType -> {
|
||||
val dom = messageType.formattedCaption?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||
val formattedCaption = dom?.let(::parseHtml)
|
||||
?: messageType.caption?.withLinks()
|
||||
TimelineItemVoiceContent(
|
||||
eventId = eventId,
|
||||
filename = messageType.filename,
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
formattedCaption = formattedCaption,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
@@ -192,12 +205,15 @@ class TimelineItemContentMessageFactory(
|
||||
)
|
||||
}
|
||||
is FileMessageType -> {
|
||||
val dom = messageType.formattedCaption?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||
val formattedCaption = dom?.let(::parseHtml)
|
||||
?: messageType.caption?.withLinks()
|
||||
val fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
|
||||
TimelineItemFileContent(
|
||||
filename = messageType.filename,
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = parseHtml(messageType.formattedCaption) ?: messageType.caption?.withLinks(),
|
||||
formattedCaption = formattedCaption,
|
||||
isEdited = content.isEdited,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mediaSource = messageType.source,
|
||||
@@ -208,9 +224,9 @@ class TimelineItemContentMessageFactory(
|
||||
}
|
||||
is NoticeMessageType -> {
|
||||
val body = messageType.body.trimEnd()
|
||||
val formattedBody = parseHtml(messageType.formatted) ?: textPillificationHelper.pillify(
|
||||
body
|
||||
).safeLinkify()
|
||||
val dom = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||
val formattedBody = dom?.let(::parseHtml)
|
||||
?: textPillificationHelper.pillify(body).safeLinkify()
|
||||
val htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||
TimelineItemNoticeContent(
|
||||
body = body,
|
||||
@@ -221,12 +237,13 @@ class TimelineItemContentMessageFactory(
|
||||
}
|
||||
is TextMessageType -> {
|
||||
val body = messageType.body.trimEnd()
|
||||
val formattedBody = parseHtml(messageType.formatted) ?: textPillificationHelper.pillify(
|
||||
body
|
||||
).safeLinkify()
|
||||
val dom = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||
val formattedBody = dom?.let(::parseHtml)
|
||||
?: textPillificationHelper.pillify(body).safeLinkify()
|
||||
val htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser)
|
||||
TimelineItemTextContent(
|
||||
body = body,
|
||||
htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser),
|
||||
htmlDocument = htmlDocument,
|
||||
formattedBody = formattedBody,
|
||||
isEdited = content.isEdited,
|
||||
)
|
||||
@@ -253,21 +270,11 @@ class TimelineItemContentMessageFactory(
|
||||
return result?.takeIf { it.isFinite() }
|
||||
}
|
||||
|
||||
private fun parseHtml(formattedBody: FormattedBody?, prefix: String? = null): CharSequence? {
|
||||
if (formattedBody == null || formattedBody.format != MessageFormat.HTML) return null
|
||||
val result = htmlConverterProvider.provide()
|
||||
.fromHtmlToSpans(formattedBody.body.trimEnd())
|
||||
private fun parseHtml(document: Document): CharSequence? {
|
||||
return htmlConverterProvider.provide()
|
||||
.fromDocumentToSpans(document)
|
||||
.let { textPillificationHelper.pillify(it) }
|
||||
.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.toImmutableList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.jsoup.nodes.Document
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -187,7 +188,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
}
|
||||
}.toSpannable()
|
||||
val sut = createTimelineItemContentMessageFactory(
|
||||
htmlConverterTransform = { expected }
|
||||
domConverterTransform = { expected }
|
||||
)
|
||||
val result = sut.create(
|
||||
content = createMessageContent(
|
||||
@@ -679,7 +680,7 @@ class TimelineItemContentMessageFactoryTest {
|
||||
}
|
||||
}.toSpannable()
|
||||
val sut = createTimelineItemContentMessageFactory(
|
||||
htmlConverterTransform = { expectedSpanned },
|
||||
domConverterTransform = { expectedSpanned },
|
||||
permalinkParser = FakePermalinkParser { PermalinkData.FallbackLink(Uri.EMPTY) }
|
||||
)
|
||||
val result = sut.create(
|
||||
@@ -765,11 +766,12 @@ class TimelineItemContentMessageFactoryTest {
|
||||
|
||||
private fun createTimelineItemContentMessageFactory(
|
||||
htmlConverterTransform: (String) -> CharSequence = { it },
|
||||
domConverterTransform: (Document) -> CharSequence = { it.body().html() },
|
||||
permalinkParser: FakePermalinkParser = FakePermalinkParser(),
|
||||
) = TimelineItemContentMessageFactory(
|
||||
fileSizeFormatter = FakeFileSizeFormatter(),
|
||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||
htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform),
|
||||
htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform, domConverterTransform),
|
||||
permalinkParser = permalinkParser,
|
||||
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 io.element.android.features.messages.api.timeline.HtmlConverterProvider
|
||||
import io.element.android.wysiwyg.utils.HtmlConverter
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
class FakeHtmlConverterProvider(
|
||||
private val transform: (String) -> CharSequence = { it },
|
||||
private val transformDom: (Document) -> CharSequence = { it.html() },
|
||||
) : HtmlConverterProvider {
|
||||
@Composable
|
||||
override fun Update() = Unit
|
||||
@@ -23,6 +25,10 @@ class FakeHtmlConverterProvider(
|
||||
override fun fromHtmlToSpans(html: String): CharSequence {
|
||||
return transform(html)
|
||||
}
|
||||
|
||||
override fun fromDocumentToSpans(dom: Document): CharSequence {
|
||||
return transformDom(dom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ private fun VideoQualitySelectorDialog(
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = ElementTheme.materialTypography.bodyMedium,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -404,7 +404,7 @@ private fun RoomHeaderSection(
|
||||
}.toImmutableList(),
|
||||
isTombstoned = isTombstoned,
|
||||
),
|
||||
contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_room_avatar) },
|
||||
contentDescription = stringResource(CommonStrings.a11y_room_avatar),
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
enabled = avatarUrl != null,
|
||||
|
||||
@@ -26,7 +26,7 @@ data class RoomMemberListState(
|
||||
val moderationState: RoomMemberModerationState,
|
||||
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 {
|
||||
|
||||
@@ -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_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_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_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_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.
|
||||
@@ -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_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_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_since_invite_option_title">"Les membres uniquement depuis qu’ils ont été invités"</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_invite_option_title">"Seulement les membres, depuis leur invitation"</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.
|
||||
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>
|
||||
|
||||
@@ -13,7 +13,8 @@ data class RoomMemberModerationPermissions(
|
||||
val canKick: 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 {
|
||||
val DEFAULT = RoomMemberModerationPermissions(
|
||||
|
||||
@@ -161,16 +161,27 @@ class RoomMemberModerationPresenter(
|
||||
val canModerateThisUser = currentUserPowerLevel > targetMemberPowerLevel
|
||||
// Assume the member is joined when it's unknown
|
||||
val membership = member?.membership ?: RoomMembershipState.JOIN
|
||||
if (permissions.canKick) {
|
||||
// Unban requires kick permission instead of a dedicated unban permission
|
||||
if (membership == RoomMembershipState.BAN) {
|
||||
add(ModerationActionState(action = ModerationAction.UnbanUser, isEnabled = canModerateThisUser))
|
||||
} else if (membership != RoomMembershipState.LEAVE) {
|
||||
add(ModerationActionState(action = ModerationAction.KickUser, isEnabled = canModerateThisUser))
|
||||
when (membership) {
|
||||
RoomMembershipState.BAN -> {
|
||||
if (permissions.canUnban) {
|
||||
add(ModerationActionState(action = ModerationAction.UnbanUser, 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()
|
||||
}
|
||||
|
||||
@@ -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_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_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_lost_recovery_key">"Izgubili ste ključ za oporavak?"</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_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_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_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_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.
|
||||
@@ -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_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_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_since_invite_option_title">"Les membres uniquement depuis qu’ils ont été invités"</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_invite_option_title">"Seulement les membres, depuis leur invitation"</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.
|
||||
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>
|
||||
|
||||
@@ -115,7 +115,7 @@ private fun SpaceInfoSection(
|
||||
Avatar(
|
||||
avatarData = AvatarData(roomId.value, name, avatarUrl, AvatarSize.SpaceListItem),
|
||||
avatarType = AvatarType.Space(),
|
||||
contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_avatar) },
|
||||
contentDescription = stringResource(CommonStrings.a11y_avatar),
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
|
||||
@@ -140,7 +140,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||
onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onShareSpace: () -> Unit = EnsureNeverCalled(),
|
||||
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
|
||||
onDetailsClick: () -> Unit = EnsureNeverCalled(),
|
||||
onSettingsClick: () -> Unit = EnsureNeverCalled(),
|
||||
onViewMembersClick: () -> Unit = EnsureNeverCalled(),
|
||||
acceptDeclineInviteView: @Composable () -> Unit = {},
|
||||
) {
|
||||
@@ -151,7 +151,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
|
||||
onRoomClick = onRoomClick,
|
||||
onShareSpace = onShareSpace,
|
||||
onLeaveSpaceClick = onLeaveSpaceClick,
|
||||
onSettingsClick = onDetailsClick,
|
||||
onSettingsClick = onSettingsClick,
|
||||
onViewMembersClick = onViewMembersClick,
|
||||
acceptDeclineInviteView = acceptDeclineInviteView,
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ fun UserProfileHeaderSection(
|
||||
Avatar(
|
||||
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
|
||||
avatarType = AvatarType.User,
|
||||
contentDescription = avatarUrl?.let { stringResource(CommonStrings.a11y_user_avatar) },
|
||||
contentDescription = stringResource(CommonStrings.a11y_user_avatar),
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.clickable(
|
||||
|
||||
@@ -22,7 +22,7 @@ camera = "1.5.2"
|
||||
work = "2.11.0"
|
||||
|
||||
# Compose
|
||||
compose_bom = "2025.12.00"
|
||||
compose_bom = "2025.12.01"
|
||||
|
||||
# Coroutines
|
||||
coroutines = "1.10.2"
|
||||
@@ -32,7 +32,7 @@ accompanist = "0.37.3"
|
||||
|
||||
# Test
|
||||
test_core = "1.7.0"
|
||||
roborazzi = "1.52.0"
|
||||
roborazzi = "1.55.0"
|
||||
|
||||
# Jetbrain
|
||||
datetime = "0.7.1"
|
||||
@@ -44,7 +44,7 @@ coil = "3.3.0"
|
||||
showkase = "1.0.5"
|
||||
appyx = "1.7.1"
|
||||
sqldelight = "2.2.1"
|
||||
wysiwyg = "2.40.0"
|
||||
wysiwyg = "2.41.0"
|
||||
telephoto = "0.18.0"
|
||||
haze = "1.7.1"
|
||||
|
||||
@@ -52,7 +52,7 @@ haze = "1.7.1"
|
||||
dependencyAnalysis = "3.5.1"
|
||||
|
||||
# DI
|
||||
metro = "0.9.2"
|
||||
metro = "0.9.3"
|
||||
|
||||
# Auto service
|
||||
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
|
||||
# All new features should not be implemented in the pull request that upgrades the version, developers should
|
||||
# only fix API breaks and may add some TODOs.
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.12.19"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.1.7"
|
||||
|
||||
# Others
|
||||
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" }
|
||||
sqlcipher = "net.zetetic:sqlcipher-android:4.12.0"
|
||||
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"
|
||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", 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"
|
||||
|
||||
# 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"
|
||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||
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.ContributesBinding
|
||||
import dev.zacsweers.metro.Provider
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Provides a Json instance configured to ignore unknown keys.
|
||||
*/
|
||||
typealias JsonProvider = Provider<Json>
|
||||
fun interface JsonProvider {
|
||||
operator fun invoke(): Json
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
|
||||
Binary file not shown.
@@ -17,13 +17,14 @@ import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.compoundTypography
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TypographyPreview() = ElementTheme {
|
||||
Surface {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
with(ElementTheme.materialTypography) {
|
||||
with(compoundTypography) {
|
||||
TypographyTokenPreview(displayLarge, "Display large")
|
||||
TypographyTokenPreview(displayMedium, "Display medium")
|
||||
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
|
||||
private fun TypographyTokenPreview(style: TextStyle, text: String) {
|
||||
Text(text = text, style = style)
|
||||
|
||||
@@ -62,14 +62,6 @@ object ElementTheme {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -8,18 +8,10 @@
|
||||
|
||||
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 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.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.TypographyTokens
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
@@ -32,35 +24,7 @@ class CompoundTypographyTest {
|
||||
@Config(sdk = [35], qualifiers = "h2048dp-xxhdpi")
|
||||
fun screenshots() {
|
||||
captureRoboImage(file = screenshotFile("Compound Typography.png")) {
|
||||
ElementTheme {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CompoundTypographyPreview()
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
modifier = modifier.toggleable(
|
||||
value = true,
|
||||
role = Role.Companion.Checkbox,
|
||||
role = Role.Checkbox,
|
||||
enabled = enabled,
|
||||
onValueChange = {},
|
||||
),
|
||||
|
||||
@@ -59,7 +59,7 @@ fun DmAvatars(
|
||||
Avatar(
|
||||
avatarData = userAvatarData,
|
||||
avatarType = AvatarType.User,
|
||||
contentDescription = userAvatarData.url?.let { stringResource(CommonStrings.a11y_your_avatar) },
|
||||
contentDescription = stringResource(CommonStrings.a11y_your_avatar),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.graphicsLayer {
|
||||
@@ -94,7 +94,7 @@ fun DmAvatars(
|
||||
Avatar(
|
||||
avatarData = otherUserAvatarData,
|
||||
avatarType = AvatarType.User,
|
||||
contentDescription = otherUserAvatarData.url?.let { stringResource(CommonStrings.a11y_other_user_avatar) },
|
||||
contentDescription = stringResource(CommonStrings.a11y_other_user_avatar),
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.clip(CircleShape)
|
||||
|
||||
@@ -36,7 +36,7 @@ internal fun ImageAvatar(
|
||||
SubcomposeAsyncImage(
|
||||
model = avatarData,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = ContentScale.Companion.Crop,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clip(avatarShape)
|
||||
|
||||
@@ -57,14 +57,14 @@ fun ErrorDialogWithDoNotShowAgain(
|
||||
Column {
|
||||
Text(
|
||||
text = content,
|
||||
style = ElementTheme.materialTypography.bodyMedium,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = doNotShowAgain, onCheckedChange = { doNotShowAgain = it })
|
||||
Text(
|
||||
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 {
|
||||
Text(
|
||||
text = content,
|
||||
style = ElementTheme.materialTypography.bodyMedium,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ internal fun SimpleAlertDialogContent(
|
||||
content = {
|
||||
Text(
|
||||
text = content,
|
||||
style = ElementTheme.materialTypography.bodyMedium,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
},
|
||||
submitText = submitText,
|
||||
|
||||
@@ -15,9 +15,15 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
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.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TextFieldLabelScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
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)
|
||||
@Composable
|
||||
internal fun FilledTextFieldLightPreview() =
|
||||
|
||||
@@ -114,7 +114,7 @@ fun ListItem(
|
||||
|
||||
val decoratedHeadlineContent: @Composable () -> Unit = {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides ElementTheme.materialTypography.bodyLarge,
|
||||
LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular,
|
||||
LocalContentColor provides headlineColor,
|
||||
) {
|
||||
headlineContent()
|
||||
@@ -123,7 +123,7 @@ fun ListItem(
|
||||
val decoratedSupportingContent: (@Composable () -> Unit)? = supportingContent?.let { content ->
|
||||
{
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides ElementTheme.materialTypography.bodyMedium,
|
||||
LocalTextStyle provides ElementTheme.typography.fontBodyMdRegular,
|
||||
LocalContentColor provides supportingColor,
|
||||
) {
|
||||
content()
|
||||
|
||||
@@ -50,6 +50,6 @@ object ElementNavigationBarItemDefaults {
|
||||
selectedTextColor = ElementTheme.colors.textPrimary,
|
||||
unselectedIconColor = ElementTheme.colors.iconTertiary,
|
||||
unselectedTextColor = ElementTheme.colors.textDisabled,
|
||||
selectedIndicatorColor = Color.Companion.Transparent,
|
||||
selectedIndicatorColor = Color.Transparent,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -70,12 +70,6 @@ enum class FeatureFlags(
|
||||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
Space(
|
||||
key = "feature.space",
|
||||
title = "Spaces",
|
||||
defaultValue = { true },
|
||||
isFinished = true,
|
||||
),
|
||||
SpaceSettings(
|
||||
key = "feature.spaceSettings",
|
||||
title = "Space settings",
|
||||
|
||||
@@ -25,10 +25,14 @@ class DefaultFeatureFlagService(
|
||||
private val featuresProvider: FeaturesProvider,
|
||||
) : FeatureFlagService {
|
||||
override fun isFeatureEnabledFlow(feature: Feature): Flow<Boolean> {
|
||||
return providers.filter { it.hasFeature(feature) }
|
||||
.maxByOrNull(FeatureFlagProvider::priority)
|
||||
?.isFeatureEnabledFlow(feature)
|
||||
?: flowOf(feature.defaultValue(buildMeta))
|
||||
return if (feature.isFinished) {
|
||||
flowOf(feature.defaultValue(buildMeta))
|
||||
} else {
|
||||
providers.filter { it.hasFeature(feature) }
|
||||
.maxByOrNull(FeatureFlagProvider::priority)
|
||||
?.isFeatureEnabledFlow(feature)
|
||||
?: flowOf(feature.defaultValue(buildMeta))
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
/**
|
||||
* 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.JoinedRoom
|
||||
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.join.JoinRule
|
||||
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.KnockRequestsListener
|
||||
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.TimelineFilter
|
||||
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 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.ShieldState
|
||||
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.EventTimelineItem as RustEventTimelineItem
|
||||
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
|
||||
@@ -58,7 +58,7 @@ class EventTimelineItemMapper(
|
||||
content = contentMapper.map(content),
|
||||
origin = origin?.map(),
|
||||
timelineItemDebugInfoProvider = { lazyProvider.debugInfo().map() },
|
||||
messageShieldProvider = { strict -> lazyProvider.getShields(strict)?.map() },
|
||||
messageShieldProvider = { strict -> lazyProvider.getShields(strict).map() },
|
||||
sendHandleProvider = { lazyProvider.getSendHandle()?.let(::RustSendHandle) }
|
||||
)
|
||||
}
|
||||
@@ -182,13 +182,13 @@ private fun ShieldState?.map(): MessageShield? {
|
||||
is ShieldState.Red -> true
|
||||
}
|
||||
return when (shieldStateCode) {
|
||||
ShieldStateCode.AUTHENTICITY_NOT_GUARANTEED -> MessageShield.AuthenticityNotGuaranteed(isCritical)
|
||||
ShieldStateCode.UNKNOWN_DEVICE -> MessageShield.UnknownDevice(isCritical)
|
||||
ShieldStateCode.UNSIGNED_DEVICE -> MessageShield.UnsignedDevice(isCritical)
|
||||
ShieldStateCode.UNVERIFIED_IDENTITY -> MessageShield.UnverifiedIdentity(isCritical)
|
||||
ShieldStateCode.SENT_IN_CLEAR -> MessageShield.SentInClear(isCritical)
|
||||
ShieldStateCode.VERIFICATION_VIOLATION -> MessageShield.VerificationViolation(isCritical)
|
||||
ShieldStateCode.MISMATCHED_SENDER -> MessageShield.MismatchedSender(isCritical)
|
||||
TimelineEventShieldStateCode.AUTHENTICITY_NOT_GUARANTEED -> MessageShield.AuthenticityNotGuaranteed(isCritical)
|
||||
TimelineEventShieldStateCode.UNKNOWN_DEVICE -> MessageShield.UnknownDevice(isCritical)
|
||||
TimelineEventShieldStateCode.UNSIGNED_DEVICE -> MessageShield.UnsignedDevice(isCritical)
|
||||
TimelineEventShieldStateCode.UNVERIFIED_IDENTITY -> MessageShield.UnverifiedIdentity(isCritical)
|
||||
TimelineEventShieldStateCode.SENT_IN_CLEAR -> MessageShield.SentInClear(isCritical)
|
||||
TimelineEventShieldStateCode.VERIFICATION_VIOLATION -> MessageShield.VerificationViolation(isCritical)
|
||||
TimelineEventShieldStateCode.MISMATCHED_SENDER -> MessageShield.MismatchedSender(isCritical)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,8 @@ class RustSessionVerificationService(
|
||||
// Listen for changes in verification status and update accordingly
|
||||
private val verificationStateListenerTaskHandle = encryptionService.verificationStateListener(object : VerificationStateListener {
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,22 +21,24 @@ import org.matrix.rustcomponents.sdk.ShieldState
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||
import uniffi.matrix_sdk_ui.EventItemOrigin
|
||||
|
||||
fun aRustEventTimelineItem(
|
||||
internal fun aRustEventTimelineItem(
|
||||
isRemote: Boolean = true,
|
||||
eventOrTransactionId: EventOrTransactionId = EventOrTransactionId.EventId(AN_EVENT_ID.value),
|
||||
sender: String = A_USER_ID.value,
|
||||
senderProfile: ProfileDetails = ProfileDetails.Unavailable,
|
||||
isOwn: Boolean = true,
|
||||
isEditable: Boolean = true,
|
||||
content: TimelineItemContent = aRustTimelineItemMessageContent(),
|
||||
content: TimelineItemContent = aRustTimelineItemContentMsgLike(),
|
||||
timestamp: ULong = 0uL,
|
||||
debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(),
|
||||
localSendState: EventSendState? = null,
|
||||
readReceipts: Map<String, Receipt> = emptyMap(),
|
||||
origin: EventItemOrigin? = EventItemOrigin.SYNC,
|
||||
canBeRepliedTo: Boolean = true,
|
||||
shieldsState: ShieldState? = null,
|
||||
shieldsState: ShieldState = ShieldState.None,
|
||||
localCreatedAt: ULong? = null,
|
||||
forwarder: String? = null,
|
||||
forwarderProfile: ProfileDetails? = null,
|
||||
) = EventTimelineItem(
|
||||
isRemote = isRemote,
|
||||
eventOrTransactionId = eventOrTransactionId,
|
||||
@@ -54,5 +56,7 @@ fun aRustEventTimelineItem(
|
||||
lazyProvider = FakeFfiLazyTimelineItemProvider(
|
||||
debugInfo = debugInfo,
|
||||
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
|
||||
|
||||
fun anEventTimelineItemDebugInfo(
|
||||
internal fun anEventTimelineItemDebugInfo(
|
||||
model: String = "model",
|
||||
originalJson: 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_USER_NAME
|
||||
import org.matrix.rustcomponents.sdk.Action
|
||||
import org.matrix.rustcomponents.sdk.BatchNotificationResult
|
||||
import org.matrix.rustcomponents.sdk.JoinRule
|
||||
import org.matrix.rustcomponents.sdk.NotificationEvent
|
||||
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.TimelineEvent
|
||||
|
||||
fun aRustNotificationItem(
|
||||
internal fun aRustNotificationItem(
|
||||
event: NotificationEvent = aRustNotificationEventTimeline(),
|
||||
senderInfo: NotificationSenderInfo = aRustNotificationSenderInfo(),
|
||||
roomInfo: NotificationRoomInfo = aRustNotificationRoomInfo(),
|
||||
@@ -39,13 +40,13 @@ fun aRustNotificationItem(
|
||||
actions = actions,
|
||||
)
|
||||
|
||||
fun aRustBatchNotificationResult(
|
||||
internal fun aRustBatchNotificationResultOk(
|
||||
notificationStatus: NotificationStatus = NotificationStatus.Event(aRustNotificationItem()),
|
||||
) = org.matrix.rustcomponents.sdk.BatchNotificationResult.Ok(
|
||||
) = BatchNotificationResult.Ok(
|
||||
status = notificationStatus,
|
||||
)
|
||||
|
||||
fun aRustNotificationSenderInfo(
|
||||
internal fun aRustNotificationSenderInfo(
|
||||
displayName: String? = A_USER_NAME,
|
||||
avatarUrl: String? = null,
|
||||
isNameAmbiguous: Boolean = false,
|
||||
@@ -55,7 +56,7 @@ fun aRustNotificationSenderInfo(
|
||||
isNameAmbiguous = isNameAmbiguous,
|
||||
)
|
||||
|
||||
fun aRustNotificationRoomInfo(
|
||||
internal fun aRustNotificationRoomInfo(
|
||||
displayName: String = A_ROOM_NAME,
|
||||
avatarUrl: String? = null,
|
||||
canonicalAlias: String? = null,
|
||||
@@ -77,7 +78,7 @@ fun aRustNotificationRoomInfo(
|
||||
isSpace = isSpace,
|
||||
)
|
||||
|
||||
fun aRustNotificationEventTimeline(
|
||||
internal fun aRustNotificationEventTimeline(
|
||||
event: TimelineEvent = FakeFfiTimelineEvent(),
|
||||
) = NotificationEvent.Timeline(
|
||||
event = event,
|
||||
|
||||
@@ -22,15 +22,13 @@ internal fun aRustRoomDescription(
|
||||
joinRule: PublicRoomJoinRule = PublicRoomJoinRule.PUBLIC,
|
||||
isWorldReadable: Boolean = true,
|
||||
joinedMembers: ULong = 2u,
|
||||
): RoomDescription {
|
||||
return RoomDescription(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
topic = topic,
|
||||
alias = alias,
|
||||
avatarUrl = avatarUrl,
|
||||
joinRule = joinRule,
|
||||
isWorldReadable = isWorldReadable,
|
||||
joinedMembers = joinedMembers,
|
||||
)
|
||||
}
|
||||
) = RoomDescription(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
topic = topic,
|
||||
alias = alias,
|
||||
avatarUrl = avatarUrl,
|
||||
joinRule = joinRule,
|
||||
isWorldReadable = isWorldReadable,
|
||||
joinedMembers = joinedMembers,
|
||||
)
|
||||
|
||||
@@ -14,10 +14,8 @@ import org.matrix.rustcomponents.sdk.RoomHero
|
||||
|
||||
internal fun aRustRoomHero(
|
||||
userId: UserId = A_USER_ID,
|
||||
): RoomHero {
|
||||
return RoomHero(
|
||||
userId = userId.value,
|
||||
displayName = "displayName",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
}
|
||||
) = RoomHero(
|
||||
userId = userId.value,
|
||||
displayName = "displayName",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.matrix.rustcomponents.sdk.RoomPowerLevels
|
||||
import org.matrix.rustcomponents.sdk.SuccessorRoom
|
||||
import uniffi.matrix_sdk_base.EncryptionState
|
||||
|
||||
fun aRustRoomInfo(
|
||||
internal fun aRustRoomInfo(
|
||||
id: String = A_ROOM_ID.value,
|
||||
displayName: 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 uniffi.matrix_sdk.RoomMemberRole
|
||||
|
||||
fun aRustRoomMember(
|
||||
internal fun aRustRoomMember(
|
||||
userId: UserId,
|
||||
displayName: 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.RoomNotificationSettings
|
||||
|
||||
fun aRustRoomNotificationSettings(
|
||||
internal fun aRustRoomNotificationSettings(
|
||||
mode: RoomNotificationMode = RoomNotificationMode.ALL_MESSAGES,
|
||||
isDefault: Boolean = true,
|
||||
) = RoomNotificationSettings(
|
||||
|
||||
@@ -19,20 +19,18 @@ internal fun aRustRoomPreviewInfo(
|
||||
canonicalAlias: String? = A_ROOM_ALIAS.value,
|
||||
membership: Membership? = Membership.JOINED,
|
||||
joinRule: JoinRule = JoinRule.Public,
|
||||
): RoomPreviewInfo {
|
||||
return RoomPreviewInfo(
|
||||
roomId = A_ROOM_ID.value,
|
||||
canonicalAlias = canonicalAlias,
|
||||
name = "name",
|
||||
topic = "topic",
|
||||
avatarUrl = "avatarUrl",
|
||||
numJoinedMembers = 1u,
|
||||
numActiveMembers = 1u,
|
||||
isDirect = false,
|
||||
roomType = RoomType.Room,
|
||||
isHistoryWorldReadable = true,
|
||||
membership = membership,
|
||||
joinRule = joinRule,
|
||||
heroes = null,
|
||||
)
|
||||
}
|
||||
) = RoomPreviewInfo(
|
||||
roomId = A_ROOM_ID.value,
|
||||
canonicalAlias = canonicalAlias,
|
||||
name = "name",
|
||||
topic = "topic",
|
||||
avatarUrl = "avatarUrl",
|
||||
numJoinedMembers = 1u,
|
||||
numActiveMembers = 1u,
|
||||
isDirect = false,
|
||||
roomType = RoomType.Room,
|
||||
isHistoryWorldReadable = true,
|
||||
membership = membership,
|
||||
joinRule = joinRule,
|
||||
heroes = null,
|
||||
)
|
||||
|
||||
@@ -18,14 +18,12 @@ internal fun aRustSession(
|
||||
proxy: SlidingSyncVersion = SlidingSyncVersion.NONE,
|
||||
accessToken: String = "accessToken",
|
||||
refreshToken: String = "refreshToken",
|
||||
): Session {
|
||||
return Session(
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
userId = A_USER_ID.value,
|
||||
deviceId = A_DEVICE_ID.value,
|
||||
homeserverUrl = A_HOMESERVER_URL,
|
||||
oidcData = null,
|
||||
slidingSyncVersion = proxy,
|
||||
)
|
||||
}
|
||||
) = Session(
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
userId = A_USER_ID.value,
|
||||
deviceId = A_DEVICE_ID.value,
|
||||
homeserverUrl = A_HOMESERVER_URL,
|
||||
oidcData = null,
|
||||
slidingSyncVersion = proxy,
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import org.matrix.rustcomponents.sdk.RoomHero
|
||||
import org.matrix.rustcomponents.sdk.RoomType
|
||||
import org.matrix.rustcomponents.sdk.SpaceRoom
|
||||
|
||||
fun aRustSpaceRoom(
|
||||
internal fun aRustSpaceRoom(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
isDirect: Boolean = false,
|
||||
canonicalAlias: String? = null,
|
||||
|
||||
@@ -15,15 +15,13 @@ import org.matrix.rustcomponents.sdk.MessageType
|
||||
import org.matrix.rustcomponents.sdk.TextMessageContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventContent
|
||||
|
||||
fun aRustTimelineEventContentMessageLike(
|
||||
internal fun aRustTimelineEventContentMessageLike(
|
||||
content: MessageLikeEventContent = aRustMessageLikeEventContentRoomMessage(),
|
||||
): TimelineEventContent.MessageLike {
|
||||
return TimelineEventContent.MessageLike(
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
) = TimelineEventContent.MessageLike(
|
||||
content = content,
|
||||
)
|
||||
|
||||
fun aRustMessageLikeEventContentRoomMessage(
|
||||
internal fun aRustMessageLikeEventContentRoomMessage(
|
||||
messageType: MessageType = aRustMessageTypeText(),
|
||||
inReplyToEventId: String? = null,
|
||||
) = MessageLikeEventContent.RoomMessage(
|
||||
@@ -31,13 +29,13 @@ fun aRustMessageLikeEventContentRoomMessage(
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
|
||||
fun aRustMessageTypeText(
|
||||
internal fun aRustMessageTypeText(
|
||||
content: TextMessageContent = aRustTextMessageContent(),
|
||||
) = MessageType.Text(
|
||||
content = content,
|
||||
)
|
||||
|
||||
fun aRustTextMessageContent(
|
||||
internal fun aRustTextMessageContent(
|
||||
body: String = A_MESSAGE,
|
||||
formatted: FormattedBody? = null,
|
||||
) = TextMessageContent(
|
||||
@@ -15,7 +15,9 @@ import org.matrix.rustcomponents.sdk.MsgLikeKind
|
||||
import org.matrix.rustcomponents.sdk.TextMessageContent
|
||||
import org.matrix.rustcomponents.sdk.TimelineItemContent
|
||||
|
||||
fun aRustTimelineItemMessageContent(body: String = "Hello") = TimelineItemContent.MsgLike(
|
||||
internal fun aRustTimelineItemContentMsgLike(
|
||||
body: String = "Hello",
|
||||
) = TimelineItemContent.MsgLike(
|
||||
content = MsgLikeContent(
|
||||
kind = MsgLikeKind.Message(
|
||||
content = MessageContent(
|
||||
@@ -19,14 +19,12 @@ internal fun aRustUnableToDecryptInfo(
|
||||
userTrustsOwnIdentity: Boolean = false,
|
||||
senderHomeserver: String = "",
|
||||
ownHomeserver: String = "",
|
||||
): UnableToDecryptInfo {
|
||||
return UnableToDecryptInfo(
|
||||
eventId = eventId,
|
||||
timeToDecryptMs = timeToDecryptMs,
|
||||
cause = cause,
|
||||
eventLocalAgeMillis = eventLocalAgeMillis,
|
||||
userTrustsOwnIdentity = userTrustsOwnIdentity,
|
||||
senderHomeserver = senderHomeserver,
|
||||
ownHomeserver = ownHomeserver,
|
||||
)
|
||||
}
|
||||
) = UnableToDecryptInfo(
|
||||
eventId = eventId,
|
||||
timeToDecryptMs = timeToDecryptMs,
|
||||
cause = cause,
|
||||
eventLocalAgeMillis = eventLocalAgeMillis,
|
||||
userTrustsOwnIdentity = userTrustsOwnIdentity,
|
||||
senderHomeserver = senderHomeserver,
|
||||
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 org.matrix.rustcomponents.sdk.UserProfile
|
||||
|
||||
fun aRustUserProfile(
|
||||
internal fun aRustUserProfile(
|
||||
userId: String = A_USER_ID.value,
|
||||
displayName: String = "displayName",
|
||||
avatarUrl: String = "avatarUrl",
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.matrix.rustcomponents.sdk.ShieldState
|
||||
|
||||
class FakeFfiLazyTimelineItemProvider(
|
||||
private val debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(),
|
||||
private val shieldsState: ShieldState? = null,
|
||||
private val shieldsState: ShieldState = ShieldState.None,
|
||||
) : LazyTimelineItemProvider(NoHandle) {
|
||||
override fun getShields(strict: Boolean) = shieldsState
|
||||
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.notification.NotificationContent
|
||||
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.aRustNotificationItem
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiNotificationClient
|
||||
@@ -38,7 +38,7 @@ class RustNotificationServiceTest {
|
||||
@Test
|
||||
fun test() = runTest {
|
||||
val notificationClient = FakeFfiNotificationClient(
|
||||
notificationItemResult = mapOf(AN_EVENT_ID.value to aRustBatchNotificationResult()),
|
||||
notificationItemResult = mapOf(AN_EVENT_ID.value to aRustBatchNotificationResultOk()),
|
||||
)
|
||||
val sut = createRustNotificationService(
|
||||
notificationClient = notificationClient,
|
||||
@@ -66,10 +66,10 @@ class RustNotificationServiceTest {
|
||||
}
|
||||
val notificationClient = FakeFfiNotificationClient(
|
||||
notificationItemResult = mapOf(
|
||||
AN_EVENT_ID.value to aRustBatchNotificationResult(
|
||||
AN_EVENT_ID.value to aRustBatchNotificationResultOk(
|
||||
notificationStatus = NotificationStatus.Event(aRustNotificationItem(aRustNotificationEventTimeline(faultyEvent)))
|
||||
),
|
||||
AN_EVENT_ID_2.value to aRustBatchNotificationResult()
|
||||
AN_EVENT_ID_2.value to aRustBatchNotificationResultOk()
|
||||
),
|
||||
)
|
||||
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.RoomMembersState
|
||||
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.join.JoinRule
|
||||
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.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
@@ -83,6 +85,8 @@ class FakeJoinedRoom(
|
||||
private val updateJoinRuleResult: (JoinRule) -> Result<Unit> = { lambdaError() },
|
||||
private val setSendQueueEnabledResult: (Boolean) -> Unit = { _: Boolean -> },
|
||||
) : JoinedRoom, BaseRoom by baseRoom {
|
||||
private val sendQueueUpdates = MutableSharedFlow<SendQueueUpdate>(extraBufferCapacity = 10)
|
||||
|
||||
fun givenRoomMembersState(state: RoomMembersState) {
|
||||
baseRoom.givenRoomMembersState(state)
|
||||
}
|
||||
@@ -219,6 +223,10 @@ class FakeJoinedRoom(
|
||||
withdrawVerificationAndResendResult(userIds, sendHandle)
|
||||
}
|
||||
|
||||
override fun subscribeToSendQueueUpdates(): Flow<SendQueueUpdate> {
|
||||
return sendQueueUpdates
|
||||
}
|
||||
|
||||
private suspend fun simulateSendMediaProgress(progressCallback: ProgressCallback?) {
|
||||
progressCallbackValues.forEach { (current, total) ->
|
||||
progressCallback?.onProgress(current, total)
|
||||
@@ -229,4 +237,8 @@ class FakeJoinedRoom(
|
||||
fun emitSyncUpdate() {
|
||||
(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.testtags)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.jsoup)
|
||||
implementation(libs.matrix.richtexteditor)
|
||||
implementation(projects.libraries.previewutils)
|
||||
|
||||
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.timeline.item.event.FormattedBody
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -34,9 +34,9 @@ fun FormattedBody.toHtmlDocument(
|
||||
?.trimEnd()
|
||||
?.let { formattedBody ->
|
||||
val dom = if (prefix != null) {
|
||||
Jsoup.parse("$prefix $formattedBody")
|
||||
HtmlToDomParser.document("$prefix $formattedBody")
|
||||
} else {
|
||||
Jsoup.parse(formattedBody)
|
||||
HtmlToDomParser.document(formattedBody)
|
||||
}
|
||||
|
||||
// Prepend `@` to mentions
|
||||
|
||||
@@ -55,8 +55,15 @@ private class PlainTextNodeVisitor : NodeVisitor {
|
||||
private val builder = StringBuilder()
|
||||
|
||||
override fun head(node: Node, depth: Int) {
|
||||
if (node is TextNode && node.text().isNotBlank()) {
|
||||
builder.append(node.text())
|
||||
if (node is TextNode) {
|
||||
// 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") {
|
||||
val index = node.elementSiblingIndex() + 1
|
||||
val isOrdered = node.parent()?.nodeName()?.lowercase() == "ol"
|
||||
|
||||
@@ -45,7 +45,7 @@ class ToPlainTextTest {
|
||||
val formattedBody = FormattedBody(
|
||||
format = MessageFormat.HTML,
|
||||
body = """
|
||||
Hello world
|
||||
Hello <strong>formatted</strong> <em>world</em>
|
||||
<ul><li>This is an unordered list.</li></ul>
|
||||
<ol><li>This is an ordered list.</li></ol>
|
||||
<br />
|
||||
@@ -53,7 +53,7 @@ class ToPlainTextTest {
|
||||
)
|
||||
assertThat(formattedBody.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo(
|
||||
"""
|
||||
Hello world
|
||||
Hello formatted world
|
||||
• This is an unordered list.
|
||||
1. This is an ordered list.
|
||||
""".trimIndent()
|
||||
|
||||
@@ -47,6 +47,12 @@ interface MediaPlayer : AutoCloseable {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -159,6 +159,10 @@ class DefaultMediaPlayer(
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPlaybackSpeed(speed: Float) {
|
||||
player.setPlaybackSpeed(speed)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
player.release()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ interface SimplePlayer {
|
||||
fun isPlaying(): Boolean
|
||||
fun pause()
|
||||
fun seekTo(positionMs: Long)
|
||||
fun setPlaybackSpeed(speed: Float)
|
||||
fun release()
|
||||
interface Listener {
|
||||
fun onIsPlayingChanged(isPlaying: Boolean)
|
||||
@@ -88,5 +89,9 @@ class DefaultSimplePlayer(
|
||||
|
||||
override fun seekTo(positionMs: Long) = p.seekTo(positionMs)
|
||||
|
||||
override fun setPlaybackSpeed(speed: Float) {
|
||||
p.setPlaybackParameters(p.playbackParameters.withSpeed(speed))
|
||||
}
|
||||
|
||||
override fun release() = p.release()
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class FakeSimplePlayer(
|
||||
private val isPlayingLambda: () -> Boolean = { lambdaError() },
|
||||
private val pauseLambda: () -> Unit = { lambdaError() },
|
||||
private val seekToLambda: (Long) -> Unit = { lambdaError() },
|
||||
private val setPlaybackSpeedLambda: (Float) -> Unit = { lambdaError() },
|
||||
private val releaseLambda: () -> Unit = { lambdaError() },
|
||||
) : SimplePlayer {
|
||||
private val listeners = mutableListOf<SimplePlayer.Listener>()
|
||||
@@ -45,6 +46,7 @@ class FakeSimplePlayer(
|
||||
override fun isPlaying() = isPlayingLambda()
|
||||
override fun pause() = pauseLambda()
|
||||
override fun seekTo(positionMs: Long) = seekToLambda(positionMs)
|
||||
override fun setPlaybackSpeed(speed: Float) = setPlaybackSpeedLambda(speed)
|
||||
override fun release() = releaseLambda()
|
||||
|
||||
fun simulateIsPlayingChanged(isPlaying: Boolean) {
|
||||
|
||||
@@ -96,6 +96,10 @@ class FakeMediaPlayer(
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPlaybackSpeed(speed: Float) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -38,6 +39,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaybackSpeedButton
|
||||
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
||||
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
||||
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.aMediaItemVoice
|
||||
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.VoiceMessageStateProvider
|
||||
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
|
||||
@@ -92,7 +94,7 @@ private fun VoiceInfoRow(
|
||||
onLongClick: () -> Unit,
|
||||
) {
|
||||
fun playPause() {
|
||||
state.eventSink(VoiceMessageEvents.PlayPause)
|
||||
state.eventSink(VoiceMessageEvent.PlayPause)
|
||||
}
|
||||
|
||||
Row(
|
||||
@@ -112,21 +114,30 @@ private fun VoiceInfoRow(
|
||||
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
when (state.button) {
|
||||
VoiceMessageState.Button.Play -> PlayButton(onClick = ::playPause)
|
||||
VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause)
|
||||
VoiceMessageState.Button.Downloading -> ProgressButton()
|
||||
VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause)
|
||||
VoiceMessageState.Button.Disabled -> PlayButton(onClick = {}, enabled = false)
|
||||
when (state.buttonType) {
|
||||
VoiceMessageState.ButtonType.Play -> PlayButton(onClick = ::playPause)
|
||||
VoiceMessageState.ButtonType.Pause -> PauseButton(onClick = ::playPause)
|
||||
VoiceMessageState.ButtonType.Downloading -> ProgressButton()
|
||||
VoiceMessageState.ButtonType.Retry -> RetryButton(onClick = ::playPause)
|
||||
VoiceMessageState.ButtonType.Disabled -> PlayButton(onClick = {}, enabled = false)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
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,
|
||||
)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
PlaybackSpeedButton(
|
||||
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))
|
||||
WaveformPlaybackView(
|
||||
modifier = Modifier
|
||||
@@ -136,7 +147,7 @@ private fun VoiceInfoRow(
|
||||
playbackProgress = state.progress,
|
||||
waveform = voice.mediaInfo.waveform.orEmpty().toImmutableList(),
|
||||
onSeek = {
|
||||
state.eventSink(VoiceMessageEvents.Seek(it))
|
||||
state.eventSink(VoiceMessageEvent.Seek(it))
|
||||
},
|
||||
seekEnabled = true,
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ class DefaultRoomGroupMessageCreator(
|
||||
hasSmartReplyError = smartReplyErrors.isNotEmpty(),
|
||||
shouldBing = events.any { it.noisy },
|
||||
customSound = events.last().soundName,
|
||||
isUpdated = events.last().isUpdated,
|
||||
isUpdated = events.last().let { it.isUpdated || it.outGoingMessage },
|
||||
),
|
||||
threadId = threadId,
|
||||
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_sender_me">"Moi"</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_thread_in_room">"Discussion dans %1$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_take_photo">"Prendre une photo"</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_unpin">"Désépingler"</string>
|
||||
<string name="action_view">"Voir"</string>
|
||||
@@ -235,6 +236,7 @@ Raison : %1$s."</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_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_more">"Chargement…"</string>
|
||||
<plurals name="common_many_members">
|
||||
@@ -252,6 +254,7 @@ Raison : %1$s."</string>
|
||||
<string name="common_message_removed">"Message supprimé"</string>
|
||||
<string name="common_modern">"Moderne"</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_no_results">"Aucun résultat"</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_message">"Nous avons rencontré un problème. Veuillez réessayer."</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">
|
||||
<item quantity="one">"%1$d Espace"</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_for_decryption_key">"En attente de la clé de déchiffrement"</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_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>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</plurals>
|
||||
<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_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_hide_password">"Sakrij zaporku"</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="action_accept">"Prihvati"</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_call">"Poziv"</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_take_photo">"Uslikaj"</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_unpin">"Otkvači"</string>
|
||||
<string name="action_view">"Prikaz"</string>
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
package io.element.android.libraries.voiceplayer.api
|
||||
|
||||
sealed interface VoiceMessageEvents {
|
||||
data object PlayPause : VoiceMessageEvents
|
||||
data class Seek(val percentage: Float) : VoiceMessageEvents
|
||||
sealed interface VoiceMessageEvent {
|
||||
data object PlayPause : VoiceMessageEvent
|
||||
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
Reference in New Issue
Block a user