Merge branch 'develop' into feature/fga/space_settings_iteration

This commit is contained in:
ganfra
2025-12-15 16:06:06 +01:00
600 changed files with 3591 additions and 2388 deletions

View File

@@ -36,7 +36,7 @@ jobs:
./tools/localazy/importSupportedLocalesFromLocalazy.py
./tools/test/generateAllScreenshots.py
- name: Create Pull Request for Strings
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
with:
token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
commit-message: Sync Strings from Localazy

View File

@@ -23,7 +23,7 @@ jobs:
- name: Run SAS String script
run: ./tools/sas/import_sas_strings.py
- name: Create Pull Request for SAS Strings
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
with:
commit-message: Sync SAS Strings
title: Sync SAS Strings

View File

@@ -83,7 +83,7 @@ jobs:
# https://github.com/codecov/codecov-action
- name: ☂️ Upload coverage reports to codecov
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -199,6 +199,10 @@ android {
resources.pickFirsts += setOf(
"META-INF/versions/9/OSGI-INF/MANIFEST.MF",
)
jniLibs {
useLegacyPackaging = project.findProperty("useLegacyPackaging")?.toString()?.toBoolean()
}
}
}

View File

@@ -69,4 +69,4 @@
-keep class org.matrix.rustcomponents.sdk.** { *;}
-keep class uniffi.** { *;}
-keep class io.element.android.x.di.** { *; }
-keepnames class io.element.android.x.**
-keepclasseswithmembernames,allowoptimization,allowshrinking class io.element.android.** { *; }

View File

@@ -13,4 +13,5 @@ object LearnMoreConfig {
const val DEVICE_VERIFICATION_URL: String = "https://element.io/help#encryption-device-verification"
const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5"
const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
const val HISTORY_VISIBLE_URL: String = "https://element.io/en/help#e2ee-history-sharing"
}

View File

@@ -8,7 +8,9 @@
package io.element.android.appnav.room.joined
import android.app.Activity
import android.os.Parcelable
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
@@ -96,6 +98,9 @@ class JoinedRoomLoadedFlowNode(
private val callback: Callback = callback()
override val graph = roomGraphFactory.create(inputs.room)
// This is an ugly hack to check activity recreation
private var currentActivity: Activity? = null
init {
lifecycle.subscribe(
onCreate = {
@@ -115,8 +120,12 @@ class JoinedRoomLoadedFlowNode(
},
onDestroy = {
Timber.v("OnDestroy")
activeRoomsHolder.removeRoom(inputs.room.sessionId, inputs.room.roomId)
inputs.room.destroy()
// 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) {
activeRoomsHolder.removeRoom(inputs.room.sessionId, inputs.room.roomId)
inputs.room.destroy()
}
appNavigationStateService.onLeavingRoom(id)
}
)
@@ -289,6 +298,8 @@ class JoinedRoomLoadedFlowNode(
@Composable
override fun View(modifier: Modifier) {
currentActivity = LocalActivity.current
BackstackView()
}
}

View File

@@ -46,7 +46,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml"))
}
dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.4.28")
detektPlugins("io.nlopez.compose.rules:detekt:0.5.1")
detektPlugins(project(":tests:detekt-rules"))
}

View File

@@ -81,7 +81,7 @@ private fun SpaceAnnouncementHeader(
showBetaLabel = true,
subTitle = stringResource(id = R.string.screen_space_announcement_subtitle),
iconStyle = BigIcon.Style.Default(
vectorIcon = CompoundIcons.WorkspaceSolid(),
vectorIcon = CompoundIcons.SpaceSolid(),
usePrimaryTint = true,
),
)

View File

@@ -40,7 +40,7 @@ import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEf
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.toImmutableList
@@ -132,7 +132,7 @@ class ConfigureRoomPresenter(
cameraPhotoPicker.launch()
} else {
pendingPermissionRequest = true
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
}
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
}

View File

@@ -19,7 +19,7 @@ import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@@ -58,7 +58,7 @@ class NotificationsOptInPresenter(
if (notificationsPermissionsState.permissionGranted) {
callback.onNotificationsOptInFinished()
} else {
notificationsPermissionsState.eventSink(PermissionsEvents.RequestPermissions)
notificationsPermissionsState.eventSink(PermissionsEvent.RequestPermissions)
}
}
NotificationsOptInEvents.NotNowClicked -> {

View File

@@ -28,7 +28,7 @@ enum class HomeNavigationBarItem(
isSelected: Boolean,
) = when (this) {
Chats -> if (isSelected) CompoundIcons.ChatSolid() else CompoundIcons.Chat()
Spaces -> if (isSelected) CompoundIcons.WorkspaceSolid() else CompoundIcons.Workspace()
Spaces -> if (isSelected) CompoundIcons.SpaceSolid() else CompoundIcons.Space()
}
companion object {

View File

@@ -121,7 +121,6 @@ internal fun RoomSummaryRow(
) {
NameAndTimestampRow(
name = room.name,
latestEvent = room.latestEvent,
timestamp = room.timestamp,
isHighlighted = room.isHighlighted
)
@@ -138,7 +137,6 @@ internal fun RoomSummaryRow(
) {
NameAndTimestampRow(
name = room.name,
latestEvent = room.latestEvent,
timestamp = null,
isHighlighted = room.isHighlighted
)
@@ -214,7 +212,6 @@ private fun RoomSummaryScaffoldRow(
@Composable
private fun NameAndTimestampRow(
name: String?,
latestEvent: LatestEvent,
timestamp: String?,
isHighlighted: Boolean,
modifier: Modifier = Modifier
@@ -236,28 +233,6 @@ private fun NameAndTimestampRow(
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Picto
when (latestEvent) {
is LatestEvent.Sending -> {
Spacer(modifier = Modifier.width(4.dp))
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.Time(),
contentDescription = null,
tint = ElementTheme.colors.iconTertiary,
)
}
is LatestEvent.Error -> {
Spacer(modifier = Modifier.width(4.dp))
Icon(
modifier = Modifier.size(16.dp),
imageVector = CompoundIcons.ErrorSolid(),
contentDescription = null,
tint = ElementTheme.colors.iconCriticalPrimary,
)
}
else -> Unit
}
}
// Timestamp
Text(
@@ -302,7 +277,6 @@ private fun MessagePreviewAndIndicatorRow(
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = spacedBy(28.dp)
) {
if (room.isTombstoned) {
Text(
@@ -316,6 +290,16 @@ private fun MessagePreviewAndIndicatorRow(
)
} else {
if (room.latestEvent is LatestEvent.Error) {
Icon(
modifier = Modifier
.padding(top = 2.dp)
.size(16.dp),
imageVector = CompoundIcons.ErrorSolid(),
// The last message contains the error.
contentDescription = null,
tint = ElementTheme.colors.iconCriticalPrimary,
)
Spacer(modifier = Modifier.width(6.dp))
Text(
modifier = Modifier.weight(1f),
text = stringResource(CommonStrings.common_message_failed_to_send),
@@ -326,6 +310,17 @@ private fun MessagePreviewAndIndicatorRow(
overflow = TextOverflow.Ellipsis,
)
} else {
if (room.latestEvent is LatestEvent.Sending) {
Icon(
modifier = Modifier
.padding(top = 2.dp)
.size(16.dp),
imageVector = CompoundIcons.Time(),
contentDescription = stringResource(CommonStrings.common_sending),
tint = ElementTheme.colors.iconTertiary,
)
Spacer(modifier = Modifier.width(6.dp))
}
val messagePreview = room.latestEvent.content()
val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.orEmpty().toString())
Text(
@@ -339,7 +334,7 @@ private fun MessagePreviewAndIndicatorRow(
)
}
}
Spacer(modifier = Modifier.width(16.dp))
// Call and unread
Row(
modifier = Modifier

View File

@@ -71,7 +71,7 @@ class DefaultPinCodeManager(
lockScreenStore.onWrongPin()
}
}
} catch (failure: Throwable) {
} catch (_: Throwable) {
false
}
}

View File

@@ -18,7 +18,7 @@ import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
@Inject
@@ -46,7 +46,7 @@ class QrCodeIntroPresenter(
canContinue = true
} else {
pendingPermissionRequest = true
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
}
}
}

View File

@@ -106,7 +106,7 @@ private fun Content(
QrCodeCameraView(
modifier = Modifier.fillMaxSize(),
onScanQrCode = { state.eventSink.invoke(QrCodeScanEvents.QrCodeScanned(it)) },
renderPreview = state.isScanning,
isScanning = state.isScanning,
)
}
}

View File

@@ -68,6 +68,7 @@ dependencies {
implementation(libs.jsoup)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.constraintlayout.compose)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
implementation(libs.sigpwned.emoji4j)

View File

@@ -30,6 +30,7 @@ import io.element.android.features.messages.api.timeline.HtmlConverterProvider
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
import io.element.android.features.messages.impl.link.LinkState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvent
@@ -101,6 +102,7 @@ class MessagesPresenter(
@Assisted private val timelinePresenter: Presenter<TimelineState>,
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
private val historyVisibleStatePresenter: Presenter<HistoryVisibleState>,
private val linkPresenter: Presenter<LinkState>,
@Assisted private val actionListPresenter: Presenter<ActionListState>,
private val customReactionPresenter: Presenter<CustomReactionState>,
@@ -152,6 +154,7 @@ class MessagesPresenter(
val timelineState = timelinePresenter.present()
val timelineProtectionState = timelineProtectionPresenter.present()
val identityChangeState = identityChangeStatePresenter.present()
val historyVisibleState = historyVisibleStatePresenter.present()
val actionListState = actionListPresenter.present()
val linkState = linkPresenter.present()
val customReactionState = customReactionPresenter.present()
@@ -274,6 +277,7 @@ class MessagesPresenter(
timelineState = timelineState,
timelineProtectionState = timelineProtectionState,
identityChangeState = identityChangeState,
historyVisibleState = historyVisibleState,
linkState = linkState,
actionListState = actionListState,
customReactionState = customReactionState,

View File

@@ -10,6 +10,7 @@ package io.element.android.features.messages.impl
import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
import io.element.android.features.messages.impl.link.LinkState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
@@ -40,6 +41,7 @@ data class MessagesState(
val timelineState: TimelineState,
val timelineProtectionState: TimelineProtectionState,
val identityChangeState: IdentityChangeState,
val historyVisibleState: HistoryVisibleState,
val linkState: LinkState,
val actionListState: ActionListState,
val customReactionState: CustomReactionState,

View File

@@ -14,7 +14,10 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessagePreviewState
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.anActionListState
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
import io.element.android.features.messages.impl.crypto.historyvisible.aHistoryVisibleState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
import io.element.android.features.messages.impl.crypto.identity.aRoomMemberIdentityStateChange
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
import io.element.android.features.messages.impl.link.LinkState
import io.element.android.features.messages.impl.link.aLinkState
@@ -38,6 +41,7 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr
import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.features.roomcall.api.aStandByCallState
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -48,6 +52,7 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -83,6 +88,19 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
timelineItems = aTimelineItemList(aTimelineItemTextContent()),
)
),
aMessagesState(
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange()))
),
aMessagesState(
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
historyVisibleState = aHistoryVisibleState(showAlert = true)
),
aMessagesState(
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange())),
historyVisibleState = aHistoryVisibleState(showAlert = true)
)
)
}
@@ -103,6 +121,7 @@ fun aMessagesState(
),
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
identityChangeState: IdentityChangeState = anIdentityChangeState(),
historyVisibleState: HistoryVisibleState = aHistoryVisibleState(),
linkState: LinkState = aLinkState(),
readReceiptBottomSheetState: ReadReceiptBottomSheetState = aReadReceiptBottomSheetState(),
actionListState: ActionListState = anActionListState(),
@@ -125,6 +144,7 @@ fun aMessagesState(
voiceMessageComposerState = voiceMessageComposerState,
timelineProtectionState = timelineProtectionState,
identityChangeState = identityChangeState,
historyVisibleState = historyVisibleState,
linkState = linkState,
timelineState = timelineState,
readReceiptBottomSheetState = readReceiptBottomSheetState,
@@ -145,11 +165,9 @@ fun aMessagesState(
)
fun aRoomMemberModerationState(
canKick: Boolean = false,
canBan: Boolean = false,
permissions: RoomMemberModerationPermissions = RoomMemberModerationPermissions.DEFAULT,
) = object : RoomMemberModerationState {
override val canKick: Boolean = canKick
override val canBan: Boolean = canBan
override val permissions: RoomMemberModerationPermissions = permissions
override val eventSink: (RoomMemberModerationEvents) -> Unit = {}
}

View File

@@ -53,6 +53,7 @@ import io.element.android.features.messages.api.timeline.voicemessages.composer.
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListView
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleStateView
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStateView
import io.element.android.features.messages.impl.link.LinkEvents
import io.element.android.features.messages.impl.link.LinkView
@@ -486,10 +487,17 @@ private fun MessagesViewComposerBottomSheetContents(
// Do not show the identity change if user is composing a Rich message or is seeing suggestion(s).
if (state.composerState.suggestions.isEmpty() &&
state.composerState.textEditorState is TextEditorState.Markdown) {
IdentityChangeStateView(
state = state.identityChangeState,
onLinkClick = onLinkClick,
)
if (state.identityChangeState.roomMemberIdentityStateChanges.isNotEmpty()) {
IdentityChangeStateView(
state = state.identityChangeState,
onLinkClick = onLinkClick,
)
} else {
HistoryVisibleStateView(
state = state.historyVisibleState,
onLinkClick = onLinkClick,
)
}
}
val verificationViolation = state.identityChangeState.roomMemberIdentityStateChanges.firstOrNull {
it.identityState == IdentityState.VerificationViolation

View File

@@ -0,0 +1,48 @@
/*
* 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.impl.crypto.historyvisible
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
interface HistoryVisibleAcknowledgementRepository {
fun hasAcknowledged(roomId: RoomId): Flow<Boolean>
suspend fun setAcknowledged(roomId: RoomId, value: Boolean)
}
@ContributesBinding(SessionScope::class)
class DefaultHistoryVisibleAcknowledgementRepository(
sessionId: SessionId,
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : HistoryVisibleAcknowledgementRepository {
val store =
sessionId.value.hash().take(16).let { hash ->
preferenceDataStoreFactory.create("elementx_historyvisible_$hash")
}
override fun hasAcknowledged(roomId: RoomId): Flow<Boolean> {
return store.data.map { prefs ->
val acknowledged = prefs[booleanPreferencesKey(roomId.value)] ?: false
acknowledged
}
}
override suspend fun setAcknowledged(roomId: RoomId, value: Boolean) {
store.edit { prefs ->
prefs[booleanPreferencesKey(roomId.value)] = value
}
}
}

View File

@@ -0,0 +1,12 @@
/*
* 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.impl.crypto.historyvisible
sealed interface HistoryVisibleEvent {
data object Acknowledge : HistoryVisibleEvent
}

View File

@@ -0,0 +1,13 @@
/*
* 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.impl.crypto.historyvisible
data class HistoryVisibleState(
val showAlert: Boolean,
val eventSink: (HistoryVisibleEvent) -> Unit,
)

View File

@@ -0,0 +1,62 @@
/*
* 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.impl.crypto.historyvisible
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@Inject
class HistoryVisibleStatePresenter(
private val featureFlagService: FeatureFlagService,
private val repository: HistoryVisibleAcknowledgementRepository,
private val room: JoinedRoom,
) : Presenter<HistoryVisibleState> {
@Composable
override fun present(): HistoryVisibleState {
val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false)
val roomInfo by room.roomInfoFlow.collectAsState()
// Implicitly assume the alert is initially acknowledged to avoid flashes in UI.
val acknowledged by repository.hasAcknowledged(room.roomId).collectAsState(initial = true)
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(roomInfo.historyVisibility, acknowledged) {
if (roomInfo.historyVisibility == RoomHistoryVisibility.Joined && acknowledged) {
repository.setAcknowledged(room.roomId, false)
}
}
fun handleEvent(event: HistoryVisibleEvent) {
when (event) {
is HistoryVisibleEvent.Acknowledge -> coroutineScope.setAcknowledged(room.roomId, true)
}
}
return HistoryVisibleState(
showAlert = isFeatureEnabled && roomInfo.historyVisibility != RoomHistoryVisibility.Joined && roomInfo.isEncrypted == true && !acknowledged,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.setAcknowledged(roomId: RoomId, value: Boolean) = launch {
repository.setAcknowledged(roomId, value)
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.impl.crypto.historyvisible
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
class HistoryVisibleStateProvider : PreviewParameterProvider<HistoryVisibleState> {
override val values: Sequence<HistoryVisibleState>
get() = sequenceOf(
aHistoryVisibleState(showAlert = true),
)
}
internal fun aHistoryVisibleState(
showAlert: Boolean = false,
eventSink: (HistoryVisibleEvent) -> Unit = {},
) = HistoryVisibleState(
showAlert,
eventSink = eventSink,
)

View File

@@ -0,0 +1,81 @@
/*
* 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.impl.crypto.historyvisible
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.appconfig.LearnMoreConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertLevel
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun HistoryVisibleStateView(
state: HistoryVisibleState,
onLinkClick: (String, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
if (!state.showAlert) {
return
}
ComposerAlertMolecule(
modifier = modifier,
avatar = null,
showIcon = true,
level = ComposerAlertLevel.Info,
content = buildAnnotatedString {
val learnMoreStr = stringResource(CommonStrings.action_learn_more)
val fullText = stringResource(CommonStrings.crypto_history_visible, learnMoreStr)
append(fullText)
val learnMoreStartIndex = fullText.lastIndexOf(learnMoreStr)
addStyle(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
fontWeight = FontWeight.Bold,
color = ElementTheme.colors.textPrimary
),
start = learnMoreStartIndex,
end = learnMoreStartIndex + learnMoreStr.length,
)
addLink(
url = LinkAnnotation.Url(
url = LearnMoreConfig.HISTORY_VISIBLE_URL,
linkInteractionListener = {
onLinkClick(LearnMoreConfig.HISTORY_VISIBLE_URL, true)
}
),
start = learnMoreStartIndex,
end = learnMoreStartIndex + learnMoreStr.length,
)
},
submitText = stringResource(CommonStrings.action_dismiss),
onSubmitClick = { state.eventSink(HistoryVisibleEvent.Acknowledge) },
)
}
@PreviewsDayNight
@Composable
internal fun HistoryVisibleStateViewPreview(
@PreviewParameter(HistoryVisibleStateProvider::class) state: HistoryVisibleState,
) = ElementPreview {
HistoryVisibleStateView(
state = state,
onLinkClick = { _, _ -> },
)
}

View File

@@ -0,0 +1,42 @@
/*
* 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.impl.crypto.historyvisible
import androidx.compose.runtime.Composable
import io.element.android.features.messages.impl.MessagesView
import io.element.android.features.messages.impl.aMessagesState
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
@PreviewsDayNight
@Composable
internal fun MessagesViewWithHistoryVisiblePreview() = ElementPreview {
MessagesView(
state = aMessagesState(
composerState = aMessageComposerState(
textEditorState = aTextEditorStateMarkdown(
initialText = "",
initialFocus = false,
)
),
historyVisibleState = aHistoryVisibleState(showAlert = true),
),
onBackClick = {},
onRoomDetailsClick = {},
onEventContentClick = { _, _ -> false },
onUserDataClick = {},
onLinkClick = { _, _ -> },
onSendLocationClick = {},
onCreatePollClick = {},
onJoinCallClick = {},
onViewAllPinnedMessagesClick = {},
knockRequestsBannerView = {}
)
}

View File

@@ -11,6 +11,8 @@ package io.element.android.features.messages.impl.di
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.Binds
import dev.zacsweers.metro.ContributesTo
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleState
import io.element.android.features.messages.impl.crypto.historyvisible.HistoryVisibleStatePresenter
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeStatePresenter
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailurePresenter
@@ -61,4 +63,7 @@ interface MessagesBindsModule {
@Binds
fun bindIdentityChangeStatePresenter(presenter: IdentityChangeStatePresenter): Presenter<IdentityChangeState>
@Binds
fun bindHistoryVisibleStatePresenter(presenter: HistoryVisibleStatePresenter): Presenter<HistoryVisibleState>
}

View File

@@ -64,7 +64,7 @@ import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
@@ -286,7 +286,7 @@ class MessageComposerPresenter(
cameraPhotoPicker.launch()
} else {
pendingEvent = event
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
}
}
MessageComposerEvent.PickAttachmentSource.VideoFromCamera -> localCoroutineScope.launch {
@@ -295,7 +295,7 @@ class MessageComposerPresenter(
cameraVideoPicker.launch()
} else {
pendingEvent = event
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
}
}
MessageComposerEvent.PickAttachmentSource.Location -> {

View File

@@ -112,7 +112,7 @@ fun EventDebugInfoView(
private fun prettyJSON(maybeJSON: String): String {
return try {
JSONObject(maybeJSON).toString(2)
} catch (e: JSONException) {
} catch (_: JSONException) {
// Prefer not pretty-printing over crashing if the data is not actually JSON
maybeJSON
}

View File

@@ -33,7 +33,7 @@ import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaupload.api.MediaSenderFactory
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
@@ -111,7 +111,7 @@ class DefaultVoiceMessageComposerPresenter(
}
else -> {
Timber.i("Voice message permission needed")
permissionState.eventSink(PermissionsEvents.RequestPermissions)
permissionState.eventSink(PermissionsEvent.RequestPermissions)
}
}
}
@@ -176,10 +176,10 @@ class DefaultVoiceMessageComposerPresenter(
localCoroutineScope.deleteRecording()
}
VoiceMessageComposerEvent.DismissPermissionsRationale -> {
permissionState.eventSink(PermissionsEvents.CloseDialog)
permissionState.eventSink(PermissionsEvent.CloseDialog)
}
VoiceMessageComposerEvent.AcceptPermissionRationale -> {
permissionState.eventSink(PermissionsEvents.OpenSystemSettingAndCloseDialog)
permissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
}
is VoiceMessageComposerEvent.LifecycleEvent -> handleLifecycleEvent(event.event)
VoiceMessageComposerEvent.DismissSendFailureDialog -> {

View File

@@ -17,6 +17,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.anActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.crypto.historyvisible.aHistoryVisibleState
import io.element.android.features.messages.impl.crypto.identity.anIdentityChangeState
import io.element.android.features.messages.impl.fixtures.aMessageEvent
import io.element.android.features.messages.impl.link.aLinkState
@@ -1297,6 +1298,7 @@ class MessagesPresenterTest {
timelinePresenter = { aTimelineState(eventSink = timelineEventSink) },
timelineProtectionPresenter = { aTimelineProtectionState() },
identityChangeStatePresenter = { anIdentityChangeState() },
historyVisibleStatePresenter = { aHistoryVisibleState() },
linkPresenter = { aLinkState() },
actionListPresenter = { anActionListState(eventSink = actionListEventSink) },
customReactionPresenter = { aCustomReactionState() },

View File

@@ -0,0 +1,42 @@
/*
* 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.impl.crypto.historyvisible
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeHistoryVisibleAcknowledgementRepository(
private val acknowledgements: MutableMap<RoomId, MutableStateFlow<Boolean>> = mutableMapOf()
) : HistoryVisibleAcknowledgementRepository {
override fun hasAcknowledged(roomId: RoomId): Flow<Boolean> {
return acknowledgements.getOrPut(roomId) {
MutableStateFlow(false)
}
}
override suspend fun setAcknowledged(roomId: RoomId, value: Boolean) {
val flow = acknowledgements.getOrPut(roomId) {
MutableStateFlow(value)
}
flow.emit(value)
}
companion object {
/**
* Create the repository with a pre-existing entry.
*/
fun withRoom(roomId: RoomId, acknowledged: Boolean = false): FakeHistoryVisibleAcknowledgementRepository {
return FakeHistoryVisibleAcknowledgementRepository(
mutableMapOf(
roomId to MutableStateFlow(acknowledged)
)
)
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.impl.crypto.historyvisible
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class HistoryVisibleStatePresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - not visible if feature disabled`() = runTest {
val room = FakeJoinedRoom()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Joined, isEncrypted = true))
val presenter = createHistoryVisibleStatePresenter(room, enabled = false, acknowledged = false)
presenter.test {
assertThat(awaitLastSequentialItem().showAlert).isFalse()
}
}
@Test
fun `present - initial with room shared, unencrypted`() = runTest {
val room = FakeJoinedRoom()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, isEncrypted = false))
val presenter = createHistoryVisibleStatePresenter(room)
presenter.test {
assertThat(awaitLastSequentialItem().showAlert).isFalse()
}
}
@Test
fun `present - initial with room joined, encrypted`() = runTest {
val room = FakeJoinedRoom()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Joined, isEncrypted = false))
val presenter = createHistoryVisibleStatePresenter(room)
presenter.test {
assertThat(awaitLastSequentialItem().showAlert).isFalse()
}
}
@Test
fun `present - initial with room shared, encrypted, unacknowledged`() = runTest {
val room = FakeJoinedRoom()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, isEncrypted = true))
val presenter = createHistoryVisibleStatePresenter(room, acknowledged = false)
presenter.test {
val initialState = awaitItem()
assertThat(initialState.showAlert).isFalse()
val nextState = awaitItem()
assertThat(nextState.showAlert).isTrue()
}
}
@Test
fun `present - initial with room shared, encrypted, acknowledged`() = runTest {
val room = FakeJoinedRoom()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, isEncrypted = true))
val presenter = createHistoryVisibleStatePresenter(room, acknowledged = true)
presenter.test {
assertThat(awaitLastSequentialItem().showAlert).isFalse()
}
}
@Test
fun `present - transition from joined + unencrypted, to shared + encrypted`() = runTest {
val room = FakeJoinedRoom()
val featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.EnableKeyShareOnInvite.key to true))
val repository = FakeHistoryVisibleAcknowledgementRepository()
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Joined, isEncrypted = false))
val presenter = HistoryVisibleStatePresenter(
featureFlagService,
repository,
room,
)
presenter.test {
// emitted by the feature flag service(?)
assertThat(awaitItem().showAlert).isFalse()
// emitted state from room info assignment
assertThat(awaitItem().showAlert).isFalse()
// room is marked as encrypted
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Joined, isEncrypted = true))
assertThat(awaitItem().showAlert).isFalse()
// room history visibility is changed to shared
room.givenRoomInfo(aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, isEncrypted = true))
assertThat(awaitItem().showAlert).isTrue()
// alert is acknowledged
repository.setAcknowledged(room.roomId, true)
assertThat(awaitItem().showAlert).isFalse()
}
}
private fun createHistoryVisibleStatePresenter(
room: JoinedRoom = FakeJoinedRoom(),
enabled: Boolean = true,
acknowledged: Boolean = false
): HistoryVisibleStatePresenter {
return HistoryVisibleStatePresenter(
room = room,
featureFlagService = FakeFeatureFlagService(mapOf("feature.enableKeyShareOnInvite" to enabled)),
repository = FakeHistoryVisibleAcknowledgementRepository.withRoom(room.roomId, acknowledged)
)
}
}

View File

@@ -21,4 +21,5 @@ sealed interface DeveloperSettingsEvents {
data class SetShowColorPicker(val show: Boolean) : DeveloperSettingsEvents
data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvents
data object ClearCache : DeveloperSettingsEvents
data object VacuumStores : DeveloperSettingsEvents
}

View File

@@ -29,6 +29,7 @@ import io.element.android.features.preferences.impl.developer.tracing.toLogLevel
import io.element.android.features.preferences.impl.model.EnabledFeature
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
@@ -61,6 +62,7 @@ class DeveloperSettingsPresenter(
private val appPreferencesStore: AppPreferencesStore,
private val buildMeta: BuildMeta,
private val enterpriseService: EnterpriseService,
private val vacuumStoresUseCase: VacuumStoresUseCase,
) : Presenter<DeveloperSettingsState> {
@Composable
override fun present(): DeveloperSettingsState {
@@ -151,6 +153,9 @@ class DeveloperSettingsPresenter(
is DeveloperSettingsEvents.SetShowColorPicker -> {
showColorPicker = event.show
}
DeveloperSettingsEvents.VacuumStores -> coroutineScope.launch {
vacuumStoresUseCase()
}
}
}

View File

@@ -146,6 +146,14 @@ fun DeveloperSettingsView(
}
val cache = state.cacheSize
PreferenceCategory(title = "Cache") {
ListItem(
headlineContent = {
Text("Vacuum stores")
},
onClick = {
state.eventSink(DeveloperSettingsEvents.VacuumStores)
}
)
ListItem(
headlineContent = {
Text("Clear cache")

View File

@@ -0,0 +1,27 @@
/*
* 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.preferences.impl.tasks
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.MatrixClient
import timber.log.Timber
fun interface VacuumStoresUseCase {
suspend operator fun invoke()
}
@ContributesBinding(AppScope::class)
class DefaultVacuumStoresUseCase(
private val matrixClient: MatrixClient,
) : VacuumStoresUseCase {
override suspend fun invoke() {
matrixClient.performDatabaseVacuum()
.onFailure { Timber.e(it, "Failed to vacuum stores") }
}
}

View File

@@ -35,7 +35,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
@@ -127,7 +127,7 @@ class EditUserProfilePresenter(
cameraPhotoPicker.launch()
} else {
pendingPermissionRequest = true
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
}
AvatarAction.Remove -> {
temporaryUriDeleter.delete(userAvatarUri?.toUri())

View File

@@ -17,6 +17,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
@@ -212,6 +213,23 @@ class DeveloperSettingsPresenterTest {
}
}
@Test
fun `present - VacuumStores action invokes the VacuumStoresUseCase`() = runTest {
var vacuumCalled = false
val presenter = createDeveloperSettingsPresenter(
vacuumStoresUseCase = VacuumStoresUseCase {
vacuumCalled = true
}
)
presenter.test {
val state = awaitItem()
assertThat(vacuumCalled).isFalse()
state.eventSink(DeveloperSettingsEvents.VacuumStores)
skipItems(1)
assertThat(vacuumCalled).isTrue()
}
}
private fun createDeveloperSettingsPresenter(
sessionId: SessionId = A_SESSION_ID,
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
@@ -230,6 +248,7 @@ class DeveloperSettingsPresenterTest {
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
buildMeta: BuildMeta = aBuildMeta(),
enterpriseService: EnterpriseService = FakeEnterpriseService(),
vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {},
): DeveloperSettingsPresenter {
return DeveloperSettingsPresenter(
sessionId = sessionId,
@@ -240,6 +259,7 @@ class DeveloperSettingsPresenterTest {
appPreferencesStore = preferencesStore,
buildMeta = buildMeta,
enterpriseService = enterpriseService,
vacuumStoresUseCase = vacuumStoresUseCase,
)
}
}

View File

@@ -2,12 +2,9 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_room_change_permissions_administrators">"Správca"</string>
<string name="screen_room_change_permissions_ban_people">"Zakázať ľudí"</string>
<string name="screen_room_change_permissions_change_settings">"Zmeniť nastavenia"</string>
<string name="screen_room_change_permissions_delete_messages">"Odstrániť správy"</string>
<string name="screen_room_change_permissions_everyone">"Člen"</string>
<string name="screen_room_change_permissions_invite_people">"Pozvať ľudí"</string>
<string name="screen_room_change_permissions_manage_space">"Spravovať priestor"</string>
<string name="screen_room_change_permissions_manage_space_rooms">"Spravovať miestnosti"</string>
<string name="screen_room_change_permissions_member_moderation">"Spravovať členov"</string>
<string name="screen_room_change_permissions_messages_and_content">"Správy a obsah"</string>
<string name="screen_room_change_permissions_moderators">"Moderátor"</string>
@@ -17,7 +14,6 @@
<string name="screen_room_change_permissions_room_name">"Zmeniť názov miestnosti"</string>
<string name="screen_room_change_permissions_room_topic">"Zmeniť tému miestnosti"</string>
<string name="screen_room_change_permissions_send_messages">"Odoslať správy"</string>
<string name="screen_room_change_permissions_title">"Povolenia"</string>
<string name="screen_room_change_role_administrators_title">"Upraviť správcov"</string>
<string name="screen_room_change_role_confirm_add_admin_description">"Túto akciu nebudete môcť vrátiť späť. Zvyšujete úroveň používateľa na rovnakú úroveň výkonu ako máte vy."</string>
<string name="screen_room_change_role_confirm_add_admin_title">"Pridať správcu?"</string>

View File

@@ -26,7 +26,7 @@ data class RoomMemberListState(
val moderationState: RoomMemberModerationState,
val eventSink: (RoomMemberListEvents) -> Unit,
) {
val showBannedSection: Boolean = moderationState.canBan && roomMembers.dataOrNull()?.banned?.isNotEmpty() == true
val showBannedSection: Boolean = moderationState.permissions.canBan && roomMembers.dataOrNull()?.banned?.isNotEmpty() == true
}
enum class SelectedSection {

View File

@@ -10,6 +10,7 @@ package io.element.android.features.roomdetails.impl.members
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.map
@@ -99,8 +100,10 @@ fun aRoomMemberModerationState(
canKick: Boolean = false,
): RoomMemberModerationState {
return object : RoomMemberModerationState {
override val canKick: Boolean = canKick
override val canBan: Boolean = canBan
override val permissions: RoomMemberModerationPermissions = RoomMemberModerationPermissions(
canBan = canBan,
canKick = canKick,
)
override val eventSink: (RoomMemberModerationEvents) -> Unit = {}
}
}

View File

@@ -84,6 +84,10 @@
<string name="screen_room_member_list_manage_member_unban_title">"Eemalda suhtluskeeld jututoas"</string>
<string name="screen_room_member_list_mode_banned">"Suhtluskeeluga kasutajad"</string>
<string name="screen_room_member_list_mode_members">"Liikmed"</string>
<plurals name="screen_room_member_list_pending_header_title">
<item quantity="one">"%1$d saatis kutse"</item>
<item quantity="other">"%1$d saatis kutse"</item>
</plurals>
<string name="screen_room_member_list_pending_status">"Ootel"</string>
<string name="screen_room_member_list_role_administrator">"Peakasutajad"</string>
<string name="screen_room_member_list_role_moderator">"Moderaatorid"</string>
@@ -133,6 +137,7 @@ Me ei soovita krüptimise kasutamist selliste avalike jututubade puhul, millega
<string name="screen_security_and_privacy_encryption_toggle_title">"Võta läbiv krüptimine kasutusele"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Kõik võivad jututoaga liituda"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Kõik"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Halda kogukondi"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Liituda saab vaid kutse olemasolul"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Vaid kutsega"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Ligipääs"</string>

View File

@@ -141,9 +141,13 @@ Nous ne recommandons pas dactiver le chiffrement pour les salons que tout le
<string name="screen_security_and_privacy_encryption_toggle_title">"Activer le chiffrement de bout en bout"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Tout le monde peut rejoindre."</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Tout le monde"</string>
<string name="screen_security_and_privacy_room_access_footer">"Choisissez les espaces dont les membres peuvent rejoindre ce salon sans invitation. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Gérer les espaces"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Seules les personnes invitées peuvent rejoindre."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Sur invitation uniquement"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Accès"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Toute personne se trouvant dans un espace autorisé peut joindre le salon."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Toute personne de lespace %1$s peut joindre le salon."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Membres de lespace"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Les Espaces ne sont pas encore supportés"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Vous aurez besoin dune adresse pour le rendre visible dans lannuaire public."</string>

View File

@@ -1,19 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_edit_room_address_room_address_section_footer">"Budete potrebovať adresu miestnosti, aby bola viditeľná v adresári."</string>
<string name="screen_edit_room_address_title">"Adresa miestnosti"</string>
<string name="screen_edit_room_address_room_address_section_footer">"Budete potrebovať adresu, aby sa zobrazovala vo verejnom adresári."</string>
<string name="screen_edit_room_address_title">"Upraviť adresu"</string>
<string name="screen_notification_settings_edit_failed_updating_default_mode">"Pri aktualizácii nastavenia oznámenia došlo k chybe."</string>
<string name="screen_notification_settings_mentions_only_disclaimer">"Váš domovský server nepodporuje túto možnosť v šifrovaných miestnostiach, v niektorých miestnostiach nemusíte dostať upozornenie."</string>
<string name="screen_polls_history_title">"Ankety"</string>
<string name="screen_room_change_permissions_administrators">"Iba správcovia"</string>
<string name="screen_room_change_permissions_administrators">"Správca"</string>
<string name="screen_room_change_permissions_ban_people">"Zakázať ľudí"</string>
<string name="screen_room_change_permissions_delete_messages">"Odstrániť správy"</string>
<string name="screen_room_change_permissions_invite_people">"Pozvite ľudí a prijmite žiadosti o pripojenie"</string>
<string name="screen_room_change_permissions_everyone">"Člen"</string>
<string name="screen_room_change_permissions_invite_people">"Pozvať ľudí"</string>
<string name="screen_room_change_permissions_member_moderation">"Spravovať členov"</string>
<string name="screen_room_change_permissions_messages_and_content">"Správy a obsah"</string>
<string name="screen_room_change_permissions_moderators">"Správcovia a moderátori"</string>
<string name="screen_room_change_permissions_remove_people">"Odstrániť ľudí a odmietnuť žiadosti o pripojenie"</string>
<string name="screen_room_change_permissions_moderators">"Moderátor"</string>
<string name="screen_room_change_permissions_remove_people">"Odstrániť ľudí"</string>
<string name="screen_room_change_permissions_room_avatar">"Zmeniť obrázok miestnosti"</string>
<string name="screen_room_change_permissions_room_details">"Upraviť miestnosť"</string>
<string name="screen_room_change_permissions_room_details">"Upraviť podrobnosti"</string>
<string name="screen_room_change_permissions_room_name">"Zmeniť názov miestnosti"</string>
<string name="screen_room_change_permissions_room_topic">"Zmeniť tému miestnosti"</string>
<string name="screen_room_change_permissions_send_messages">"Odoslať správy"</string>
@@ -40,7 +42,7 @@
<string name="screen_room_details_badge_encrypted">"Zašifrované"</string>
<string name="screen_room_details_badge_not_encrypted">"Nešifrované"</string>
<string name="screen_room_details_badge_public">"Verejná miestnosť"</string>
<string name="screen_room_details_edit_room_title">"Upraviť miestnosť"</string>
<string name="screen_room_details_edit_room_title">"Upraviť podrobnosti"</string>
<string name="screen_room_details_edition_error">"Vyskytla sa neznáma chyba a informácie nebolo možné zmeniť."</string>
<string name="screen_room_details_edition_error_title">"Nepodarilo sa aktualizovať miestnosť"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Správy sú zabezpečené zámkami. Jedine vy a príjemcovia máte jedinečné kľúče na ich odomknutie."</string>
@@ -69,6 +71,13 @@
<string name="screen_room_details_topic_title">"Téma"</string>
<string name="screen_room_details_updating_room">"Aktualizácia miestnosti…"</string>
<string name="screen_room_member_list_banned_empty">"Neexistujú žiadni zablokovaní používatelia."</string>
<plurals name="screen_room_member_list_banned_header_title">
<item quantity="one">"%1$d zakázaný"</item>
<item quantity="few">"%1$d zakázaní"</item>
<item quantity="other">"%1$d zakázaných"</item>
</plurals>
<string name="screen_room_member_list_empty_search_subtitle">"Skontrolujte preklepy alebo skúste nové vyhľadávanie"</string>
<string name="screen_room_member_list_empty_search_title">"Žiadne výsledky pre „%1$s“"</string>
<plurals name="screen_room_member_list_header_title">
<item quantity="one">"%1$d osoba"</item>
<item quantity="few">"%1$d osoby"</item>
@@ -81,8 +90,14 @@
<string name="screen_room_member_list_manage_member_unban_title">"Zrušiť zákaz prístupu do miestnosti"</string>
<string name="screen_room_member_list_mode_banned">"Zakázaní"</string>
<string name="screen_room_member_list_mode_members">"Členovia"</string>
<string name="screen_room_member_list_role_administrator">"Iba správcovia"</string>
<string name="screen_room_member_list_role_moderator">"Správcovia a moderátori"</string>
<plurals name="screen_room_member_list_pending_header_title">
<item quantity="one">"%1$d pozvaný"</item>
<item quantity="few">"%1$d pozvaní"</item>
<item quantity="other">"%1$d pozvaných"</item>
</plurals>
<string name="screen_room_member_list_pending_status">"Čaká na schválenie"</string>
<string name="screen_room_member_list_role_administrator">"Správca"</string>
<string name="screen_room_member_list_role_moderator">"Moderátor"</string>
<string name="screen_room_member_list_role_owner">"Vlastník"</string>
<string name="screen_room_member_list_room_members_header_title">"Členovia miestnosti"</string>
<string name="screen_room_member_list_unbanning_user">"Zrušenie zákazu %1$s"</string>
@@ -109,14 +124,15 @@
<string name="screen_room_roles_and_permissions_messages_and_content">"Správy a obsah"</string>
<string name="screen_room_roles_and_permissions_moderators">"Moderátori"</string>
<string name="screen_room_roles_and_permissions_owners">"Vlastníci"</string>
<string name="screen_room_roles_and_permissions_permissions_header">"Povolenia"</string>
<string name="screen_room_roles_and_permissions_reset">"Obnoviť povolenia"</string>
<string name="screen_room_roles_and_permissions_reset_confirm_description">"Po obnovení oprávnení prídete o aktuálne nastavenia."</string>
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Obnoviť oprávnenia?"</string>
<string name="screen_room_roles_and_permissions_roles_header">"Roly"</string>
<string name="screen_room_roles_and_permissions_room_details">"Podrobnosti o miestnosti"</string>
<string name="screen_room_roles_and_permissions_title">"Roly a povolenia"</string>
<string name="screen_security_and_privacy_add_room_address_action">"Pridať adresu miestnosti"</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Ktokoľvek môže požiadať o pripojenie do miestnosti, ale správca alebo moderátor bude musieť žiadosť prijať."</string>
<string name="screen_security_and_privacy_add_room_address_action">"Pridať adresu"</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Všetci musia požiadať o prístup."</string>
<string name="screen_security_and_privacy_ask_to_join_option_title">"Požiadať o pripojenie"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Áno, povoliť šifrovanie"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Po aktivácii nie je možné zakázať šifrovanie pre miestnosť. História správ bude viditeľná len pre členov miestnosti, odkedy boli pozvaní alebo keď vstúpili do miestnosti.
@@ -126,17 +142,22 @@ To môže brániť správnemu fungovaniu robotov a premostení. Neodporúčame p
<string name="screen_security_and_privacy_encryption_section_footer">"Po zapnutí už šifrovanie nie je možné vypnúť."</string>
<string name="screen_security_and_privacy_encryption_section_header">"Šifrovanie"</string>
<string name="screen_security_and_privacy_encryption_toggle_title">"Povoliť end-to-end šifrovanie"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Ktokoľvek môže nájsť a pripojiť sa"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Pripojiť sa môže ktokoľvek."</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Ktokoľvek"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Ľudia sa môžu pripojiť len vtedy, ak sú pozvaní"</string>
<string name="screen_security_and_privacy_room_access_footer">"Vyberte, ktorých členovia priestorov sa môžu pripojiť k tejto miestnosti bez pozvánky. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Spravovať priestory"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Pripojiť sa môžu iba pozvaní ľudia."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Iba na pozvánku"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Prístup do miestnosti"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Prístup"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Ktokoľvek v povolených priestoroch sa môže pripojiť."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Ktokoľvek v %1$s sa môže pripojiť."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Členovia priestoru"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Priestory momentálne nie sú podporované"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Budete potrebovať adresu miestnosti, aby bola viditeľná v adresári."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresa miestnosti"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Budete potrebovať adresu, aby sa zobrazovala vo verejnom adresári."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresa"</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Umožniť vyhľadanie tejto miestnosti v adresári verejných miestností %1$s"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Viditeľné v adresári verejných miestností"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Umožniť nájdenie vyhľadávaním vo verejnom adresári."</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Viditeľné vo verejnom adresári"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Ktokoľvek"</string>
<string name="screen_security_and_privacy_room_history_section_header">"Kto môže čítať históriu"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Len pre členov, odkedy boli pozvaní"</string>
@@ -144,6 +165,7 @@ To môže brániť správnemu fungovaniu robotov a premostení. Neodporúčame p
<string name="screen_security_and_privacy_room_publishing_section_footer">"Adresy miestností predstavujú spôsoby, ako nájsť a získať prístup k miestnostiam. To tiež zaisťuje, že môžete jednoducho zdieľať svoju miestnosť s ostatnými.
Môžete sa rozhodnúť zverejniť svoju miestnosť v adresári verejných miestností vášho domovského servera."</string>
<string name="screen_security_and_privacy_room_publishing_section_header">"Zverejnenie miestnosti"</string>
<string name="screen_security_and_privacy_room_visibility_section_header">"Viditeľnosť miestnosti"</string>
<string name="screen_security_and_privacy_room_visibility_section_footer">"Adresy sú spôsob, ako nájsť a získať prístup do miestností a priestorov. To tiež zabezpečuje, že ich môžete jednoducho zdieľať s ostatnými."</string>
<string name="screen_security_and_privacy_room_visibility_section_header">"Viditeľnosť"</string>
<string name="screen_security_and_privacy_title">"Bezpečnosť a súkromie"</string>
</resources>

View File

@@ -69,7 +69,7 @@
<string name="screen_room_details_share_room_title">"Share room"</string>
<string name="screen_room_details_title">"Room info"</string>
<string name="screen_room_details_topic_title">"Topic"</string>
<string name="screen_room_details_updating_room">"Updating room…"</string>
<string name="screen_room_details_updating_room">"Updating details…"</string>
<string name="screen_room_member_list_banned_empty">"There are no banned users."</string>
<plurals name="screen_room_member_list_banned_header_title">
<item quantity="one">"%1$d Banned"</item>
@@ -129,8 +129,10 @@
<string name="screen_room_roles_and_permissions_room_details">"Room details"</string>
<string name="screen_room_roles_and_permissions_title">"Roles &amp; permissions"</string>
<string name="screen_security_and_privacy_add_room_address_action">"Add address"</string>
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Anyone in authorized spaces can join, but everyone else must request access."</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Everyone must request access."</string>
<string name="screen_security_and_privacy_ask_to_join_option_title">"Ask to join"</string>
<string name="screen_security_and_privacy_ask_to_join_single_space_members_option_description">"Anyone in %1$s can join, but everyone else must request access."</string>
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Yes, enable encryption"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Once enabled, encryption for a room cannot be disabled, Message history will only be visible for room members since they were invited or since they joined the room.
No one besides the room members will be able to read messages. This may prevent bots and bridges to work correctly.

View File

@@ -37,7 +37,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
@@ -151,7 +151,7 @@ class RoomDetailsEditPresenter(
cameraPhotoPicker.launch()
} else {
pendingPermissionRequest = true
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions)
cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
}
AvatarAction.Remove -> {
temporaryUriDeleter.delete(roomAvatarUriEdited?.toUri())

View File

@@ -3,5 +3,5 @@
<string name="screen_room_details_edit_room_title">"Edit details"</string>
<string name="screen_room_details_edition_error">"There was an unknown error and the information couldn\'t be changed."</string>
<string name="screen_room_details_edition_error_title">"Unable to update room"</string>
<string name="screen_room_details_updating_room">"Updating room…"</string>
<string name="screen_room_details_updating_room">"Updating details…"</string>
</resources>

View File

@@ -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.features.roommembermoderation.api
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPermissions
data class RoomMemberModerationPermissions(
val canKick: Boolean,
val canBan: Boolean,
) {
companion object {
val DEFAULT = RoomMemberModerationPermissions(
canKick = false,
canBan = false,
)
}
}
fun RoomPermissions.roomMemberModerationPermissions(): RoomMemberModerationPermissions {
return RoomMemberModerationPermissions(
canKick = canOwnUserKick(),
canBan = canOwnUserBan(),
)
}

View File

@@ -12,8 +12,7 @@ import androidx.compose.runtime.Immutable
@Immutable
interface RoomMemberModerationState {
val canKick: Boolean
val canBan: Boolean
val permissions: RoomMemberModerationPermissions
val eventSink: (RoomMemberModerationEvents) -> Unit
}

View File

@@ -10,14 +10,14 @@ package io.element.android.features.roommembermoderation.impl
import io.element.android.features.roommembermoderation.api.ModerationActionState
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class InternalRoomMemberModerationState(
override val canKick: Boolean,
override val canBan: Boolean,
override val permissions: RoomMemberModerationPermissions,
val selectedUser: MatrixUser?,
val actions: ImmutableList<ModerationActionState>,
val kickUserAsyncAction: AsyncAction<Unit>,

View File

@@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roommembermoderation.api.ModerationAction
import io.element.android.features.roommembermoderation.api.ModerationActionState
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -83,8 +84,7 @@ fun anAlice() = MatrixUser(
)
fun aRoomMembersModerationState(
canKick: Boolean = false,
canBan: Boolean = false,
permissions: RoomMemberModerationPermissions = RoomMemberModerationPermissions.DEFAULT,
selectedUser: MatrixUser? = null,
actions: List<ModerationActionState> = emptyList(),
kickUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
@@ -92,8 +92,7 @@ fun aRoomMembersModerationState(
unbanUserAsyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (RoomMemberModerationEvents) -> Unit = {},
) = InternalRoomMemberModerationState(
canKick = canKick,
canBan = canBan,
permissions = permissions,
selectedUser = selectedUser,
actions = actions.toImmutableList(),
kickUserAsyncAction = kickUserAsyncAction,

View File

@@ -21,7 +21,9 @@ import im.vector.app.features.analytics.plan.RoomModeration
import io.element.android.features.roommembermoderation.api.ModerationAction
import io.element.android.features.roommembermoderation.api.ModerationActionState
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.features.roommembermoderation.api.roomMemberModerationPermissions
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
@@ -55,11 +57,8 @@ class RoomMemberModerationPresenter(
override fun present(): RoomMemberModerationState {
val coroutineScope = rememberCoroutineScope()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val permissions by room.permissionsAsState(Permissions()) { perms ->
Permissions(
canKick = perms.canOwnUserKick(),
canBan = perms.canOwnUserBan(),
)
val permissions by room.permissionsAsState(RoomMemberModerationPermissions.DEFAULT) { perms ->
perms.roomMemberModerationPermissions()
}
val currentUserMemberPowerLevel = room.userPowerLevelAsState(syncUpdateFlow.value)
@@ -136,8 +135,7 @@ class RoomMemberModerationPresenter(
}
return InternalRoomMemberModerationState(
canKick = permissions.canKick,
canBan = permissions.canBan,
permissions = permissions,
selectedUser = selectedUser,
actions = moderationActions.value,
kickUserAsyncAction = kickUserAsyncAction.value,
@@ -149,7 +147,7 @@ class RoomMemberModerationPresenter(
private fun computeModerationActions(
member: RoomMember?,
permissions: Permissions,
permissions: RoomMemberModerationPermissions,
currentUserMemberPowerLevel: Long,
): ImmutableList<ModerationActionState> {
return buildList {

View File

@@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.roommembermoderation.api.ModerationAction
import io.element.android.features.roommembermoderation.api.ModerationActionState
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@@ -49,8 +50,7 @@ class RoomMemberModerationPresenterTest {
val room = aJoinedRoom()
createRoomMemberModerationPresenter(room = room).test {
val initialState = awaitState()
assertThat(initialState.canKick).isFalse()
assertThat(initialState.canBan).isFalse()
assertThat(initialState.permissions).isEqualTo(RoomMemberModerationPermissions.DEFAULT)
assertThat(initialState.selectedUser).isNull()
assertThat(initialState.banUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(initialState.kickUserAsyncAction).isEqualTo(AsyncAction.Uninitialized)

View File

@@ -218,7 +218,7 @@ private fun RoomAccessSection(
Text(text = stringResource(R.string.screen_security_and_privacy_room_access_space_members_option_unavailable_description))
},
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.SpaceMember, enabled = false),
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Workspace())),
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Space())),
enabled = false,
)
}

View File

@@ -15,6 +15,7 @@ Me ei soovita krüptimise kasutamist selliste avalike jututubade puhul, millega
<string name="screen_security_and_privacy_encryption_toggle_title">"Võta läbiv krüptimine kasutusele"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Kõik võivad jututoaga liituda"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Kõik"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Halda kogukondi"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Liituda saab vaid kutse olemasolul"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Vaid kutsega"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Ligipääs"</string>

View File

@@ -15,9 +15,13 @@ Nous ne recommandons pas dactiver le chiffrement pour les salons que tout le
<string name="screen_security_and_privacy_encryption_toggle_title">"Activer le chiffrement de bout en bout"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Tout le monde peut rejoindre."</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Tout le monde"</string>
<string name="screen_security_and_privacy_room_access_footer">"Choisissez les espaces dont les membres peuvent rejoindre ce salon sans invitation. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Gérer les espaces"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Seules les personnes invitées peuvent rejoindre."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Sur invitation uniquement"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Accès"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Toute personne se trouvant dans un espace autorisé peut joindre le salon."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Toute personne de lespace %1$s peut joindre le salon."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Membres de lespace"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Les Espaces ne sont pas encore supportés"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Vous aurez besoin dune adresse pour le rendre visible dans lannuaire public."</string>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_edit_room_address_room_address_section_footer">"Budete potrebovať adresu miestnosti, aby bola viditeľná v adresári."</string>
<string name="screen_edit_room_address_title">"Adresa miestnosti"</string>
<string name="screen_security_and_privacy_add_room_address_action">"Pridať adresu miestnosti"</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Ktokoľvek môže požiadať o pripojenie do miestnosti, ale správca alebo moderátor bude musieť žiadosť prijať."</string>
<string name="screen_edit_room_address_room_address_section_footer">"Budete potrebovať adresu, aby sa zobrazovala vo verejnom adresári."</string>
<string name="screen_edit_room_address_title">"Upraviť adresu"</string>
<string name="screen_security_and_privacy_add_room_address_action">"Pridať adresu"</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Všetci musia požiadať o prístup."</string>
<string name="screen_security_and_privacy_ask_to_join_option_title">"Požiadať o pripojenie"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Áno, povoliť šifrovanie"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Po aktivácii nie je možné zakázať šifrovanie pre miestnosť. História správ bude viditeľná len pre členov miestnosti, odkedy boli pozvaní alebo keď vstúpili do miestnosti.
@@ -13,17 +13,22 @@ To môže brániť správnemu fungovaniu robotov a premostení. Neodporúčame p
<string name="screen_security_and_privacy_encryption_section_footer">"Po zapnutí už šifrovanie nie je možné vypnúť."</string>
<string name="screen_security_and_privacy_encryption_section_header">"Šifrovanie"</string>
<string name="screen_security_and_privacy_encryption_toggle_title">"Povoliť end-to-end šifrovanie"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Ktokoľvek môže nájsť a pripojiť sa"</string>
<string name="screen_security_and_privacy_room_access_anyone_option_description">"Pripojiť sa môže ktokoľvek."</string>
<string name="screen_security_and_privacy_room_access_anyone_option_title">"Ktokoľvek"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Ľudia sa môžu pripojiť len vtedy, ak sú pozvaní"</string>
<string name="screen_security_and_privacy_room_access_footer">"Vyberte, ktorých členovia priestorov sa môžu pripojiť k tejto miestnosti bez pozvánky. %1$s"</string>
<string name="screen_security_and_privacy_room_access_footer_manage_spaces_action">"Spravovať priestory"</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_description">"Pripojiť sa môžu iba pozvaní ľudia."</string>
<string name="screen_security_and_privacy_room_access_invite_only_option_title">"Iba na pozvánku"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Prístup do miestnosti"</string>
<string name="screen_security_and_privacy_room_access_section_header">"Prístup"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_multiple_parents_description">"Ktokoľvek v povolených priestoroch sa môže pripojiť."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_single_parent_description">"Ktokoľvek v %1$s sa môže pripojiť."</string>
<string name="screen_security_and_privacy_room_access_space_members_option_title">"Členovia priestoru"</string>
<string name="screen_security_and_privacy_room_access_space_members_option_unavailable_description">"Priestory momentálne nie sú podporované"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Budete potrebovať adresu miestnosti, aby bola viditeľná v adresári."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresa miestnosti"</string>
<string name="screen_security_and_privacy_room_address_section_footer">"Budete potrebovať adresu, aby sa zobrazovala vo verejnom adresári."</string>
<string name="screen_security_and_privacy_room_address_section_header">"Adresa"</string>
<string name="screen_security_and_privacy_room_directory_visibility_section_footer">"Umožniť vyhľadanie tejto miestnosti v adresári verejných miestností %1$s"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Viditeľné v adresári verejných miestností"</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_description">"Umožniť nájdenie vyhľadávaním vo verejnom adresári."</string>
<string name="screen_security_and_privacy_room_directory_visibility_toggle_title">"Viditeľné vo verejnom adresári"</string>
<string name="screen_security_and_privacy_room_history_anyone_option_title">"Ktokoľvek"</string>
<string name="screen_security_and_privacy_room_history_section_header">"Kto môže čítať históriu"</string>
<string name="screen_security_and_privacy_room_history_since_invite_option_title">"Len pre členov, odkedy boli pozvaní"</string>
@@ -31,6 +36,7 @@ To môže brániť správnemu fungovaniu robotov a premostení. Neodporúčame p
<string name="screen_security_and_privacy_room_publishing_section_footer">"Adresy miestností predstavujú spôsoby, ako nájsť a získať prístup k miestnostiam. To tiež zaisťuje, že môžete jednoducho zdieľať svoju miestnosť s ostatnými.
Môžete sa rozhodnúť zverejniť svoju miestnosť v adresári verejných miestností vášho domovského servera."</string>
<string name="screen_security_and_privacy_room_publishing_section_header">"Zverejnenie miestnosti"</string>
<string name="screen_security_and_privacy_room_visibility_section_header">"Viditeľnosť miestnosti"</string>
<string name="screen_security_and_privacy_room_visibility_section_footer">"Adresy sú spôsob, ako nájsť a získať prístup do miestností a priestorov. To tiež zabezpečuje, že ich môžete jednoducho zdieľať s ostatnými."</string>
<string name="screen_security_and_privacy_room_visibility_section_header">"Viditeľnosť"</string>
<string name="screen_security_and_privacy_title">"Bezpečnosť a súkromie"</string>
</resources>

View File

@@ -3,8 +3,10 @@
<string name="screen_edit_room_address_room_address_section_footer">"Youll need an address in order to make it visible in the public directory."</string>
<string name="screen_edit_room_address_title">"Edit address"</string>
<string name="screen_security_and_privacy_add_room_address_action">"Add address"</string>
<string name="screen_security_and_privacy_ask_to_join_multiple_spaces_members_option_description">"Anyone in authorized spaces can join, but everyone else must request access."</string>
<string name="screen_security_and_privacy_ask_to_join_option_description">"Everyone must request access."</string>
<string name="screen_security_and_privacy_ask_to_join_option_title">"Ask to join"</string>
<string name="screen_security_and_privacy_ask_to_join_single_space_members_option_description">"Anyone in %1$s can join, but everyone else must request access."</string>
<string name="screen_security_and_privacy_enable_encryption_alert_confirm_button_title">"Yes, enable encryption"</string>
<string name="screen_security_and_privacy_enable_encryption_alert_description">"Once enabled, encryption for a room cannot be disabled, Message history will only be visible for room members since they were invited or since they joined the room.
No one besides the room members will be able to read messages. This may prevent bots and bridges to work correctly.

View File

@@ -3,7 +3,7 @@
[versions]
# Project
android_gradle_plugin = "8.13.1"
android_gradle_plugin = "8.13.2"
# When updateing this, please also update the version in the file ./idea/kotlinc.xml
kotlin = "2.2.20"
kotlinpoet = "2.2.0"
@@ -18,7 +18,7 @@ constraintlayout_compose = "1.1.1"
lifecycle = "2.9.2"
activity = "1.11.0"
media3 = "1.8.0"
camera = "1.5.1"
camera = "1.5.2"
work = "2.11.0"
# Compose
@@ -41,7 +41,7 @@ serialization_json = "1.9.0"
#other
coil = "3.3.0"
# Rollback to 1.0.4, 1.0.5 has this issue: https://github.com/airbnb/Showkase/issues/420
showkase = "1.0.4"
showkase = "1.0.5"
appyx = "1.7.1"
sqldelight = "2.2.1"
wysiwyg = "2.40.0"
@@ -52,7 +52,7 @@ haze = "1.6.10"
dependencyAnalysis = "3.5.1"
# DI
metro = "0.8.1"
metro = "0.8.2"
# Auto service
autoservice = "1.1.1"
@@ -160,7 +160,7 @@ test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" }
test_arch_core = "androidx.arch.core:core-testing:2.2.0"
test_junit = "junit:junit:4.13.2"
test_runner = "androidx.test:runner:1.7.0"
test_mockk = "io.mockk:mockk:1.14.6"
test_mockk = "io.mockk:mockk:1.14.7"
test_konsist = "com.lemonappdev:konsist:0.17.3"
test_turbine = "app.cash.turbine:turbine:1.2.1"
test_truth = "com.google.truth:truth:1.4.5"
@@ -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.2"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.12.10"
# Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
@@ -199,14 +199,14 @@ matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose",
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-jvm = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
sqlcipher = "net.zetetic:sqlcipher-android:4.11.0"
sqlcipher = "net.zetetic:sqlcipher-android:4.12.0"
sqlite = "androidx.sqlite:sqlite-ktx:2.6.2"
unifiedpush = "org.unifiedpush.android:connector:3.1.2"
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.2"
maplibre = "org.maplibre.gl:android-sdk:12.2.1"
maplibre = "org.maplibre.gl:android-sdk:12.2.2"
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.2.0"
@@ -217,7 +217,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0"
# Analytics
posthog = "com.posthog:posthog-android:3.26.0"
sentry = "io.sentry:sentry-android:8.27.1"
sentry = "io.sentry:sentry-android:8.28.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"

View File

@@ -60,7 +60,7 @@ fun Activity.openUrlInChromeCustomTab(
})
}
.launchUrl(this, url.toUri())
} catch (activityNotFoundException: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
openUrlInExternalApp(url)
}
}

View File

@@ -20,7 +20,7 @@ fun String.hash() = try {
digest.digest()
.joinToString("") { String.format(Locale.ROOT, "%02X", it) }
.lowercase(Locale.ROOT)
} catch (exc: Exception) {
} catch (_: Exception) {
// Should not happen, but just in case
hashCode().toString()
}

View File

@@ -32,7 +32,7 @@ fun Context.getApplicationLabel(packageName: String): String {
return try {
val ai = packageManager.getApplicationInfoCompat(packageName, 0)
packageManager.getApplicationLabel(ai).toString()
} catch (e: PackageManager.NameNotFoundException) {
} catch (_: PackageManager.NameNotFoundException) {
packageName
}
}
@@ -96,7 +96,7 @@ fun Context.startNotificationSettingsIntent(
} else {
startActivity(intent)
}
} catch (activityNotFoundException: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
toast(noActivityFoundMessage)
}
}
@@ -112,7 +112,7 @@ fun Context.openAppSettingsPage(
data = Uri.fromParts("package", packageName, null)
}
)
} catch (activityNotFoundException: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
toast(noActivityFoundMessage)
}
}
@@ -126,7 +126,7 @@ fun Context.startInstallFromSourceIntent(
.setData("package:$packageName".toUri())
try {
activityResultLauncher.launch(intent)
} catch (activityNotFoundException: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
toast(noActivityFoundMessage)
}
}
@@ -157,7 +157,7 @@ fun Context.startSharePlainTextIntent(
} else {
startActivity(intent)
}
} catch (activityNotFoundException: ActivityNotFoundException) {
} catch (_: ActivityNotFoundException) {
toast(noActivityFoundMessage)
}
}

View File

@@ -0,0 +1,15 @@
/*
* 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.androidutils.text
import java.net.URLDecoder
import java.net.URLEncoder
import java.nio.charset.Charset
fun String.urlEncoded(charset: Charset = Charsets.UTF_8): String = URLEncoder.encode(this, charset.name())
fun String.urlDecoded(charset: Charset = Charsets.UTF_8): String = URLDecoder.decode(this, charset.name())

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,10 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
/**
* !!! WARNING !!!
*
@@ -14,8 +12,6 @@
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated
@@ -185,6 +181,9 @@ object CompoundIcons {
@Composable fun ErrorSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_error_solid)
}
@Composable fun ExitFullScreen(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_exit_full_screen)
}
@Composable fun Expand(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_expand)
}
@@ -218,6 +217,9 @@ object CompoundIcons {
@Composable fun Forward(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_forward)
}
@Composable fun FullScreen(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_full_screen)
}
@Composable fun Grid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_grid)
}
@@ -296,6 +298,9 @@ object CompoundIcons {
@Composable fun Leave(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_leave)
}
@Composable fun LeftPanelClose(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_left_panel_close)
}
@Composable fun Link(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_link)
}
@@ -503,6 +508,12 @@ object CompoundIcons {
@Composable fun SignOut(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_sign_out)
}
@Composable fun Space(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_space)
}
@Composable fun SpaceSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_space_solid)
}
@Composable fun Spinner(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_spinner)
}
@@ -620,12 +631,6 @@ object CompoundIcons {
@Composable fun Windows(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_windows)
}
@Composable fun Workspace(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_workspace)
}
@Composable fun WorkspaceSolid(): ImageVector {
return ImageVector.vectorResource(R.drawable.ic_compound_workspace_solid)
}
val all @Composable get() = persistentListOf<ImageVector>(
Admin(),
@@ -681,6 +686,7 @@ object CompoundIcons {
EndCall(),
Error(),
ErrorSolid(),
ExitFullScreen(),
Expand(),
Explore(),
ExportArchive(),
@@ -692,6 +698,7 @@ object CompoundIcons {
Files(),
Filter(),
Forward(),
FullScreen(),
Grid(),
Group(),
Guest(),
@@ -718,6 +725,7 @@ object CompoundIcons {
Keyboard(),
Labs(),
Leave(),
LeftPanelClose(),
Link(),
Linux(),
ListBulleted(),
@@ -787,6 +795,8 @@ object CompoundIcons {
Shield(),
Sidebar(),
SignOut(),
Space(),
SpaceSolid(),
Spinner(),
Spotlight(),
SpotlightView(),
@@ -826,8 +836,6 @@ object CompoundIcons {
Warning(),
WebBrowser(),
Windows(),
Workspace(),
WorkspaceSolid(),
)
val allResIds get() = persistentListOf(
@@ -884,6 +892,7 @@ object CompoundIcons {
R.drawable.ic_compound_end_call,
R.drawable.ic_compound_error,
R.drawable.ic_compound_error_solid,
R.drawable.ic_compound_exit_full_screen,
R.drawable.ic_compound_expand,
R.drawable.ic_compound_explore,
R.drawable.ic_compound_export_archive,
@@ -895,6 +904,7 @@ object CompoundIcons {
R.drawable.ic_compound_files,
R.drawable.ic_compound_filter,
R.drawable.ic_compound_forward,
R.drawable.ic_compound_full_screen,
R.drawable.ic_compound_grid,
R.drawable.ic_compound_group,
R.drawable.ic_compound_guest,
@@ -921,6 +931,7 @@ object CompoundIcons {
R.drawable.ic_compound_keyboard,
R.drawable.ic_compound_labs,
R.drawable.ic_compound_leave,
R.drawable.ic_compound_left_panel_close,
R.drawable.ic_compound_link,
R.drawable.ic_compound_linux,
R.drawable.ic_compound_list_bulleted,
@@ -990,6 +1001,8 @@ object CompoundIcons {
R.drawable.ic_compound_shield,
R.drawable.ic_compound_sidebar,
R.drawable.ic_compound_sign_out,
R.drawable.ic_compound_space,
R.drawable.ic_compound_space_solid,
R.drawable.ic_compound_spinner,
R.drawable.ic_compound_spotlight,
R.drawable.ic_compound_spotlight_view,
@@ -1029,7 +1042,5 @@ object CompoundIcons {
R.drawable.ic_compound_warning,
R.drawable.ic_compound_web_browser,
R.drawable.ic_compound_windows,
R.drawable.ic_compound_workspace,
R.drawable.ic_compound_workspace_solid,
)
}

View File

@@ -1,19 +1,10 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
/**
* !!! WARNING !!!
*
@@ -21,12 +12,14 @@ import androidx.compose.ui.graphics.Color
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated
import androidx.compose.ui.graphics.Color
/**
* This class holds all the semantic tokens of the Compound theme.
*/
* This class holds all the semantic tokens of the Compound theme.
*/
data class SemanticColors(
/** Background colour for accent or brand actions. State: Hover */
val bgAccentHovered: Color,
@@ -50,17 +43,21 @@ data class SemanticColors(
val bgActionSecondaryPressed: Color,
/** Background colour for secondary actions. State: Rest. */
val bgActionSecondaryRest: Color,
/** Background colour for tertiary actions. State: Hover */
val bgActionTertiaryHovered: Color,
/** Background colour for tertiary actions. State: Rest */
val bgActionTertiaryRest: Color,
/** Background colour for tertiary actions. State: Selected */
val bgActionTertiarySelected: Color,
/** Badge accent background colour */
val bgBadgeAccent: Color,
/** Badge default background colour */
val bgBadgeDefault: Color,
/** Badge info background colour */
val bgBadgeInfo: Color,
/** Default global background for the user interface.
Elevation: Default (Level 0) */
/** Default global background for the user interface. Elevation: Default (Level 0) */
val bgCanvasDefault: Color,
/** Default global background for the user interface.
Elevation: Level 1. */
/** Default global background for the user interface. Elevation: Level 1. */
val bgCanvasDefaultLevel1: Color,
/** Default background for disabled elements. There's no minimum contrast requirement. */
val bgCanvasDisabled: Color,
@@ -86,14 +83,11 @@ Elevation: Level 1. */
val bgDecorative6: Color,
/** Subtle background colour for informational elements. State: Rest. */
val bgInfoSubtle: Color,
/** Medium contrast surfaces.
Elevation: Default (Level 2). */
/** Medium contrast surfaces. Elevation: Default (Level 2). */
val bgSubtlePrimary: Color,
/** Low contrast surfaces.
Elevation: Default (Level 1). */
/** Low contrast surfaces. Elevation: Default (Level 1). */
val bgSubtleSecondary: Color,
/** Lower contrast surfaces.
Elevation: Level 0. */
/** Lower contrast surfaces. Elevation: Level 0. */
val bgSubtleSecondaryLevel0: Color,
/** Subtle background colour for success state elements. State: Rest. */
val bgSuccessSubtle: Color,

View File

@@ -1,6 +1,5 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
@@ -13,8 +12,6 @@
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated
@@ -37,9 +34,12 @@ val compoundColorsDark = SemanticColors(
bgActionSecondaryHovered = DarkColorTokens.colorAlphaGray200,
bgActionSecondaryPressed = DarkColorTokens.colorAlphaGray300,
bgActionSecondaryRest = DarkColorTokens.colorThemeBg,
bgBadgeAccent = DarkColorTokens.colorAlphaGreen300,
bgBadgeDefault = DarkColorTokens.colorAlphaGray300,
bgBadgeInfo = DarkColorTokens.colorAlphaBlue300,
bgActionTertiaryHovered = DarkColorTokens.colorGray300,
bgActionTertiaryRest = DarkColorTokens.colorThemeBg,
bgActionTertiarySelected = DarkColorTokens.colorGray400,
bgBadgeAccent = DarkColorTokens.colorAlphaGreen500,
bgBadgeDefault = DarkColorTokens.colorAlphaGray500,
bgBadgeInfo = DarkColorTokens.colorAlphaBlue500,
bgCanvasDefault = DarkColorTokens.colorThemeBg,
bgCanvasDefaultLevel1 = DarkColorTokens.colorGray300,
bgCanvasDisabled = DarkColorTokens.colorGray200,
@@ -89,7 +89,7 @@ val compoundColorsDark = SemanticColors(
iconAccentTertiary = DarkColorTokens.colorGreen800,
iconCriticalPrimary = DarkColorTokens.colorRed900,
iconDisabled = DarkColorTokens.colorGray700,
iconInfoPrimary = DarkColorTokens.colorBlue900,
iconInfoPrimary = DarkColorTokens.colorBlue1100,
iconOnSolidPrimary = DarkColorTokens.colorThemeBg,
iconPrimary = DarkColorTokens.colorGray1400,
iconPrimaryAlpha = DarkColorTokens.colorAlphaGray1400,
@@ -112,8 +112,8 @@ val compoundColorsDark = SemanticColors(
textDecorative5 = DarkColorTokens.colorPink1100,
textDecorative6 = DarkColorTokens.colorOrange1100,
textDisabled = DarkColorTokens.colorGray800,
textInfoPrimary = DarkColorTokens.colorBlue900,
textLinkExternal = DarkColorTokens.colorBlue900,
textInfoPrimary = DarkColorTokens.colorBlue1100,
textLinkExternal = DarkColorTokens.colorBlue1100,
textOnSolidPrimary = DarkColorTokens.colorThemeBg,
textPrimary = DarkColorTokens.colorGray1400,
textSecondary = DarkColorTokens.colorGray900,

View File

@@ -1,6 +1,5 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
@@ -13,8 +12,6 @@
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated
@@ -37,9 +34,12 @@ val compoundColorsHcDark = SemanticColors(
bgActionSecondaryHovered = DarkHcColorTokens.colorAlphaGray200,
bgActionSecondaryPressed = DarkHcColorTokens.colorAlphaGray300,
bgActionSecondaryRest = DarkHcColorTokens.colorThemeBg,
bgBadgeAccent = DarkHcColorTokens.colorAlphaGreen300,
bgBadgeDefault = DarkHcColorTokens.colorAlphaGray300,
bgBadgeInfo = DarkHcColorTokens.colorAlphaBlue300,
bgActionTertiaryHovered = DarkHcColorTokens.colorGray300,
bgActionTertiaryRest = DarkHcColorTokens.colorThemeBg,
bgActionTertiarySelected = DarkHcColorTokens.colorGray400,
bgBadgeAccent = DarkHcColorTokens.colorAlphaGreen500,
bgBadgeDefault = DarkHcColorTokens.colorAlphaGray500,
bgBadgeInfo = DarkHcColorTokens.colorAlphaBlue500,
bgCanvasDefault = DarkHcColorTokens.colorThemeBg,
bgCanvasDefaultLevel1 = DarkHcColorTokens.colorGray300,
bgCanvasDisabled = DarkHcColorTokens.colorGray200,
@@ -89,7 +89,7 @@ val compoundColorsHcDark = SemanticColors(
iconAccentTertiary = DarkHcColorTokens.colorGreen800,
iconCriticalPrimary = DarkHcColorTokens.colorRed900,
iconDisabled = DarkHcColorTokens.colorGray700,
iconInfoPrimary = DarkHcColorTokens.colorBlue900,
iconInfoPrimary = DarkHcColorTokens.colorBlue1100,
iconOnSolidPrimary = DarkHcColorTokens.colorThemeBg,
iconPrimary = DarkHcColorTokens.colorGray1400,
iconPrimaryAlpha = DarkHcColorTokens.colorAlphaGray1400,
@@ -112,8 +112,8 @@ val compoundColorsHcDark = SemanticColors(
textDecorative5 = DarkHcColorTokens.colorPink1100,
textDecorative6 = DarkHcColorTokens.colorOrange1100,
textDisabled = DarkHcColorTokens.colorGray800,
textInfoPrimary = DarkHcColorTokens.colorBlue900,
textLinkExternal = DarkHcColorTokens.colorBlue900,
textInfoPrimary = DarkHcColorTokens.colorBlue1100,
textLinkExternal = DarkHcColorTokens.colorBlue1100,
textOnSolidPrimary = DarkHcColorTokens.colorThemeBg,
textPrimary = DarkHcColorTokens.colorGray1400,
textSecondary = DarkHcColorTokens.colorGray900,

View File

@@ -1,6 +1,5 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
@@ -13,8 +12,6 @@
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated
@@ -37,9 +34,12 @@ val compoundColorsLight = SemanticColors(
bgActionSecondaryHovered = LightColorTokens.colorAlphaGray200,
bgActionSecondaryPressed = LightColorTokens.colorAlphaGray300,
bgActionSecondaryRest = LightColorTokens.colorThemeBg,
bgBadgeAccent = LightColorTokens.colorAlphaGreen300,
bgBadgeDefault = LightColorTokens.colorAlphaGray300,
bgBadgeInfo = LightColorTokens.colorAlphaBlue300,
bgActionTertiaryHovered = LightColorTokens.colorGray300,
bgActionTertiaryRest = LightColorTokens.colorThemeBg,
bgActionTertiarySelected = LightColorTokens.colorGray400,
bgBadgeAccent = LightColorTokens.colorAlphaGreen400,
bgBadgeDefault = LightColorTokens.colorAlphaGray400,
bgBadgeInfo = LightColorTokens.colorAlphaBlue400,
bgCanvasDefault = LightColorTokens.colorThemeBg,
bgCanvasDefaultLevel1 = LightColorTokens.colorThemeBg,
bgCanvasDisabled = LightColorTokens.colorGray200,
@@ -89,7 +89,7 @@ val compoundColorsLight = SemanticColors(
iconAccentTertiary = LightColorTokens.colorGreen800,
iconCriticalPrimary = LightColorTokens.colorRed900,
iconDisabled = LightColorTokens.colorGray700,
iconInfoPrimary = LightColorTokens.colorBlue900,
iconInfoPrimary = LightColorTokens.colorBlue1100,
iconOnSolidPrimary = LightColorTokens.colorThemeBg,
iconPrimary = LightColorTokens.colorGray1400,
iconPrimaryAlpha = LightColorTokens.colorAlphaGray1400,
@@ -112,8 +112,8 @@ val compoundColorsLight = SemanticColors(
textDecorative5 = LightColorTokens.colorPink1100,
textDecorative6 = LightColorTokens.colorOrange1100,
textDisabled = LightColorTokens.colorGray800,
textInfoPrimary = LightColorTokens.colorBlue900,
textLinkExternal = LightColorTokens.colorBlue900,
textInfoPrimary = LightColorTokens.colorBlue1100,
textLinkExternal = LightColorTokens.colorBlue1100,
textOnSolidPrimary = LightColorTokens.colorThemeBg,
textPrimary = LightColorTokens.colorGray1400,
textSecondary = LightColorTokens.colorGray900,

View File

@@ -1,6 +1,5 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
@@ -13,8 +12,6 @@
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated
@@ -37,9 +34,12 @@ val compoundColorsHcLight = SemanticColors(
bgActionSecondaryHovered = LightHcColorTokens.colorAlphaGray200,
bgActionSecondaryPressed = LightHcColorTokens.colorAlphaGray300,
bgActionSecondaryRest = LightHcColorTokens.colorThemeBg,
bgBadgeAccent = LightHcColorTokens.colorAlphaGreen300,
bgBadgeDefault = LightHcColorTokens.colorAlphaGray300,
bgBadgeInfo = LightHcColorTokens.colorAlphaBlue300,
bgActionTertiaryHovered = LightHcColorTokens.colorGray300,
bgActionTertiaryRest = LightHcColorTokens.colorThemeBg,
bgActionTertiarySelected = LightHcColorTokens.colorGray400,
bgBadgeAccent = LightHcColorTokens.colorAlphaGreen400,
bgBadgeDefault = LightHcColorTokens.colorAlphaGray400,
bgBadgeInfo = LightHcColorTokens.colorAlphaBlue400,
bgCanvasDefault = LightHcColorTokens.colorThemeBg,
bgCanvasDefaultLevel1 = LightHcColorTokens.colorThemeBg,
bgCanvasDisabled = LightHcColorTokens.colorGray200,
@@ -89,7 +89,7 @@ val compoundColorsHcLight = SemanticColors(
iconAccentTertiary = LightHcColorTokens.colorGreen800,
iconCriticalPrimary = LightHcColorTokens.colorRed900,
iconDisabled = LightHcColorTokens.colorGray700,
iconInfoPrimary = LightHcColorTokens.colorBlue900,
iconInfoPrimary = LightHcColorTokens.colorBlue1100,
iconOnSolidPrimary = LightHcColorTokens.colorThemeBg,
iconPrimary = LightHcColorTokens.colorGray1400,
iconPrimaryAlpha = LightHcColorTokens.colorAlphaGray1400,
@@ -112,8 +112,8 @@ val compoundColorsHcLight = SemanticColors(
textDecorative5 = LightHcColorTokens.colorPink1100,
textDecorative6 = LightHcColorTokens.colorOrange1100,
textDisabled = LightHcColorTokens.colorGray800,
textInfoPrimary = LightHcColorTokens.colorBlue900,
textLinkExternal = LightHcColorTokens.colorBlue900,
textInfoPrimary = LightHcColorTokens.colorBlue1100,
textLinkExternal = LightHcColorTokens.colorBlue1100,
textOnSolidPrimary = LightHcColorTokens.colorThemeBg,
textPrimary = LightHcColorTokens.colorGray1400,
textSecondary = LightHcColorTokens.colorGray900,

View File

@@ -1,12 +1,10 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
/**
* !!! WARNING !!!
*
@@ -14,8 +12,6 @@
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated

View File

@@ -1,6 +1,5 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
@@ -13,8 +12,6 @@
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated.internal

View File

@@ -1,6 +1,5 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
@@ -13,8 +12,6 @@
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated.internal

View File

@@ -1,6 +1,5 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
@@ -13,8 +12,6 @@
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated.internal

View File

@@ -1,6 +1,5 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
@@ -13,8 +12,6 @@
* DO NOT EDIT MANUALLY.
*/
@file:Suppress("all")
package io.element.android.compound.tokens.generated.internal

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10,20a1,1 0,1 1,-2 0v-4L4,16a1,1 0,1 1,0 -2h6zM20,14a1,1 0,1 1,0 2h-4v4a1,1 0,1 1,-2 0v-6zM9,3a1,1 0,0 1,1 1v6L4,10a1,1 0,0 1,0 -2h4L8,4a1,1 0,0 1,1 -1m6,0a1,1 0,0 1,1 1v4h4a1,1 0,1 1,0 2h-6L14,4a1,1 0,0 1,1 -1"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,14a1,1 0,0 1,1 1v4h4a1,1 0,1 1,0 2L3,21v-6a1,1 0,0 1,1 -1m16,0a1,1 0,0 1,1 1v6h-6a1,1 0,1 1,0 -2h4v-4a1,1 0,0 1,1 -1M9,3a1,1 0,0 1,0 2L5,5v4a1,1 0,0 1,-2 0L3,3zM21,9a1,1 0,1 1,-2 0L19,5h-4a1,1 0,1 1,0 -2h6z"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M16.5,14.8L16.5,9.2q0,-0.35 -0.3,-0.475t-0.55,0.125L13.2,11.3q-0.3,0.3 -0.3,0.7t0.3,0.7l2.45,2.45q0.25,0.25 0.55,0.125t0.3,-0.475M5,19q-0.824,0 -1.412,-0.587A1.93,1.93 0,0 1,3 17L3,7q0,-0.824 0.587,-1.412A1.93,1.93 0,0 1,5 5h14q0.824,0 1.413,0.588Q21,6.175 21,7v10q0,0.824 -0.587,1.413A1.93,1.93 0,0 1,19 19zM8,17L8,7L5,7v10zM10,17h9L19,7h-9z"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,21q-1.65,0 -2.825,-1.175T2,17t1.175,-2.825T6,13t2.825,1.175T10,17t-1.175,2.825T6,21m12,0q-1.65,0 -2.825,-1.175T14,17t1.175,-2.825T18,13t2.825,1.175T22,17t-1.175,2.825T18,21M6,19q0.824,0 1.412,-0.587Q8,17.825 8,17t-0.588,-1.412A1.93,1.93 0,0 0,6 15q-0.824,0 -1.412,0.588A1.93,1.93 0,0 0,4 17q0,0.824 0.588,1.413Q5.175,19 6,19m12,0q0.824,0 1.413,-0.587Q20,17.825 20,17t-0.587,-1.412A1.93,1.93 0,0 0,18 15q-0.824,0 -1.413,0.588A1.93,1.93 0,0 0,16 17q0,0.824 0.587,1.413Q17.176,19 18,19m-6,-8q-1.65,0 -2.825,-1.175T8,7t1.175,-2.825T12,3t2.825,1.175T16,7t-1.175,2.825T12,11m0,-2q0.825,0 1.412,-0.588Q14,7.826 14,7q0,-0.824 -0.588,-1.412A1.93,1.93 0,0 0,12 5q-0.825,0 -1.412,0.588A1.93,1.93 0,0 0,10 7q0,0.824 0.588,1.412Q11.175,9 12,9"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3.175,19.825Q4.35,21 6,21t2.825,-1.175T10,17t-1.175,-2.825T6,13t-2.825,1.175T2,17t1.175,2.825m12,0Q16.35,21 18,21t2.825,-1.175T22,17t-1.175,-2.825T18,13t-2.825,1.175T14,17t1.175,2.825m-6,-10Q10.35,11 12,11t2.825,-1.175T16,7t-1.175,-2.825T12,3 9.175,4.175 8,7t1.175,2.825"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -4,10 +4,10 @@
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7 10a0.97 0.97 0 0 1-0.71-0.29A0.97 0.97 0 0 1 6 9q0-0.42 0.29-0.71A0.97 0.97 0 0 1 7 8h10q0.42 0 0.71 0.29T18 9t-0.29 0.71A0.97 0.97 0 0 1 17 10z m0 4a0.97 0.97 0 0 1-0.71-0.29A0.97 0.97 0 0 1 6 13q0-0.42 0.29-0.71A0.97 0.97 0 0 1 7 12h6q0.42 0 0.71 0.29T14 13q0 0.42-0.29 0.71A0.97 0.97 0 0 1 13 14z"/>
<path
android:fillColor="#FF000000"
android:pathData="M3.7 21.3C3.09 21.91 2 21.47 2 20.58V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6zM6 17h14V5H4v13.17l0.59-0.58A2 2 0 0 1 6 17"/>
<path
android:pathData="M7,10a0.97,0.97 0,0 1,-0.713 -0.287A0.97,0.97 0,0 1,6 9q0,-0.424 0.287,-0.713A0.97,0.97 0,0 1,7 8h10q0.424,0 0.712,0.287Q18,8.576 18,9t-0.288,0.713A0.97,0.97 0,0 1,17 10zM7,14a0.97,0.97 0,0 1,-0.713 -0.287A0.97,0.97 0,0 1,6 13q0,-0.424 0.287,-0.713A0.97,0.97 0,0 1,7 12h6q0.424,0 0.713,0.287 0.287,0.288 0.287,0.713 0,0.424 -0.287,0.713A0.97,0.97 0,0 1,13 14z"
android:fillColor="#FF000000"/>
<path
android:pathData="M3.707,21.293c-0.63,0.63 -1.707,0.184 -1.707,-0.707V5a2,2 0,0 1,2 -2h16a2,2 0,0 1,2 2v12a2,2 0,0 1,-2 2H6zM6,17h14V5H4v13.172l0.586,-0.586A2,2 0,0 1,6 17"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -4,7 +4,7 @@
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.3 2.3C3.09 21.91 2 21.47 2 20.58V5a2 2 0 0 1 2-2m3 7h10q0.42 0 0.71-0.29A0.97 0.97 0 0 0 18 9a0.97 0.97 0 0 0-0.29-0.71A0.97 0.97 0 0 0 17 8H7a0.97 0.97 0 0 0-0.71 0.29A0.97 0.97 0 0 0 6 9q0 0.42 0.29 0.71T7 10m0 4h6q0.42 0 0.71-0.29A0.97 0.97 0 0 0 14 13a0.97 0.97 0 0 0-0.29-0.71A0.97 0.97 0 0 0 13 12H7a0.97 0.97 0 0 0-0.71 0.29A0.97 0.97 0 0 0 6 13q0 0.42 0.29 0.71T7 14"/>
<path
android:pathData="M4,3h16a2,2 0,0 1,2 2v12a2,2 0,0 1,-2 2H6l-2.293,2.293c-0.63,0.63 -1.707,0.184 -1.707,-0.707V5a2,2 0,0 1,2 -2m3,7h10q0.424,0 0.712,-0.287A0.97,0.97 0,0 0,18 9a0.97,0.97 0,0 0,-0.288 -0.713A0.97,0.97 0,0 0,17 8H7a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,6 9q0,0.424 0.287,0.713Q6.576,10 7,10m0,4h6q0.424,0 0.713,-0.287A0.97,0.97 0,0 0,14 13a0.97,0.97 0,0 0,-0.287 -0.713A0.97,0.97 0,0 0,13 12H7a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,6 13q0,0.424 0.287,0.713Q6.576,14 7,14"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,21q-1.65,0 -2.825,-1.175T2,17t1.175,-2.825T6,13t2.825,1.175T10,17t-1.175,2.825T6,21m12,0q-1.65,0 -2.825,-1.175T14,17t1.175,-2.825T18,13t2.825,1.175T22,17t-1.175,2.825T18,21M6,19q0.824,0 1.412,-0.587Q8,17.825 8,17t-0.588,-1.412A1.93,1.93 0,0 0,6 15q-0.824,0 -1.412,0.588A1.93,1.93 0,0 0,4 17q0,0.824 0.588,1.413Q5.175,19 6,19m12,0q0.824,0 1.413,-0.587Q20,17.825 20,17t-0.587,-1.412A1.93,1.93 0,0 0,18 15q-0.824,0 -1.413,0.588A1.93,1.93 0,0 0,16 17q0,0.824 0.587,1.413Q17.176,19 18,19m-6,-8q-1.65,0 -2.825,-1.175T8,7t1.175,-2.825T12,3t2.825,1.175T16,7t-1.175,2.825T12,11m0,-2q0.825,0 1.412,-0.588Q14,7.826 14,7q0,-0.824 -0.588,-1.412A1.93,1.93 0,0 0,12 5q-0.825,0 -1.412,0.588A1.93,1.93 0,0 0,10 7q0,0.824 0.588,1.412Q11.175,9 12,9"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3.175,19.825Q4.35,21 6,21t2.825,-1.175T10,17t-1.175,-2.825T6,13t-2.825,1.175T2,17t1.175,2.825m12,0Q16.35,21 18,21t2.825,-1.175T22,17t-1.175,-2.825T18,13t-2.825,1.175T14,17t1.175,2.825m-6,-10Q10.35,11 12,11t2.825,-1.175T16,7t-1.175,-2.825T12,3 9.175,4.175 8,7t1.175,2.825"
android:fillColor="#FF000000"/>
</vector>

View File

@@ -21,7 +21,7 @@ fun String.md5() = try {
digest.digest()
.joinToString("") { String.format(locale, "%02X", it) }
.lowercase(locale)
} catch (exc: Exception) {
} catch (_: Exception) {
// Should not happen, but just in case
hashCode().toString()
}

View File

@@ -14,7 +14,7 @@ fun String.isValidUrl(): Boolean {
return try {
URI(this).toURL()
true
} catch (t: Throwable) {
} catch (_: Throwable) {
false
}
}

View File

@@ -10,6 +10,7 @@ package io.element.android.libraries.deeplink.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.text.urlEncoded
import io.element.android.libraries.deeplink.api.DeepLinkCreator
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
@@ -21,13 +22,13 @@ class DefaultDeepLinkCreator : DeepLinkCreator {
override fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?, eventId: EventId?): String {
return buildString {
append("$SCHEME://$HOST/")
append(sessionId.value)
append(sessionId.value.urlEncoded())
append("/")
append(roomId?.value.orEmpty())
append(roomId?.value?.urlEncoded().orEmpty())
append("/")
append(threadId?.value.orEmpty())
append(threadId?.value?.urlEncoded().orEmpty())
append("/")
append(eventId?.value.orEmpty())
append(eventId?.value?.urlEncoded().orEmpty())
}
// Remove all possible trailing '/' characters:
// No event id

View File

@@ -12,6 +12,7 @@ import android.content.Intent
import android.net.Uri
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.text.urlDecoded
import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.deeplink.api.DeeplinkParser
import io.element.android.libraries.matrix.api.core.EventId
@@ -31,7 +32,7 @@ class DefaultDeeplinkParser : DeeplinkParser {
private fun Uri.toDeeplinkData(): DeeplinkData? {
if (scheme != SCHEME) return null
if (host != HOST) return null
val pathBits = path.orEmpty().split("/").drop(1)
val pathBits = encodedPath.orEmpty().split("/").drop(1).map { it.urlDecoded() }
val sessionId = pathBits.elementAtOrNull(0)?.let(::SessionId) ?: return null
return when (val screenPathComponent = pathBits.elementAtOrNull(1)) {

Some files were not shown because too many files have changed in this diff Show More