Merge branch 'develop' into separate_import_error
This commit is contained in:
commit
8f8e190e68
2773 changed files with 29051 additions and 10914 deletions
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector 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.
|
||||
|
|
@ -9,10 +9,10 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.sessionstorage.impl.memory"
|
||||
namespace = "io.element.android.libraries.accountselect.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.accountselect.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface AccountSelectEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onSelectAccount(sessionId: SessionId)
|
||||
fun onCancel()
|
||||
}
|
||||
}
|
||||
35
libraries/accountselect/impl/build.gradle.kts
Normal file
35
libraries/accountselect/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.accountselect.impl"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.libraries.accountselect.api)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.accountselect.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
class AccountSelectNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: AccountSelectPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private val callbacks = plugins.filterIsInstance<AccountSelectEntryPoint.Callback>()
|
||||
|
||||
private fun onDismiss() {
|
||||
callbacks.forEach { it.onCancel() }
|
||||
}
|
||||
|
||||
private fun onSelectAccount(sessionId: SessionId) {
|
||||
callbacks.forEach { it.onSelectAccount(sessionId) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
AccountSelectView(
|
||||
state = state,
|
||||
onDismiss = ::onDismiss,
|
||||
onSelectAccount = ::onSelectAccount,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.accountselect.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Inject
|
||||
class AccountSelectPresenter(
|
||||
private val sessionStore: SessionStore,
|
||||
) : Presenter<AccountSelectState> {
|
||||
@Composable
|
||||
override fun present(): AccountSelectState {
|
||||
val accounts by produceState(persistentListOf()) {
|
||||
// Do not use sessionStore.sessionsFlow() to not make it change when an account is selected.
|
||||
value = sessionStore.getAllSessions()
|
||||
.map {
|
||||
MatrixUser(
|
||||
userId = UserId(it.userId),
|
||||
displayName = it.userDisplayName,
|
||||
avatarUrl = it.userAvatarUrl,
|
||||
)
|
||||
}
|
||||
.toPersistentList()
|
||||
}
|
||||
|
||||
return AccountSelectState(
|
||||
accounts = accounts,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.accountselect.impl
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class AccountSelectState(
|
||||
val accounts: ImmutableList<MatrixUser>,
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.accountselect.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
open class AccountSelectStateProvider : PreviewParameterProvider<AccountSelectState> {
|
||||
override val values: Sequence<AccountSelectState>
|
||||
get() = sequenceOf(
|
||||
anAccountSelectState(),
|
||||
anAccountSelectState(accounts = aMatrixUserList()),
|
||||
)
|
||||
}
|
||||
|
||||
private fun anAccountSelectState(
|
||||
accounts: List<MatrixUser> = listOf(),
|
||||
) = AccountSelectState(
|
||||
accounts = accounts.toPersistentList(),
|
||||
)
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.accountselect.impl
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Suppress("MultipleEmitters") // False positive
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountSelectView(
|
||||
state: AccountSelectState,
|
||||
onSelectAccount: (SessionId) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler(onBack = { onDismiss() })
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
titleStr = stringResource(CommonStrings.common_select_account),
|
||||
navigationIcon = {
|
||||
BackButton(onClick = { onDismiss() })
|
||||
},
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
) {
|
||||
LazyColumn {
|
||||
items(state.accounts, key = { it.userId }) { matrixUser ->
|
||||
Column {
|
||||
MatrixUserRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onSelectAccount(matrixUser.userId)
|
||||
}
|
||||
.padding(vertical = 8.dp),
|
||||
matrixUser = matrixUser,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun AccountSelectViewPreview(@PreviewParameter(AccountSelectStateProvider::class) state: AccountSelectState) = ElementPreview {
|
||||
AccountSelectView(
|
||||
state = state,
|
||||
onSelectAccount = {},
|
||||
onDismiss = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.accountselect.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultAccountSelectEntryPoint : AccountSelectEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): AccountSelectEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : AccountSelectEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: AccountSelectEntryPoint.Callback): AccountSelectEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<AccountSelectNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.accountselect.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class AccountSelectPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createAccountSelectPresenter()
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.accounts).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - multiple accounts case`() = runTest {
|
||||
val presenter = createAccountSelectPresenter(
|
||||
sessionStore = InMemorySessionStore(
|
||||
initialList = listOf(
|
||||
aSessionData(sessionId = A_SESSION_ID.value),
|
||||
aSessionData(
|
||||
sessionId = A_SESSION_ID_2.value,
|
||||
userDisplayName = "Bob",
|
||||
userAvatarUrl = "avatarUrl",
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.accounts).hasSize(2)
|
||||
val firstAccount = initialState.accounts[0]
|
||||
assertThat(firstAccount).isEqualTo(
|
||||
MatrixUser(
|
||||
userId = A_SESSION_ID,
|
||||
displayName = null,
|
||||
avatarUrl = null,
|
||||
)
|
||||
)
|
||||
val secondAccount = initialState.accounts[1]
|
||||
assertThat(secondAccount).isEqualTo(
|
||||
MatrixUser(
|
||||
userId = A_SESSION_ID_2,
|
||||
displayName = "Bob",
|
||||
avatarUrl = "avatarUrl",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createAccountSelectPresenter(
|
||||
sessionStore: SessionStore = InMemorySessionStore(),
|
||||
) = AccountSelectPresenter(
|
||||
sessionStore = sessionStore,
|
||||
)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.accountselect.impl
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultAccountSelectEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultAccountSelectEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
AccountSelectNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenter = createAccountSelectPresenter(),
|
||||
)
|
||||
}
|
||||
val callback = object : AccountSelectEntryPoint.Callback {
|
||||
override fun onSelectAccount(sessionId: SessionId) = lambdaError()
|
||||
override fun onCancel() = lambdaError()
|
||||
}
|
||||
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
|
||||
.callback(callback)
|
||||
.build()
|
||||
assertThat(result).isInstanceOf(AccountSelectNode::class.java)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -18,26 +19,22 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.di)
|
||||
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.services.toolbox.api)
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.activity.activity)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
implementation(libs.androidx.exifinterface)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
api(libs.androidx.browser)
|
||||
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,16 @@ import android.content.ClipData
|
|||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import javax.inject.Inject
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class AndroidClipboardHelper @Inject constructor(
|
||||
@Inject
|
||||
class AndroidClipboardHelper(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : ClipboardHelper {
|
||||
private val clipboardManager = requireNotNull(context.getSystemService<ClipboardManager>())
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ package io.element.android.libraries.androidutils.file
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
interface TemporaryUriDeleter {
|
||||
/**
|
||||
|
|
@ -23,7 +23,8 @@ interface TemporaryUriDeleter {
|
|||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultTemporaryUriDeleter @Inject constructor(
|
||||
@Inject
|
||||
class DefaultTemporaryUriDeleter(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : TemporaryUriDeleter {
|
||||
private val baseCacheUri = "content://${context.packageName}.fileprovider/cache"
|
||||
|
|
|
|||
|
|
@ -10,14 +10,15 @@ package io.element.android.libraries.androidutils.filesize
|
|||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.format.Formatter
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AndroidFileSizeFormatter @Inject constructor(
|
||||
@Inject
|
||||
class AndroidFileSizeFormatter(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
||||
) : FileSizeFormatter {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@ class VideoCompressorHelper(
|
|||
fun getOutputSize(inputSize: Size): Size {
|
||||
val resultMajor = min(inputSize.major(), maxSize)
|
||||
val aspectRatio = inputSize.major().toFloat() / inputSize.minor().toFloat()
|
||||
return Size(resultMajor, (resultMajor / aspectRatio).roundToInt())
|
||||
return if (inputSize.isLandscape()) {
|
||||
Size(resultMajor, (resultMajor / aspectRatio).roundToInt())
|
||||
} else {
|
||||
Size((resultMajor / aspectRatio).roundToInt(), resultMajor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -42,5 +46,6 @@ class VideoCompressorHelper(
|
|||
}
|
||||
}
|
||||
|
||||
internal fun Size.major(): Int = if (width > height) width else height
|
||||
internal fun Size.minor(): Int = if (width < height) width else height
|
||||
private fun Size.isLandscape(): Boolean = width > height
|
||||
private fun Size.major(): Int = if (isLandscape()) width else height
|
||||
private fun Size.minor(): Int = if (isLandscape()) height else width
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.preferences
|
||||
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
|
||||
object DefaultPreferencesCorruptionHandlerFactory {
|
||||
/**
|
||||
* Creates a [ReplaceFileCorruptionHandler] that will replace the corrupted preferences file with an empty preferences object.
|
||||
*/
|
||||
fun replaceWithEmpty(): ReplaceFileCorruptionHandler<Preferences> {
|
||||
return ReplaceFileCorruptionHandler(
|
||||
produceNewData = {
|
||||
// If the preferences file is corrupted, we return an empty preferences object
|
||||
emptyPreferences()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,15 +11,15 @@ import android.content.BroadcastReceiver
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.androidutils.system.DateTimeObserver.Event
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
interface DateTimeObserver {
|
||||
val changes: Flow<Event>
|
||||
|
|
@ -32,7 +32,8 @@ interface DateTimeObserver {
|
|||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultDateTimeObserver @Inject constructor(
|
||||
@Inject
|
||||
class DefaultDateTimeObserver(
|
||||
@ApplicationContext context: Context
|
||||
) : DateTimeObserver {
|
||||
private val dateTimeReceiver = object : BroadcastReceiver() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="error_no_compatible_app_found">"이 동작을 수행할 수 있는 앱을 찾지 못했습니다."</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.media
|
||||
|
||||
import android.util.Size
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class VideoCompressorHelperTest {
|
||||
@Test
|
||||
fun `test getOutputSize`() {
|
||||
val helper = VideoCompressorHelper(maxSize = 720)
|
||||
|
||||
// Landscape input
|
||||
var inputSize = Size(1920, 1080)
|
||||
var outputSize = helper.getOutputSize(inputSize)
|
||||
assertThat(outputSize).isEqualTo(Size(720, 405))
|
||||
|
||||
// Landscape input small height
|
||||
inputSize = Size(1920, 200)
|
||||
outputSize = helper.getOutputSize(inputSize)
|
||||
assertThat(outputSize).isEqualTo(Size(720, 75))
|
||||
|
||||
// Portrait input
|
||||
inputSize = Size(1080, 1920)
|
||||
outputSize = helper.getOutputSize(inputSize)
|
||||
assertThat(outputSize).isEqualTo(Size(405, 720))
|
||||
|
||||
// Portrait input small width
|
||||
inputSize = Size(200, 1920)
|
||||
outputSize = helper.getOutputSize(inputSize)
|
||||
assertThat(outputSize).isEqualTo(Size(75, 720))
|
||||
|
||||
// Square input
|
||||
inputSize = Size(1000, 1000)
|
||||
outputSize = helper.getOutputSize(inputSize)
|
||||
assertThat(outputSize).isEqualTo(Size(720, 720))
|
||||
|
||||
// Square input same size
|
||||
inputSize = Size(720, 720)
|
||||
outputSize = helper.getOutputSize(inputSize)
|
||||
assertThat(outputSize).isEqualTo(Size(720, 720))
|
||||
|
||||
// Square input no downscaling
|
||||
inputSize = Size(240, 240)
|
||||
outputSize = helper.getOutputSize(inputSize)
|
||||
assertThat(outputSize).isEqualTo(Size(240, 240))
|
||||
|
||||
// Small input landscape (no downscaling)
|
||||
inputSize = Size(640, 480)
|
||||
outputSize = helper.getOutputSize(inputSize)
|
||||
assertThat(outputSize).isEqualTo(Size(640, 480))
|
||||
|
||||
// Small input portrait (no downscaling)
|
||||
inputSize = Size(480, 640)
|
||||
outputSize = helper.getOutputSize(inputSize)
|
||||
assertThat(outputSize).isEqualTo(Size(480, 640))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test calculateOptimalBitrate`() {
|
||||
val helper = VideoCompressorHelper(maxSize = 720)
|
||||
val inputSize = Size(1920, 1080)
|
||||
var bitrate = helper.calculateOptimalBitrate(inputSize, frameRate = 30)
|
||||
// Output size will be 720x405, so bitrate = 720*405*0.1*30 = 874800
|
||||
assertThat(bitrate).isEqualTo(874_800L)
|
||||
// Half frame rate, half bitrate
|
||||
bitrate = helper.calculateOptimalBitrate(inputSize, frameRate = 15)
|
||||
assertThat(bitrate).isEqualTo(437_400L)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -13,15 +16,15 @@ android {
|
|||
namespace = "io.element.android.libraries.architecture"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.di)
|
||||
api(projects.libraries.core)
|
||||
api(libs.dagger)
|
||||
api(libs.metro.runtime)
|
||||
api(libs.appyx.core)
|
||||
api(libs.androidx.lifecycle.runtime)
|
||||
api(libs.molecule.runtime)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.test.truth)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ import com.bumble.appyx.core.modality.BuildContext
|
|||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
|
||||
interface AssistedNodeFactory<NODE : Node> {
|
||||
fun interface AssistedNodeFactory<NODE : Node> {
|
||||
fun create(buildContext: BuildContext, plugins: List<Plugin>): NODE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.architecture
|
|||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
|
@ -159,16 +160,19 @@ suspend inline fun <T> runUpdatingState(
|
|||
callsInPlace(resultBlock, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
state.value = AsyncAction.Loading
|
||||
return resultBlock().fold(
|
||||
return try {
|
||||
resultBlock()
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
state.value = AsyncAction.Failure(errorTransform(e))
|
||||
throw e
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
state.value = AsyncAction.Success(it)
|
||||
Result.success(it)
|
||||
},
|
||||
onFailure = {
|
||||
val error = errorTransform(it)
|
||||
state.value = AsyncAction.Failure(
|
||||
error = error,
|
||||
)
|
||||
state.value = AsyncAction.Failure(error)
|
||||
Result.failure(error)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,34 +10,30 @@ package io.element.android.libraries.architecture
|
|||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.di.DaggerComponentOwner
|
||||
import io.element.android.libraries.di.DependencyInjectionGraphOwner
|
||||
|
||||
inline fun <reified T : Any> Node.optionalBindings() = optionalBindings(T::class.java)
|
||||
inline fun <reified T : Any> Node.bindings() = bindings(T::class.java)
|
||||
inline fun <reified T : Any> Context.bindings() = bindings(T::class.java)
|
||||
|
||||
fun <T : Any> Context.bindings(klass: Class<T>): T {
|
||||
// search dagger components in the context hierarchy
|
||||
// search the components in the dependency injection graph
|
||||
return generateSequence(this) { (it as? ContextWrapper)?.baseContext }
|
||||
.plus(applicationContext)
|
||||
.filterIsInstance<DaggerComponentOwner>()
|
||||
.map { it.daggerComponent }
|
||||
.flatMap { if (it is Collection<*>) it else listOf(it) }
|
||||
.filterIsInstance<DependencyInjectionGraphOwner>()
|
||||
.map { it.graph }
|
||||
.flatMap { it as? Collection<*> ?: listOf(it) }
|
||||
.filterIsInstance(klass)
|
||||
.firstOrNull()
|
||||
?: error("Unable to find bindings for ${klass.name}")
|
||||
}
|
||||
|
||||
fun <T : Any> Node.optionalBindings(klass: Class<T>): T? {
|
||||
// search dagger components in node hierarchy
|
||||
fun <T : Any> Node.bindings(klass: Class<T>): T {
|
||||
// search the components in the node hierarchy
|
||||
return generateSequence(this, Node::parent)
|
||||
.filterIsInstance<DaggerComponentOwner>()
|
||||
.map { it.daggerComponent }
|
||||
.flatMap { if (it is Collection<*>) it else listOf(it) }
|
||||
.filterIsInstance<DependencyInjectionGraphOwner>()
|
||||
.map { it.graph }
|
||||
.flatMap { it as? Collection<*> ?: listOf(it) }
|
||||
.filterIsInstance(klass)
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
fun <T : Any> Node.bindings(klass: Class<T>): T {
|
||||
return optionalBindings(klass) ?: error("Unable to find bindings for ${klass.name}")
|
||||
?: error("Unable to find bindings for ${klass.name}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,6 @@ interface FeatureEntryPoint
|
|||
/**
|
||||
* Can be used when the feature only exposes a simple node without the need of plugins.
|
||||
*/
|
||||
interface SimpleFeatureEntryPoint : FeatureEntryPoint {
|
||||
fun interface SimpleFeatureEntryPoint : FeatureEntryPoint {
|
||||
fun createNode(parentNode: Node, buildContext: BuildContext): Node
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
|
||||
package io.element.android.libraries.architecture
|
||||
|
||||
import android.content.Context
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Multibinds
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
inline fun <reified N : Node> Node.createNode(
|
||||
buildContext: BuildContext,
|
||||
|
|
@ -20,23 +21,15 @@ inline fun <reified N : Node> Node.createNode(
|
|||
return bindings.createNode(buildContext, plugins)
|
||||
}
|
||||
|
||||
inline fun <reified N : Node> Context.createNode(
|
||||
buildContext: BuildContext,
|
||||
plugins: List<Plugin> = emptyList()
|
||||
): N {
|
||||
val bindings: NodeFactoriesBindings = bindings()
|
||||
return bindings.createNode(buildContext, plugins)
|
||||
}
|
||||
|
||||
inline fun <reified N : Node> NodeFactoriesBindings.createNode(
|
||||
buildContext: BuildContext,
|
||||
plugins: List<Plugin> = emptyList()
|
||||
plugins: List<Plugin>,
|
||||
): N {
|
||||
val nodeClass = N::class.java
|
||||
val nodeClass = N::class
|
||||
val nodeFactoryMap = nodeFactories()
|
||||
// Note to developers: If you got the error below, make sure to build again after
|
||||
// clearing the cache (sometimes several times) to let Dagger generate the NodeFactory.
|
||||
val nodeFactory = nodeFactoryMap[nodeClass] ?: error("Cannot find NodeFactory for ${nodeClass.name}.")
|
||||
// clearing the cache (sometimes several times) to let codegen generate the NodeFactory.
|
||||
val nodeFactory = nodeFactoryMap[nodeClass] ?: error("Cannot find NodeFactory for ${nodeClass.java.name}.")
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val castedNodeFactory = nodeFactory as? AssistedNodeFactory<N>
|
||||
|
|
@ -44,6 +37,7 @@ inline fun <reified N : Node> NodeFactoriesBindings.createNode(
|
|||
return node as N
|
||||
}
|
||||
|
||||
interface NodeFactoriesBindings {
|
||||
fun nodeFactories(): Map<Class<out Node>, AssistedNodeFactory<*>>
|
||||
fun interface NodeFactoriesBindings {
|
||||
@Multibinds
|
||||
fun nodeFactories(): Map<KClass<out Node>, AssistedNodeFactory<*>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
package io.element.android.libraries.architecture
|
||||
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import dagger.MapKey
|
||||
import dev.zacsweers.metro.MapKey
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@MapKey
|
||||
annotation class NodeKey(val value: KClass<out Node>)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.architecture.appyx
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.core.Transition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler
|
||||
import com.bumble.appyx.core.navigation.transition.TransitionDescriptor
|
||||
|
||||
/**
|
||||
* A [ModifierTransitionHandler] that delegates the creation of the modifier to another handler
|
||||
* based on the [NavTarget]. The idea is to allow different transitions for different [NavTarget]s.
|
||||
*/
|
||||
class DelegateTransitionHandler<NavTarget, State>(
|
||||
private val handlerProvider: (NavTarget) -> ModifierTransitionHandler<NavTarget, State>,
|
||||
) : ModifierTransitionHandler<NavTarget, State>() {
|
||||
@SuppressLint("ModifierFactoryExtensionFunction")
|
||||
override fun createModifier(modifier: Modifier, transition: Transition<State>, descriptor: TransitionDescriptor<NavTarget, State>): Modifier {
|
||||
return handlerProvider(descriptor.element).createModifier(modifier, transition, descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <NavTarget, State> rememberDelegateTransitionHandler(
|
||||
handlerProvider: (NavTarget) -> ModifierTransitionHandler<NavTarget, State>,
|
||||
): ModifierTransitionHandler<NavTarget, State> =
|
||||
remember(handlerProvider) { DelegateTransitionHandler(handlerProvider = handlerProvider) }
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.architecture
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class AsyncActionTest {
|
||||
@Test
|
||||
fun `updates state on timeout`() = runTest {
|
||||
val state: MutableState<AsyncAction<Int>> = mutableStateOf(AsyncAction.Uninitialized)
|
||||
val timeoutMillis = 500L
|
||||
val operationTimeMillis = 1000L
|
||||
|
||||
try {
|
||||
runUpdatingState(state = state) {
|
||||
withTimeout(timeoutMillis.milliseconds) {
|
||||
delay(operationTimeMillis)
|
||||
}
|
||||
Result.success(0)
|
||||
}
|
||||
fail("Expected TimeoutCancellationException, but nothing was thrown")
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
assertTrue(state.value.isFailure())
|
||||
assertSame(e, state.value.errorOrNull())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import extension.setupAnvil
|
||||
import extension.setupDependencyInjection
|
||||
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
|
|
@ -14,12 +14,11 @@ android {
|
|||
namespace = "io.element.android.libraries.audio.impl"
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.audio.api)
|
||||
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.di)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,15 +13,16 @@ import android.media.AudioFocusRequest
|
|||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.audio.api.AudioFocus
|
||||
import io.element.android.libraries.audio.api.AudioFocusRequester
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultAudioFocus @Inject constructor(
|
||||
@Inject
|
||||
class DefaultAudioFocus(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : AudioFocus {
|
||||
private val audioManager = requireNotNull(context.getSystemService<AudioManager>())
|
||||
|
|
|
|||
|
|
@ -8,9 +8,18 @@
|
|||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
data class CoroutineDispatchers(
|
||||
val io: CoroutineDispatcher,
|
||||
val computation: CoroutineDispatcher,
|
||||
val main: CoroutineDispatcher,
|
||||
)
|
||||
) {
|
||||
companion object {
|
||||
val Default = CoroutineDispatchers(
|
||||
io = Dispatchers.IO,
|
||||
computation = Dispatchers.Default,
|
||||
main = Dispatchers.Main,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -15,13 +16,11 @@ android {
|
|||
namespace = "io.element.android.libraries.cryptography.impl"
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.di)
|
||||
api(projects.libraries.cryptography.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,21 +7,22 @@
|
|||
|
||||
package io.element.android.libraries.cryptography.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.cryptography.api.AESEncryptionSpecs
|
||||
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
|
||||
import io.element.android.libraries.cryptography.api.EncryptionResult
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Default implementation of [EncryptionDecryptionService] using AES encryption.
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AESEncryptionDecryptionService @Inject constructor() : EncryptionDecryptionService {
|
||||
@Inject
|
||||
class AESEncryptionDecryptionService : EncryptionDecryptionService {
|
||||
override fun createEncryptionCipher(key: SecretKey): Cipher {
|
||||
return Cipher.getInstance(AESEncryptionSpecs.CIPHER_TRANSFORMATION).apply {
|
||||
init(Cipher.ENCRYPT_MODE, key)
|
||||
|
|
|
|||
|
|
@ -7,16 +7,16 @@
|
|||
|
||||
package io.element.android.libraries.cryptography.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.BindingContainer
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.Provides
|
||||
import java.security.KeyStore
|
||||
|
||||
internal const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
@Module
|
||||
@BindingContainer
|
||||
object CryptographyModule {
|
||||
@Provides
|
||||
fun providesAndroidKeyStore(): KeyStore {
|
||||
|
|
|
|||
|
|
@ -10,23 +10,24 @@ package io.element.android.libraries.cryptography.impl
|
|||
import android.annotation.SuppressLint
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.cryptography.api.AESEncryptionSpecs
|
||||
import io.element.android.libraries.cryptography.api.SecretKeyRepository
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import timber.log.Timber
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Default implementation of [SecretKeyRepository] that uses the Android Keystore to store the keys.
|
||||
* The generated key uses AES algorithm, with a key size of 128 bits, and the GCM block mode.
|
||||
*/
|
||||
@ContributesBinding(AppScope::class)
|
||||
class KeyStoreSecretKeyRepository @Inject constructor(
|
||||
@Inject
|
||||
class KeyStoreSecretKeyRepository(
|
||||
private val keyStore: KeyStore,
|
||||
) : SecretKeyRepository {
|
||||
// False positive lint issue
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -13,7 +15,6 @@ android {
|
|||
namespace = "io.element.android.libraries.dateformatter.api"
|
||||
|
||||
dependencies {
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -11,7 +12,7 @@ plugins {
|
|||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
setupDependencyInjection()
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.dateformatter.impl"
|
||||
|
|
@ -31,7 +32,6 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.di)
|
||||
|
|
@ -41,13 +41,8 @@ android {
|
|||
api(projects.libraries.dateformatter.api)
|
||||
api(libs.datetime)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs, true)
|
||||
testImplementation(projects.libraries.dateformatter.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.extensions.safeCapitalize
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
interface DateFormatterDay {
|
||||
fun format(
|
||||
|
|
@ -20,7 +20,8 @@ interface DateFormatterDay {
|
|||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultDateFormatterDay @Inject constructor(
|
||||
@Inject
|
||||
class DefaultDateFormatterDay(
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) : DateFormatterDay {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class DateFormatterFull @Inject constructor(
|
||||
@Inject
|
||||
class DateFormatterFull(
|
||||
private val stringProvider: StringProvider,
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.extensions.safeCapitalize
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class DateFormatterMonth @Inject constructor(
|
||||
@Inject
|
||||
class DateFormatterMonth(
|
||||
private val stringProvider: StringProvider,
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import javax.inject.Inject
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
||||
class DateFormatterTime @Inject constructor(
|
||||
@Inject
|
||||
class DateFormatterTime(
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import javax.inject.Inject
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
||||
class DateFormatterTimeOnly @Inject constructor(
|
||||
@Inject
|
||||
class DateFormatterTimeOnly(
|
||||
private val localDateTimeProvider: LocalDateTimeProvider,
|
||||
private val dateFormatters: DateFormatters,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@
|
|||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toJavaLocalDate
|
||||
|
|
@ -17,12 +18,12 @@ import kotlinx.datetime.toJavaLocalDateTime
|
|||
import timber.log.Timber
|
||||
import java.time.Period
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.time.Clock
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
class DateFormatters @Inject constructor(
|
||||
@Inject
|
||||
class DateFormatters(
|
||||
localeChangeObserver: LocaleChangeObserver,
|
||||
private val clock: Clock,
|
||||
private val timeZoneProvider: TimezoneProvider,
|
||||
|
|
|
|||
|
|
@ -7,14 +7,15 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultDateFormatter @Inject constructor(
|
||||
@Inject
|
||||
class DefaultDateFormatter(
|
||||
private val dateFormatterFull: DateFormatterFull,
|
||||
private val dateFormatterMonth: DateFormatterMonth,
|
||||
private val dateFormatterDay: DateFormatterDay,
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
class LocalDateTimeProvider @Inject constructor(
|
||||
@Inject
|
||||
class LocalDateTimeProvider(
|
||||
private val clock: Clock,
|
||||
private val timezoneProvider: TimezoneProvider,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import javax.inject.Inject
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
|
||||
fun interface LocaleChangeObserver {
|
||||
fun addListener(listener: LocaleChangeListener)
|
||||
|
|
@ -28,7 +28,8 @@ interface LocaleChangeListener {
|
|||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultLocaleChangeObserver @Inject constructor(
|
||||
@Inject
|
||||
class DefaultLocaleChangeObserver(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : LocaleChangeObserver {
|
||||
init {
|
||||
|
|
|
|||
|
|
@ -7,16 +7,16 @@
|
|||
|
||||
package io.element.android.libraries.dateformatter.impl.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.BindingContainer
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.Provides
|
||||
import io.element.android.libraries.dateformatter.impl.TimezoneProvider
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import kotlinx.datetime.TimeZone
|
||||
import java.util.Locale
|
||||
import kotlin.time.Clock
|
||||
|
||||
@Module
|
||||
@BindingContainer
|
||||
@ContributesTo(AppScope::class)
|
||||
object DateFormatterModule {
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="common_date_date_at_time">"%1$s itt: %2$s"</string>
|
||||
<string name="common_date_date_at_time">"%1$s, %2$s"</string>
|
||||
<string name="common_date_this_month">"Ebben a hónapban"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="common_date_date_at_time">"%2$s 에 %1$s"</string>
|
||||
<string name="common_date_this_month">"이번 달"</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="common_date_date_at_time">"%1$s la %2$s"</string>
|
||||
<string name="common_date_this_month">"Luna aceasta"</string>
|
||||
</resources>
|
||||
|
|
@ -4,7 +4,14 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
package io.element.android.libraries.di
|
||||
android {
|
||||
namespace = "io.element.android.libraries.deeplink.api"
|
||||
}
|
||||
|
||||
abstract class AppScope private constructor()
|
||||
dependencies {
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.deeplink.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
|
||||
fun interface DeepLinkCreator {
|
||||
fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.deeplink
|
||||
package io.element.android.libraries.deeplink.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.deeplink.api
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
fun interface DeeplinkParser {
|
||||
fun getFromIntent(intent: Intent): DeeplinkData?
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.deeplink.api.usecase
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
fun interface InviteFriendsUseCase {
|
||||
fun execute(activity: Activity)
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
|
|
@ -6,19 +7,20 @@ import extension.setupAnvil
|
|||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.deeplink"
|
||||
namespace = "io.element.android.libraries.deeplink.impl"
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.deeplink.api)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
|
@ -27,9 +29,6 @@ dependencies {
|
|||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.deeplink
|
||||
package io.element.android.libraries.deeplink.impl
|
||||
|
||||
internal const val SCHEME = "elementx"
|
||||
internal const val HOST = "open"
|
||||
|
|
@ -1,19 +1,24 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector 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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.deeplink
|
||||
package io.element.android.libraries.deeplink.impl
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.deeplink.api.DeepLinkCreator
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeepLinkCreator @Inject constructor() {
|
||||
fun room(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String {
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultDeepLinkCreator : DeepLinkCreator {
|
||||
override fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String {
|
||||
return buildString {
|
||||
append("$SCHEME://$HOST/")
|
||||
append(sessionId.value)
|
||||
|
|
@ -1,21 +1,27 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector 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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.deeplink
|
||||
package io.element.android.libraries.deeplink.impl
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.deeplink.api.DeeplinkParser
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeeplinkParser @Inject constructor() {
|
||||
fun getFromIntent(intent: Intent): DeeplinkData? {
|
||||
@ContributesBinding(AppScope::class)
|
||||
@Inject
|
||||
class DefaultDeeplinkParser : DeeplinkParser {
|
||||
override fun getFromIntent(intent: Intent): DeeplinkData? {
|
||||
return intent
|
||||
.takeIf { it.action == Intent.ACTION_VIEW }
|
||||
?.data
|
||||
|
|
@ -1,30 +1,35 @@
|
|||
/*
|
||||
* Copyright 2023, 2024 New Vector 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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.deeplink.usecase
|
||||
package io.element.android.libraries.deeplink.impl.usecase
|
||||
|
||||
import android.app.Activity
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import io.element.android.libraries.androidutils.R as AndroidUtilsR
|
||||
|
||||
class InviteFriendsUseCase @Inject constructor(
|
||||
@ContributesBinding(SessionScope::class)
|
||||
@Inject
|
||||
class DefaultInviteFriendsUseCase(
|
||||
private val stringProvider: StringProvider,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val permalinkBuilder: PermalinkBuilder,
|
||||
) {
|
||||
fun execute(activity: Activity) {
|
||||
) : InviteFriendsUseCase {
|
||||
override fun execute(activity: Activity) {
|
||||
val permalinkResult = permalinkBuilder.permalinkForUser(matrixClient.sessionId)
|
||||
permalinkResult.fold(
|
||||
onSuccess = { permalink ->
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.deeplink
|
||||
package io.element.android.libraries.deeplink.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
|
|
@ -13,15 +13,15 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import org.junit.Test
|
||||
|
||||
class DeepLinkCreatorTest {
|
||||
class DefaultDeepLinkCreatorTest {
|
||||
@Test
|
||||
fun room() {
|
||||
val sut = DeepLinkCreator()
|
||||
assertThat(sut.room(A_SESSION_ID, null, null))
|
||||
fun create() {
|
||||
val sut = DefaultDeepLinkCreator()
|
||||
assertThat(sut.create(A_SESSION_ID, null, null))
|
||||
.isEqualTo("elementx://open/@alice:server.org")
|
||||
assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, null))
|
||||
assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, null))
|
||||
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain")
|
||||
assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
|
||||
assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
|
||||
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,12 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.deeplink
|
||||
package io.element.android.libraries.deeplink.impl
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
|
|
@ -19,7 +20,7 @@ import org.junit.runner.RunWith
|
|||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DeeplinkParserTest {
|
||||
class DefaultDeeplinkParserTest {
|
||||
companion object {
|
||||
const val A_URI =
|
||||
"elementx://open/@alice:server.org"
|
||||
|
|
@ -29,10 +30,9 @@ class DeeplinkParserTest {
|
|||
"elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId"
|
||||
}
|
||||
|
||||
private val sut = DeeplinkParser()
|
||||
|
||||
@Test
|
||||
fun `nominal cases`() {
|
||||
val sut = DefaultDeeplinkParser()
|
||||
assertThat(sut.getFromIntent(createIntent(A_URI)))
|
||||
.isEqualTo(DeeplinkData.Root(A_SESSION_ID))
|
||||
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM)))
|
||||
|
|
@ -43,7 +43,7 @@ class DeeplinkParserTest {
|
|||
|
||||
@Test
|
||||
fun `error cases`() {
|
||||
val sut = DeeplinkParser()
|
||||
val sut = DefaultDeeplinkParser()
|
||||
// Bad scheme
|
||||
assertThat(sut.getFromIntent(createIntent("x://open/@alice:server.org"))).isNull()
|
||||
// Bad host
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -42,10 +44,6 @@ android {
|
|||
ksp(libs.showkase.processor)
|
||||
implementation(libs.showkase)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(libs.molecule.runtime)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testCommonDependencies(libs)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,18 @@ import io.element.android.compound.theme.ElementTheme
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
fun RoomPreviewDescriptionAtom(description: String, modifier: Modifier = Modifier) {
|
||||
fun RoomPreviewDescriptionAtom(
|
||||
description: String,
|
||||
modifier: Modifier = Modifier,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = description,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
textAlign = TextAlign.Center,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
maxLines = 3,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
maxLines = maxLines,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ fun RoomPreviewSubtitleAtom(subtitle: String, modifier: Modifier = Modifier) {
|
|||
Text(
|
||||
modifier = modifier,
|
||||
text = subtitle,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
textAlign = TextAlign.Center,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ fun RoomPreviewTitleAtom(
|
|||
Text(
|
||||
modifier = modifier,
|
||||
text = title,
|
||||
style = ElementTheme.typography.fontHeadingMdBold,
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
textAlign = TextAlign.Center,
|
||||
fontStyle = fontStyle,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
|
|
@ -28,9 +30,13 @@ fun UnreadIndicatorAtom(
|
|||
size: Dp = 12.dp,
|
||||
color: Color = ElementTheme.colors.unreadIndicator,
|
||||
isVisible: Boolean = true,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
contentDescription?.let { this.contentDescription = it }
|
||||
}
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.background(if (isVisible) color else Color.Transparent)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.molecules
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun InviteButtonsRowMolecule(
|
||||
onAcceptClick: () -> Unit,
|
||||
onDeclineClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
declineText: String = stringResource(CommonStrings.action_decline),
|
||||
acceptText: String = stringResource(CommonStrings.action_accept),
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
text = declineText,
|
||||
onClick = onDeclineClick,
|
||||
size = ButtonSize.MediumLowPadding,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Button(
|
||||
text = acceptText,
|
||||
onClick = onAcceptClick,
|
||||
size = ButtonSize.MediumLowPadding,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,8 +25,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
fun RoomPreviewMembersCountMolecule(
|
||||
memberCount: Long,
|
||||
fun MembersCountMolecule(
|
||||
memberCount: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
|
|
@ -51,13 +51,13 @@ fun RoomPreviewMembersCountMolecule(
|
|||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun RoomPreviewMembersCountMoleculePreview() = ElementPreview {
|
||||
internal fun MembersCountMoleculePreview() = ElementPreview {
|
||||
Column(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
RoomPreviewMembersCountMolecule(memberCount = 1)
|
||||
RoomPreviewMembersCountMolecule(memberCount = 888)
|
||||
RoomPreviewMembersCountMolecule(memberCount = 123_456)
|
||||
MembersCountMolecule(memberCount = 1)
|
||||
MembersCountMolecule(memberCount = 888)
|
||||
MembersCountMolecule(memberCount = 123_456)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,14 +34,13 @@ fun RoomPreviewOrganism(
|
|||
title()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
subtitle()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (memberCount != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
memberCount()
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (description != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
description()
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
*
|
||||
* Ref: https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0
|
||||
* @param modifier Classical modifier.
|
||||
* @param renderBackground whether to render the background image or not.
|
||||
* @param contentAlignment horizontal alignment of the contents.
|
||||
* @param footer optional footer.
|
||||
* @param content main content.
|
||||
|
|
@ -38,6 +39,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
@Composable
|
||||
fun OnBoardingPage(
|
||||
modifier: Modifier = Modifier,
|
||||
renderBackground: Boolean = true,
|
||||
contentAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
||||
footer: @Composable () -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
|
|
@ -47,13 +49,15 @@ fun OnBoardingPage(
|
|||
.fillMaxSize()
|
||||
) {
|
||||
// BG
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
painter = painterResource(id = R.drawable.onboarding_bg),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null,
|
||||
)
|
||||
if (renderBackground) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
painter = painterResource(id = R.drawable.onboarding_bg),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ import kotlin.math.roundToInt
|
|||
|
||||
@Composable
|
||||
fun ExpandableBottomSheetLayout(
|
||||
sheetDragHandle: @Composable BoxScope.() -> Unit,
|
||||
sheetDragHandle: @Composable BoxScope.(toggleAction: () -> Unit) -> Unit,
|
||||
bottomSheetContent: @Composable ColumnScope.() -> Unit,
|
||||
state: ExpandableBottomSheetLayoutState,
|
||||
maxBottomSheetContentHeight: Dp,
|
||||
|
|
@ -152,7 +152,19 @@ fun ExpandableBottomSheetLayout(
|
|||
}
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
sheetDragHandle()
|
||||
sheetDragHandle {
|
||||
coroutineScope.launch {
|
||||
val destination = if (state.position == ExpandableBottomSheetLayoutState.Position.EXPANDED) {
|
||||
state.internalPosition = ExpandableBottomSheetLayoutState.Position.COLLAPSED
|
||||
minBottomContentHeightPx.toFloat()
|
||||
} else {
|
||||
state.internalPosition = ExpandableBottomSheetLayoutState.Position.EXPANDED
|
||||
calculatedMaxBottomContentHeightPx.toFloat()
|
||||
}
|
||||
animatable.snapTo(currentBottomContentHeightPx.toFloat())
|
||||
animatable.animateTo(destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
bottomSheetContent()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class ExpandableBottomSheetLayoutState {
|
|||
/**
|
||||
* The current position of the bottom sheet layout.
|
||||
*/
|
||||
val position = internalPosition
|
||||
val position get() = internalPosition
|
||||
|
||||
/**
|
||||
* The percentage of the bottom sheet layout that is currently being dragged.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,18 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
|
|||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* A progress dialog, with a spinner, and optional text content.
|
||||
*
|
||||
* @param modifier
|
||||
* @param text Optional text to show under the spinner.
|
||||
* @param type
|
||||
* @param properties
|
||||
* @param showCancelButton
|
||||
* @param onDismissRequest
|
||||
* @param content Optional additional content to show under the spinner, and above the cancel button (if shown). If both `text` and `content` are supplied,
|
||||
* `text` is shown above `content`.
|
||||
*/
|
||||
@Composable
|
||||
fun ProgressDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -46,6 +58,7 @@ fun ProgressDialog(
|
|||
properties: DialogProperties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||
showCancelButton: Boolean = false,
|
||||
onDismissRequest: () -> Unit = {},
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
|
|
@ -75,7 +88,8 @@ fun ProgressDialog(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +110,8 @@ private fun ProgressDialogContent(
|
|||
CircularProgressIndicator(
|
||||
color = ElementTheme.colors.iconPrimary
|
||||
)
|
||||
}
|
||||
},
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
@ -118,6 +133,7 @@ private fun ProgressDialogContent(
|
|||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
content()
|
||||
if (showCancelButton) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Box(
|
||||
|
|
@ -138,7 +154,7 @@ private fun ProgressDialogContent(
|
|||
@Composable
|
||||
internal fun ProgressDialogContentPreview() = ElementThemedPreview {
|
||||
DialogPreview {
|
||||
ProgressDialogContent(text = "test dialog content", showCancelButton = true)
|
||||
ProgressDialogContent(text = "test dialog content", showCancelButton = true, content = {})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,3 +163,34 @@ internal fun ProgressDialogContentPreview() = ElementThemedPreview {
|
|||
internal fun ProgressDialogPreview() = ElementPreview {
|
||||
ProgressDialog(text = "test dialog content", showCancelButton = true)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ProgressDialogWithContentPreview() = ElementPreview {
|
||||
ProgressDialog(showCancelButton = true) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Heading",
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Subtext",
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ProgressDialogWithTextAndContentPreview() = ElementPreview {
|
||||
ProgressDialog(text = "Text Content") {
|
||||
Text(
|
||||
text = "blah blah",
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ open class AvatarDataProvider : PreviewParameterProvider<AvatarData> {
|
|||
.map {
|
||||
sequenceOf(
|
||||
anAvatarData(size = it),
|
||||
anAvatarData(size = it).copy(name = null),
|
||||
anAvatarData(size = it).copy(url = "aUrl"),
|
||||
anAvatarData(size = it, name = null),
|
||||
anAvatarData(size = it, url = "aUrl"),
|
||||
)
|
||||
}
|
||||
.flatten()
|
||||
|
|
@ -26,10 +26,12 @@ open class AvatarDataProvider : PreviewParameterProvider<AvatarData> {
|
|||
fun anAvatarData(
|
||||
// Let's the id not start with a 'a'.
|
||||
id: String = "@id_of_alice:server.org",
|
||||
name: String = "Alice",
|
||||
name: String? = "Alice",
|
||||
url: String? = null,
|
||||
size: AvatarSize = AvatarSize.RoomListItem,
|
||||
) = AvatarData(
|
||||
id = id,
|
||||
name = name,
|
||||
url = url,
|
||||
size = size,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.avatar
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.avatar.internal.OverlapRatioProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.toPx
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Draw a row of avatars (they must all have the same size), from start to end.
|
||||
* @param avatarDataList the avatars to render. Note: they will all be rendered, the caller may
|
||||
* want to limit the list size
|
||||
* @param avatarType the type of avatars to render
|
||||
* @param modifier Jetpack Compose modifier
|
||||
* @param overlapRatio the overlap ration. When 0f, avatars will render without overlap, when 1f
|
||||
* only the first avatar will be visible
|
||||
* @param lastOnTop if true, the last visible avatar will be rendered on top.
|
||||
*/
|
||||
@Composable
|
||||
fun AvatarRow(
|
||||
avatarDataList: ImmutableList<AvatarData>,
|
||||
avatarType: AvatarType,
|
||||
modifier: Modifier = Modifier,
|
||||
overlapRatio: Float = 0.5f,
|
||||
lastOnTop: Boolean = false,
|
||||
) {
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
val lastItemIndex = avatarDataList.size - 1
|
||||
val avatarSize = avatarDataList.firstOrNull()?.size?.dp ?: return
|
||||
val avatarSizePx = avatarSize.toPx()
|
||||
avatarDataList
|
||||
.let {
|
||||
if (lastOnTop) {
|
||||
it
|
||||
} else {
|
||||
it.reversed()
|
||||
}
|
||||
}
|
||||
.forEachIndexed { index, avatarData ->
|
||||
val startPadding = if (lastOnTop) {
|
||||
avatarSize * (1 - overlapRatio) * index
|
||||
} else {
|
||||
avatarSize * (1 - overlapRatio) * (lastItemIndex - index)
|
||||
}
|
||||
Avatar(
|
||||
modifier = Modifier
|
||||
.padding(start = startPadding)
|
||||
.graphicsLayer {
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
.drawWithContent {
|
||||
// Draw content and clear the pixels for the avatar on the left (right in RTL) or when lastOnTop is true on
|
||||
// the right (left in RTL).
|
||||
drawContent()
|
||||
if (index < lastItemIndex) {
|
||||
val xOffset = if (isRtl == lastOnTop) {
|
||||
avatarSizePx * (overlapRatio - 0.5f)
|
||||
} else {
|
||||
size.width - avatarSizePx * (overlapRatio - 0.5f)
|
||||
}
|
||||
drawCircle(
|
||||
color = Color.Black,
|
||||
center = Offset(
|
||||
x = xOffset,
|
||||
y = size.height / 2,
|
||||
),
|
||||
radius = avatarSizePx / 2,
|
||||
blendMode = BlendMode.Clear,
|
||||
)
|
||||
}
|
||||
}
|
||||
.size(size = avatarSize)
|
||||
// Keep internal padding, it has the advantage to not reduce the size of the Avatar image,
|
||||
// which is already small in our use case.
|
||||
.padding(2.dp),
|
||||
avatarData = avatarData,
|
||||
avatarType = avatarType,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewsDayNight
|
||||
internal fun AvatarRowPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
|
||||
ElementPreview {
|
||||
ContentToPreview(overlapRatio)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewsDayNight
|
||||
internal fun AvatarRowLastOnTopPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
|
||||
ElementPreview {
|
||||
ContentToPreview(
|
||||
overlapRatio = overlapRatio,
|
||||
lastOnTop = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewsDayNight
|
||||
internal fun AvatarRowRtlPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides LayoutDirection.Rtl,
|
||||
) {
|
||||
ElementPreview {
|
||||
ContentToPreview(overlapRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewsDayNight
|
||||
internal fun AvatarRowLastOnTopRtlPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides LayoutDirection.Rtl,
|
||||
) {
|
||||
ElementPreview {
|
||||
ContentToPreview(
|
||||
overlapRatio = overlapRatio,
|
||||
lastOnTop = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(
|
||||
overlapRatio: Float,
|
||||
lastOnTop: Boolean = false,
|
||||
) {
|
||||
AvatarRow(
|
||||
avatarDataList = listOf("A", "B", "C").map {
|
||||
AvatarData(
|
||||
id = it,
|
||||
name = it,
|
||||
size = AvatarSize.RoomListItem,
|
||||
)
|
||||
}.toImmutableList(),
|
||||
avatarType = AvatarType.User,
|
||||
overlapRatio = overlapRatio,
|
||||
lastOnTop = lastOnTop,
|
||||
)
|
||||
}
|
||||
|
|
@ -14,9 +14,11 @@ enum class AvatarSize(val dp: Dp) {
|
|||
CurrentUserTopBar(32.dp),
|
||||
|
||||
IncomingCall(140.dp),
|
||||
RoomHeader(96.dp),
|
||||
RoomDetailsHeader(96.dp),
|
||||
RoomListItem(52.dp),
|
||||
|
||||
SpaceListItem(52.dp),
|
||||
|
||||
RoomSelectRoomListItem(36.dp),
|
||||
|
||||
UserPreference(56.dp),
|
||||
|
|
@ -32,6 +34,7 @@ enum class AvatarSize(val dp: Dp) {
|
|||
TimelineRoom(32.dp),
|
||||
TimelineSender(32.dp),
|
||||
TimelineReadReceipt(16.dp),
|
||||
TimelineThreadLatestEventSender(24.dp),
|
||||
|
||||
ComposerAlert(32.dp),
|
||||
|
||||
|
|
@ -63,4 +66,13 @@ enum class AvatarSize(val dp: Dp) {
|
|||
DmCreationConfirmation(64.dp),
|
||||
|
||||
UserVerification(52.dp),
|
||||
|
||||
OrganizationHeader(64.dp),
|
||||
SpaceHeader(64.dp),
|
||||
RoomPreviewHeader(64.dp),
|
||||
RoomPreviewInviter(56.dp),
|
||||
SpaceMember(24.dp),
|
||||
LeaveSpaceRoom(32.dp),
|
||||
|
||||
AccountItem(32.dp),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.avatar.internal
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
class OverlapRatioProvider : PreviewParameterProvider<Float> {
|
||||
override val values: Sequence<Float> = sequenceOf(
|
||||
0f,
|
||||
0.25f,
|
||||
0.5f,
|
||||
0.75f,
|
||||
1f
|
||||
)
|
||||
}
|
||||
|
|
@ -142,7 +142,7 @@ internal fun SimpleAlertDialogContent(
|
|||
Text(
|
||||
text = titleText,
|
||||
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
textAlign = if (icon != null) TextAlign.Center else TextAlign.Start,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -510,3 +510,43 @@ internal fun DialogWithThirdButtonPreview() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Dialogs, name = "Dialog with a very long title")
|
||||
@Composable
|
||||
@Suppress("MaxLineLength")
|
||||
internal fun DialogWithVeryLongTitlePreview() {
|
||||
ElementThemedPreview(showBackground = false) {
|
||||
DialogPreview {
|
||||
SimpleAlertDialogContent(
|
||||
title = "Dialog Title that takes more than one line",
|
||||
content = "A dialog is a type of modal window that appears in front of app content to provide critical information," +
|
||||
" or prompt for a decision to be made. Learn more",
|
||||
submitText = "OK",
|
||||
onSubmitClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Dialogs, name = "Dialog with a very long title and icon")
|
||||
@Composable
|
||||
@Suppress("MaxLineLength")
|
||||
internal fun DialogWithVeryLongTitleAndIconPreview() {
|
||||
ElementThemedPreview(showBackground = false) {
|
||||
DialogPreview {
|
||||
SimpleAlertDialogContent(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.NotificationsSolid(),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
title = "Dialog Title that takes more than one line",
|
||||
content = "A dialog is a type of modal window that appears in front of app content to provide critical information," +
|
||||
" or prompt for a decision to be made. Learn more",
|
||||
submitText = "OK",
|
||||
onSubmitClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ import androidx.compose.material3.TextFieldColors
|
|||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
|
|
@ -67,16 +70,19 @@ fun <T> SearchBar(
|
|||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
if (!active) {
|
||||
onQueryChange("")
|
||||
focusManager.clearFocus()
|
||||
val updatedOnQueryChange by rememberUpdatedState(onQueryChange)
|
||||
LaunchedEffect(active) {
|
||||
if (!active) {
|
||||
updatedOnQueryChange("")
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
SearchBar(
|
||||
inputField = {
|
||||
SearchBarDefaults.InputField(
|
||||
query = query,
|
||||
onQueryChange = onQueryChange,
|
||||
onQueryChange = updatedOnQueryChange,
|
||||
onSearch = { focusManager.clearFocus() },
|
||||
expanded = active,
|
||||
onExpandedChange = onActiveChange,
|
||||
|
|
|
|||
|
|
@ -11,5 +11,5 @@ plugins {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(libs.inject)
|
||||
api(libs.metro.runtime)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.di
|
||||
|
||||
import dev.zacsweers.metro.Qualifier
|
||||
|
||||
/**
|
||||
* Qualifies a [File] object which represents the application base directory.
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@Qualifier
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
AnnotationTarget.TYPE,
|
||||
)
|
||||
public annotation class BaseDirectory
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
import dev.zacsweers.metro.Qualifier
|
||||
|
||||
/**
|
||||
* Qualifies a [File] object which represents the application cache directory.
|
||||
|
|
@ -15,4 +15,13 @@ import javax.inject.Qualifier
|
|||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@Qualifier
|
||||
annotation class CacheDirectory
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
AnnotationTarget.TYPE,
|
||||
)
|
||||
public annotation class CacheDirectory
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
package io.element.android.libraries.di
|
||||
|
||||
/**
|
||||
* A [DaggerComponentOwner] is anything that "owns" a Dagger Component.
|
||||
* A [DependencyInjectionGraphOwner] is anything that "owns" a DI Graph.
|
||||
*
|
||||
*/
|
||||
interface DaggerComponentOwner {
|
||||
/** This is either a component, or a list of components. */
|
||||
val daggerComponent: Any
|
||||
interface DependencyInjectionGraphOwner {
|
||||
/** This is either a graph, or a list of graphs. */
|
||||
val graph: Any
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright 2022-2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.di
|
||||
|
||||
import javax.inject.Scope
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class SingleIn(val clazz: KClass<*>)
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.di.annotations
|
||||
|
||||
import javax.inject.Qualifier
|
||||
import dev.zacsweers.metro.Qualifier
|
||||
|
||||
/**
|
||||
* Qualifies a [CoroutineScope] object which represents the base coroutine scope to use for the application.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
/*
|
||||
* Copyright 2022-2024 New Vector 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.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.di
|
||||
package io.element.android.libraries.di.annotations
|
||||
|
||||
import javax.inject.Qualifier
|
||||
import dev.zacsweers.metro.Qualifier
|
||||
|
||||
/**
|
||||
* Qualifies a [Context] object that represents the application context.
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.di.annotations
|
||||
|
||||
import javax.inject.Qualifier
|
||||
import dev.zacsweers.metro.Qualifier
|
||||
|
||||
/**
|
||||
* Qualifies a [CoroutineScope] object which represents the base coroutine scope to use for an active session.
|
||||
|
|
|
|||
|
|
@ -7,8 +7,19 @@
|
|||
|
||||
package io.element.android.libraries.eventformatter.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
|
||||
interface TimelineEventFormatter {
|
||||
fun format(event: EventTimelineItem): CharSequence?
|
||||
fun format(event: EventTimelineItem): CharSequence? {
|
||||
return format(
|
||||
content = event.content,
|
||||
isOutgoing = event.isOwn,
|
||||
sender = event.sender,
|
||||
senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
|
||||
)
|
||||
}
|
||||
fun format(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): CharSequence?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
|
|
@ -21,7 +22,7 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.core)
|
||||
|
|
@ -32,9 +33,7 @@ dependencies {
|
|||
implementation(projects.services.toolbox.api)
|
||||
api(projects.libraries.eventformatter.api)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.services.toolbox.impl)
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ package io.element.android.libraries.eventformatter.impl
|
|||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
|
|
@ -35,10 +36,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getDisambigua
|
|||
import io.element.android.libraries.matrix.ui.messages.toPlainText
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultPinnedMessagesBannerFormatter @Inject constructor(
|
||||
@Inject
|
||||
class DefaultPinnedMessagesBannerFormatter(
|
||||
private val sp: StringProvider,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
) : PinnedMessagesBannerFormatter {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
package io.element.android.libraries.eventformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
||||
|
|
@ -42,10 +43,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getDisambigua
|
|||
import io.element.android.libraries.matrix.ui.messages.toPlainText
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultRoomLastMessageFormatter @Inject constructor(
|
||||
@Inject
|
||||
class DefaultRoomLastMessageFormatter(
|
||||
private val sp: StringProvider,
|
||||
private val roomMembershipContentFormatter: RoomMembershipContentFormatter,
|
||||
private val profileChangeContentFormatter: ProfileChangeContentFormatter,
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@
|
|||
|
||||
package io.element.android.libraries.eventformatter.impl
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
|
|
@ -29,10 +32,10 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnknownConten
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultTimelineEventFormatter @Inject constructor(
|
||||
@Inject
|
||||
class DefaultTimelineEventFormatter(
|
||||
private val sp: StringProvider,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val roomMembershipContentFormatter: RoomMembershipContentFormatter,
|
||||
|
|
@ -42,12 +45,16 @@ class DefaultTimelineEventFormatter @Inject constructor(
|
|||
override fun format(event: EventTimelineItem): CharSequence? {
|
||||
val isOutgoing = event.isOwn
|
||||
val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender)
|
||||
return when (val content = event.content) {
|
||||
return format(event.content, isOutgoing, event.sender, senderDisambiguatedDisplayName)
|
||||
}
|
||||
|
||||
override fun format(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): CharSequence? {
|
||||
return when (content) {
|
||||
is RoomMembershipContent -> {
|
||||
roomMembershipContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is ProfileChangeContent -> {
|
||||
profileChangeContentFormatter.format(content, event.sender, senderDisambiguatedDisplayName, isOutgoing)
|
||||
profileChangeContentFormatter.format(content, sender, senderDisambiguatedDisplayName, isOutgoing)
|
||||
}
|
||||
is StateContent -> {
|
||||
stateContentFormatter.format(content, senderDisambiguatedDisplayName, isOutgoing, RenderingMode.Timeline)
|
||||
|
|
@ -65,7 +72,7 @@ class DefaultTimelineEventFormatter @Inject constructor(
|
|||
is FailedToParseStateContent,
|
||||
is UnknownContent -> {
|
||||
if (buildMeta.isDebuggable) {
|
||||
error("You should not use this formatter for this event: $event")
|
||||
error("You should not use this formatter for this event content: $content")
|
||||
}
|
||||
sp.getString(CommonStrings.common_unsupported_event)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@
|
|||
|
||||
package io.element.android.libraries.eventformatter.impl
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class ProfileChangeContentFormatter @Inject constructor(
|
||||
@Inject
|
||||
class ProfileChangeContentFormatter(
|
||||
private val sp: StringProvider,
|
||||
) {
|
||||
fun format(
|
||||
|
|
|
|||
|
|
@ -7,14 +7,15 @@
|
|||
|
||||
package io.element.android.libraries.eventformatter.impl
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomMembershipContentFormatter @Inject constructor(
|
||||
@Inject
|
||||
class RoomMembershipContentFormatter(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val sp: StringProvider,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -7,15 +7,16 @@
|
|||
|
||||
package io.element.android.libraries.eventformatter.impl
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class StateContentFormatter @Inject constructor(
|
||||
@Inject
|
||||
class StateContentFormatter(
|
||||
private val sp: StringProvider,
|
||||
) {
|
||||
fun format(
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@
|
|||
<string name="state_event_room_knock_accepted">"%1$s получи достъп до %2$s"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"Вие позволихте на %1$s да се присъедини"</string>
|
||||
<string name="state_event_room_knock_by_you">"Вие поискахте да се присъедините"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s отхвърли заявката на %2$s за присъединяване"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Вие отхвърлихте заявката на %1$s за присъединяване"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s отхвърли вашата заявка за присъединяване"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s вече не се интересува от присъединяване"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Вие отменихте заявката си за присъединяване"</string>
|
||||
<string name="state_event_room_leave">"%1$s напусна стаята"</string>
|
||||
<string name="state_event_room_leave_by_you">"Вие напуснахте стаята"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s промени името на стаята на: %2$s"</string>
|
||||
|
|
@ -39,14 +43,19 @@
|
|||
<string name="state_event_room_pinned_events_changed_by_you">"Вие променихте закачените съобщения"</string>
|
||||
<string name="state_event_room_pinned_events_pinned">"%1$s закачи съобщение"</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"Вие закачихте съобщение"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned">"%1$s откачи съобщение"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned_by_you">"Вие откачихте съобщение"</string>
|
||||
<string name="state_event_room_reject">"%1$s отхвърли поканата"</string>
|
||||
<string name="state_event_room_reject_by_you">"Вие отхвърлихте поканата"</string>
|
||||
<string name="state_event_room_remove">"%1$s премахна %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"Вие премахнахте %1$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s изпрати покана на %2$s за присъединяване към стаята"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Вие изпратихте покана на %1$s за присъединяване към стаята"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s отмени поканата на %2$s за присъединяване към стаята"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Вие отменихте поканата на %1$s за присъединяване към стаята"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s промени темата на: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Вие променихте темата на: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s премахна темата на стаята"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Вие премахнахте темата на стаята"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s направи неизвестна промяна в членството си"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -2,72 +2,72 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="state_event_avatar_changed_too">"(Avatar wurde auch geändert)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s hat den Avatar geändert"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Sie änderten ihren Avatar"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"Du hast deinen Avatar geändert"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s wurde zum Mitglied herabgestuft"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s wurde zum Moderator herabgestuft"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s hat den Anzeigenamen von %2$s auf %3$s geändert"</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"Du hast deinen Anzeigenamen von %1$s auf %2$s geändert"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s hat den Anzeigenamen entfernt (war %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Sie haben Ihren Anzeigenamen entfernt (er lautete %1$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"Du hast deinen Anzeigenamen entfernt (war %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s hat den Anzeigenamen auf %2$s geändert"</string>
|
||||
<string name="state_event_display_name_set_by_you">"Du hast deinen Anzeigenamen zu %1$s geändert"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s ist jetzt Administrator*in"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s ist jetzt Admin"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s ist jetzt Moderator*in"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s hat den Raum-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Dir haben den Zimmer-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s hat den Raum-Avatar entfernt"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Sie haben den Raum-Avatar entfernt"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s hat den Chat -Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"Du hast den Chat-Avatar geändert"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s hat den Chat-Avatar entfernt"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"Du hast den Chat-Avatar entfernt"</string>
|
||||
<string name="state_event_room_ban">"%1$s hat %2$s gesperrt"</string>
|
||||
<string name="state_event_room_ban_by_you">"Sie haben %1$s gesperrt"</string>
|
||||
<string name="state_event_room_ban_by_you_with_reason">"Sie haben %1$s gesperrt: %2$s"</string>
|
||||
<string name="state_event_room_ban_by_you">"Du hast %1$s gesperrt"</string>
|
||||
<string name="state_event_room_ban_by_you_with_reason">"Du hast %1$s gesperrt: %2$s"</string>
|
||||
<string name="state_event_room_ban_with_reason">"%1$s sperrte %2$s: %3$s"</string>
|
||||
<string name="state_event_room_created">"%1$s hat den Raum erstellt"</string>
|
||||
<string name="state_event_room_created_by_you">"Sie erstellten den Chatroom"</string>
|
||||
<string name="state_event_room_created">"%1$s hat den Chat erstellt"</string>
|
||||
<string name="state_event_room_created_by_you">"Du hast den Chat erstellt"</string>
|
||||
<string name="state_event_room_invite">"%1$s hat %2$s eingeladen"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s hat die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Sie haben die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_by_you">"Sie luden %1$s ein"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"Du hast die Einladung angenommen"</string>
|
||||
<string name="state_event_room_invite_by_you">"Du hast %1$s eingeladen"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s hat dich eingeladen"</string>
|
||||
<string name="state_event_room_join">"%1$s hat den Raum betreten"</string>
|
||||
<string name="state_event_room_join_by_you">"Sie sind dem Raum beigetreten"</string>
|
||||
<string name="state_event_room_knock">"%1$s beantragt den Beitritt"</string>
|
||||
<string name="state_event_room_join">"%1$s ist dem Chat beigetreten"</string>
|
||||
<string name="state_event_room_join_by_you">"Du bist dem Chat beigetreten"</string>
|
||||
<string name="state_event_room_knock">"%1$s fragt den Beitritt an"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s hat %2$s den Beitritt erlaubt"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"Sie genehmigten die Beitrittsanfrage für %1$s"</string>
|
||||
<string name="state_event_room_knock_by_you">"Sie haben um Beitritt gebeten"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"Du hast %1$s den Beitritt erlaubt"</string>
|
||||
<string name="state_event_room_knock_by_you">"Du hast angefragt beizutreten"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s hat die Beitrittsanfrage von %2$s abgelehnt"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Sie haben die Beitrittsanfrage für %1$s abgelehnt"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"Du hast die Beitrittsanfrage von %1$s abgelehnt"</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s hat deine Beitrittsanfrage abgelehnt"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s ist nicht mehr an einem Beitritt interessiert"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Sie haben ihre Beitrittsanfrage widerrufen"</string>
|
||||
<string name="state_event_room_leave">"%1$s hat den Raum verlassen"</string>
|
||||
<string name="state_event_room_leave_by_you">"Sie verließen den Raum"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s hat den Raum-Namen geändert in: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Sie haben den Raumnamen geändert in: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s hat den Raum-Namen entfernt"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Sie haben den Raumnamen entfernt"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"Du hast deine Beitrittsanfrage abgebrochen"</string>
|
||||
<string name="state_event_room_leave">"%1$s hat den Chat verlassen"</string>
|
||||
<string name="state_event_room_leave_by_you">"Du hast den Chat verlassen"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s hat den Chat-Namen geändert in: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"Du hast den Chat-Namen geändert in: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s hat den Chat-Namen entfernt"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"Du hast den Chat-Namen entfernt"</string>
|
||||
<string name="state_event_room_none">"%1$s hat keine Änderungen vorgenommen"</string>
|
||||
<string name="state_event_room_none_by_you">"Sie haben keine Änderungen vorgenommen"</string>
|
||||
<string name="state_event_room_none_by_you">"Du hast keine Änderungen vorgenommen"</string>
|
||||
<string name="state_event_room_pinned_events_changed">"%1$s hat die fixierten Nachrichten geändert"</string>
|
||||
<string name="state_event_room_pinned_events_changed_by_you">"Sie haben die angehefteten Nachrichten geändert."</string>
|
||||
<string name="state_event_room_pinned_events_changed_by_you">"Du hast die fixierten Nachrichten geändert"</string>
|
||||
<string name="state_event_room_pinned_events_pinned">"%1$s fixierte eine Nachricht"</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"Sie haben eine Nachricht angeheftet."</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"Du hast eine Nachricht fixiert"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned">"%1$s löste eine Nachricht"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned_by_you">"Sie haben eine angeheftete Nachricht entfernt."</string>
|
||||
<string name="state_event_room_pinned_events_unpinned_by_you">"Du hast eine Nachricht gelöst"</string>
|
||||
<string name="state_event_room_reject">"%1$s lehnte die Einladung ab"</string>
|
||||
<string name="state_event_room_reject_by_you">"Sie lehnten die Einladung ab"</string>
|
||||
<string name="state_event_room_reject_by_you">"Du hast die Einladung abgelehnt"</string>
|
||||
<string name="state_event_room_remove">"%1$s hat %2$s entfernt"</string>
|
||||
<string name="state_event_room_remove_by_you">"Sie haben %1$s entfernt"</string>
|
||||
<string name="state_event_room_remove_by_you_with_reason">"Sie haben %1$s entfernt: %2$s"</string>
|
||||
<string name="state_event_room_remove_by_you">"Du hast %1$s entfernt"</string>
|
||||
<string name="state_event_room_remove_by_you_with_reason">"Du hast %1$s entfernt: %2$s"</string>
|
||||
<string name="state_event_room_remove_with_reason">"%1$s entfernt %2$s: %3$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s hat %2$s eingeladen, den Raum zu beizutreten"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Sie haben eine Einladung an %1$s gesendet, um dem Chatroom beizutreten."</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s hat die Einladung an %2$s zum Betreten des Raums zurückgezogen"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Sie haben die Einladung für %1$s widerrufen"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s hat %2$s eingeladen, den Chat zu beizutreten"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"Du hast eine Einladung an %1$s gesendet, dem Chat beizutreten"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s hat die Einladung an %2$s zum Beitritt des Chat zurückgezogen"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"Du hast die Einladung an %1$s zum Beitritt des Chat zurückgezogen"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s hat das Thema geändert in: %2$s"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Sie haben das Thema geändert in:%1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s hat das Raum-Thema entfernt"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Sie haben das Raumthema entfernt"</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"Du hast das Thema geändert in: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s hat das Chat-Thema entfernt"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"Du hast das Chat-Thema entfernt"</string>
|
||||
<string name="state_event_room_unban">"%1$s hat die Sperre für %2$s aufgehoben"</string>
|
||||
<string name="state_event_room_unban_by_you">"Sie haben die Sperre für %1$s aufgehoben"</string>
|
||||
<string name="state_event_room_unban_by_you">"Du hast die Sperre für %1$s aufgehoben"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s hat eine unbekannte Änderung vorgenommen"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="state_event_avatar_changed_too">"(프로필 사진도 변경됨)"</string>
|
||||
<string name="state_event_avatar_url_changed">"%1$s님이 프로필 사진을 변경함"</string>
|
||||
<string name="state_event_avatar_url_changed_by_you">"프로필 사진을 변경함"</string>
|
||||
<string name="state_event_demoted_to_member">"%1$s 회원으로 강등되었습니다"</string>
|
||||
<string name="state_event_demoted_to_moderator">"%1$s 중재자로 강등되었습니다"</string>
|
||||
<string name="state_event_display_name_changed_from">"%1$s님이 표시 이름을 %2$s에서 %3$s(으)로 변경했습니다."</string>
|
||||
<string name="state_event_display_name_changed_from_by_you">"표시 이름을 %1$s에서 %2$s(으)로 변경했습니다"</string>
|
||||
<string name="state_event_display_name_removed">"%1$s님이 표시 이름을 제거했습니다 (이전 이름 %2$s)"</string>
|
||||
<string name="state_event_display_name_removed_by_you">"표시 이름을 제거했습니다 (이전 이름 %1$s)"</string>
|
||||
<string name="state_event_display_name_set">"%1$s님이 표시되는 이름을 %2$s(으)로 변경함"</string>
|
||||
<string name="state_event_display_name_set_by_you">"%1$s(으)로 표시되는 이름을 변경함"</string>
|
||||
<string name="state_event_promoted_to_administrator">"%1$s 는 관리자로 승진되었습니다"</string>
|
||||
<string name="state_event_promoted_to_moderator">"%1$s 는 중재자로 승진되었습니다"</string>
|
||||
<string name="state_event_room_avatar_changed">"%1$s님이 방 아바타를 변경함"</string>
|
||||
<string name="state_event_room_avatar_changed_by_you">"방 아바타를 변경함"</string>
|
||||
<string name="state_event_room_avatar_removed">"%1$s님이 방 아바타를 삭제함"</string>
|
||||
<string name="state_event_room_avatar_removed_by_you">"방 아바타를 삭제함"</string>
|
||||
<string name="state_event_room_ban">"%1$s님이 %2$s님을 차단함"</string>
|
||||
<string name="state_event_room_ban_by_you">"%1$s님을 차단함"</string>
|
||||
<string name="state_event_room_ban_by_you_with_reason">"당신은 차단했습니다 %1$s: %2$s"</string>
|
||||
<string name="state_event_room_ban_with_reason">"%1$s 차단됨 %2$s: %3$s"</string>
|
||||
<string name="state_event_room_created">"%1$s님이 방을 생성함"</string>
|
||||
<string name="state_event_room_created_by_you">"방을 생성함"</string>
|
||||
<string name="state_event_room_invite">"%1$s님이 %2$s님을 초대함"</string>
|
||||
<string name="state_event_room_invite_accepted">"%1$s님이 초대를 수락함"</string>
|
||||
<string name="state_event_room_invite_accepted_by_you">"초대를 수락함"</string>
|
||||
<string name="state_event_room_invite_by_you">"%1$s님을 초대함"</string>
|
||||
<string name="state_event_room_invite_you">"%1$s님으로부터 초대받음"</string>
|
||||
<string name="state_event_room_join">"%1$s님이 방에 참석함"</string>
|
||||
<string name="state_event_room_join_by_you">"방에 참석함"</string>
|
||||
<string name="state_event_room_knock">"%1$s님이 참가를 요청함"</string>
|
||||
<string name="state_event_room_knock_accepted">"%1$s님이 %2$s님의 참가를 승인함"</string>
|
||||
<string name="state_event_room_knock_accepted_by_you">"%1$s님이 참가를 승인함"</string>
|
||||
<string name="state_event_room_knock_by_you">"참가를 요청함"</string>
|
||||
<string name="state_event_room_knock_denied">"%1$s이 %2$s의 참가 요청을 거절함"</string>
|
||||
<string name="state_event_room_knock_denied_by_you">"%1$s님의 가입 요청을 거부했습니다."</string>
|
||||
<string name="state_event_room_knock_denied_you">"%1$s님의 가입 요청을 거부했습니다"</string>
|
||||
<string name="state_event_room_knock_retracted">"%1$s이 참가 요청에 관심이 없음"</string>
|
||||
<string name="state_event_room_knock_retracted_by_you">"참가 요청을 거부함"</string>
|
||||
<string name="state_event_room_leave">"%1$s님이 방을 떠남"</string>
|
||||
<string name="state_event_room_leave_by_you">"방을 떠남"</string>
|
||||
<string name="state_event_room_name_changed">"%1$s님이 방 이름을 변경함: %2$s"</string>
|
||||
<string name="state_event_room_name_changed_by_you">"방 이름을 변경함: %1$s"</string>
|
||||
<string name="state_event_room_name_removed">"%1$s님이 방 이름을 삭제함"</string>
|
||||
<string name="state_event_room_name_removed_by_you">"방 이름을 삭제함"</string>
|
||||
<string name="state_event_room_none">"%1$s 변경 사항 없음"</string>
|
||||
<string name="state_event_room_none_by_you">"변경 사항이 없습니다."</string>
|
||||
<string name="state_event_room_pinned_events_changed">"%1$s 고정된 메시지가 변경되었습니다."</string>
|
||||
<string name="state_event_room_pinned_events_changed_by_you">"고정된 메시지가 변경되었습니다"</string>
|
||||
<string name="state_event_room_pinned_events_pinned">"%1$s 메시지 고정"</string>
|
||||
<string name="state_event_room_pinned_events_pinned_by_you">"당신은 메시지를 고정했습니다."</string>
|
||||
<string name="state_event_room_pinned_events_unpinned">"%1$s 메시지 고정 해제"</string>
|
||||
<string name="state_event_room_pinned_events_unpinned_by_you">"당신은 메시지 고정 해제"</string>
|
||||
<string name="state_event_room_reject">"%1$s님이 초대를 거부함"</string>
|
||||
<string name="state_event_room_reject_by_you">"초대를 거부함"</string>
|
||||
<string name="state_event_room_remove">"%1$s님이 %2$s님을 제거함"</string>
|
||||
<string name="state_event_room_remove_by_you">"%1$s님을 제거함"</string>
|
||||
<string name="state_event_room_remove_by_you_with_reason">"제거했습니다 %1$s :%2$s"</string>
|
||||
<string name="state_event_room_remove_with_reason">"%1$s 제거됨 %2$s : %3$s"</string>
|
||||
<string name="state_event_room_third_party_invite">"%1$s님이 %2$s에게 초대를 보냄"</string>
|
||||
<string name="state_event_room_third_party_invite_by_you">"%1$s님에게 초대를 보냄"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite">"%1$s님이 %2$s의 초대를 회수함"</string>
|
||||
<string name="state_event_room_third_party_revoked_invite_by_you">"%1$s의 초대를 회수함"</string>
|
||||
<string name="state_event_room_topic_changed">"%1$s님이 주제를 %2$s으로 변경했습니다."</string>
|
||||
<string name="state_event_room_topic_changed_by_you">"주제 변경함: %1$s"</string>
|
||||
<string name="state_event_room_topic_removed">"%1$s님이 방 주제를 삭제함"</string>
|
||||
<string name="state_event_room_topic_removed_by_you">"방 주제를 삭제함"</string>
|
||||
<string name="state_event_room_unban">"%1$s님이 %2$s님의 차단을 해제함"</string>
|
||||
<string name="state_event_room_unban_by_you">"%1$s님의 차단을 해제함"</string>
|
||||
<string name="state_event_room_unknown_membership_change">"%1$s님이 멤버십에 알려지지 않은 변경 사항을 만들었습니다."</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue