Merge branch 'develop' into feature-oled-black
This commit is contained in:
commit
d0dcbab750
1505 changed files with 14143 additions and 9545 deletions
|
|
@ -9,12 +9,15 @@
|
|||
package io.element.android.libraries.androidutils.ui
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.WindowInsets
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.content.getSystemService
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
fun View.hideKeyboard() {
|
||||
|
|
@ -22,6 +25,32 @@ fun View.hideKeyboard() {
|
|||
imm?.hideSoftInputFromWindow(windowToken, 0)
|
||||
}
|
||||
|
||||
suspend fun View.hideKeyboardAndAwaitAnimation() {
|
||||
val imm = context?.getSystemService<InputMethodManager>()
|
||||
|
||||
val mutex = Mutex()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
setOnApplyWindowInsetsListener { view, insets ->
|
||||
if (!insets.isVisible(WindowInsets.Type.ime())) {
|
||||
mutex.unlock()
|
||||
}
|
||||
insets
|
||||
}
|
||||
imm?.hideSoftInputFromWindow(windowToken, 0)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
imm?.hideSoftInputFromWindow(windowToken, 0, object : ResultReceiver(null) {
|
||||
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
|
||||
if (resultCode == InputMethodManager.RESULT_UNCHANGED_HIDDEN ||
|
||||
resultCode == InputMethodManager.RESULT_HIDDEN) {
|
||||
mutex.unlock()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
mutex.lock()
|
||||
}
|
||||
|
||||
fun View.showKeyboard(andRequestFocus: Boolean = false) {
|
||||
if (andRequestFocus) {
|
||||
requestFocus()
|
||||
|
|
|
|||
|
|
@ -1,4 +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>
|
||||
<string name="error_no_compatible_app_found">"Не найдено приложение для выполнения этого действия."</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.children.ChildEntry
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.navigation.model.combined.plus
|
||||
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
|
||||
|
|
@ -27,6 +26,7 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader
|
||||
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider
|
||||
import io.element.android.libraries.architecture.appyx.SafeChildren
|
||||
import io.element.android.libraries.architecture.overlay.Overlay
|
||||
|
||||
/**
|
||||
|
|
@ -66,9 +66,9 @@ inline fun <reified NavTarget : Any> BaseFlowNode<NavTarget>.BackstackView(
|
|||
transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) },
|
||||
),
|
||||
) {
|
||||
Children(
|
||||
modifier = modifier,
|
||||
SafeChildren(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
transitionHandler = transitionHandler,
|
||||
)
|
||||
}
|
||||
|
|
@ -78,9 +78,9 @@ inline fun <reified NavTarget : Any> BaseFlowNode<NavTarget>.OverlayView(
|
|||
modifier: Modifier = Modifier,
|
||||
transitionHandler: TransitionHandler<NavTarget, BackStack.State> = rememberBackstackFader(),
|
||||
) {
|
||||
Children(
|
||||
modifier = modifier,
|
||||
SafeChildren(
|
||||
navModel = overlay,
|
||||
modifier = modifier,
|
||||
transitionHandler = transitionHandler,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
package io.element.android.libraries.architecture.appyx
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.currentCompositeKeyHashCode
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import com.bumble.appyx.core.integration.NodeFactory
|
||||
import com.bumble.appyx.core.integrationpoint.IntegrationPoint
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.navigation.NavElement
|
||||
import com.bumble.appyx.core.navigation.NavKey
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.build
|
||||
import com.bumble.appyx.core.state.SavedStateMap
|
||||
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectory
|
||||
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
|
||||
import timber.log.Timber
|
||||
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains the last captured navigation state in a human-readable format, which can be attached to crash reports to help
|
||||
* with debugging `TransactionTooLargeException` crashes.
|
||||
*/
|
||||
var lastCapturedNavState: String = "No nav state captured yet"
|
||||
|
||||
private data class NodeEntry(
|
||||
val navKey: Any?,
|
||||
val children: List<NodeEntry> = emptyList()
|
||||
) {
|
||||
override fun toString(): String {
|
||||
val key = navKey ?: return ""
|
||||
return buildString {
|
||||
append(key.javaClass.name)
|
||||
if (children.isNotEmpty()) {
|
||||
append("=[")
|
||||
append(children.joinToString(", "))
|
||||
append("]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun Map<String, Any?>.buildNavStateMap(): List<NodeEntry> {
|
||||
val children = this["ChildrenState"] as? Map<NavKey<*>, Map<String, Any?>> ?: return emptyList()
|
||||
return children.entries.map { (key, value) ->
|
||||
NodeEntry(
|
||||
navKey = key.navTarget,
|
||||
children = value.buildNavStateMap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun Map<String, Any?>.buildNavModel(name: String): List<NodeEntry> {
|
||||
val navModel = this[name] as? List<NavElement<*, *>> ?: return emptyList()
|
||||
return navModel.map {
|
||||
NodeEntry(
|
||||
navKey = it.key.navTarget,
|
||||
children = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Once we have fixed the `TransactionTooLargeException` issues, we should remove this and use the default `NodeHost` implementation
|
||||
@Suppress("ComposableParamOrder") // detekt complains as 'factory' param isn't a pure lambda
|
||||
@Composable
|
||||
fun <N : Node> DebugNavStateNodeHost(
|
||||
integrationPoint: IntegrationPoint,
|
||||
modifier: Modifier = Modifier,
|
||||
customisations: NodeCustomisationDirectory = remember { NodeCustomisationDirectoryImpl() },
|
||||
factory: NodeFactory<N>
|
||||
) {
|
||||
val node by rememberNode(factory, "AppyxMainNode", customisations, integrationPoint)
|
||||
DisposableEffect(node) {
|
||||
onDispose { node.updateLifecycleState(Lifecycle.State.DESTROYED) }
|
||||
}
|
||||
node.Compose(modifier = modifier)
|
||||
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||
DisposableEffect(lifecycle) {
|
||||
node.updateLifecycleState(lifecycle.currentState)
|
||||
val observer = LifecycleEventObserver { source, _ ->
|
||||
node.updateLifecycleState(source.lifecycle.currentState)
|
||||
}
|
||||
lifecycle.addObserver(observer)
|
||||
onDispose { lifecycle.removeObserver(observer) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <N : Node> rememberNode(
|
||||
factory: NodeFactory<N>,
|
||||
key: String,
|
||||
customisations: NodeCustomisationDirectory,
|
||||
integrationPoint: IntegrationPoint,
|
||||
): State<N> {
|
||||
fun createNode(savedStateMap: SavedStateMap?): N =
|
||||
factory
|
||||
.create(
|
||||
buildContext = BuildContext.root(
|
||||
savedStateMap = savedStateMap,
|
||||
customisations = customisations
|
||||
),
|
||||
)
|
||||
.apply { this.integrationPoint = integrationPoint }
|
||||
.build()
|
||||
|
||||
// This is deprecated because using the custom key would not make this unique, but we work around that by using the currentCompositeKeyHashCode
|
||||
// as part of the key, which should be unique for each call site of rememberNode.
|
||||
@Suppress("DEPRECATION")
|
||||
return rememberSaveable(
|
||||
inputs = arrayOf(),
|
||||
key = "$key:$currentCompositeKeyHashCode",
|
||||
stateSaver = mapSaver(
|
||||
save = { node ->
|
||||
val result = node.saveInstanceState(this)
|
||||
// We want to capture the nav state in a format that's easier to read and understand in crash reports, so we build a custom map for that.
|
||||
val copy = result.toMutableMap()
|
||||
copy["ChildrenState"] = copy.buildNavStateMap()
|
||||
val navModelKey = "NavModel"
|
||||
if (copy.contains(navModelKey)) {
|
||||
copy[navModelKey] = copy.buildNavModel(navModelKey)
|
||||
}
|
||||
val permanentNavModelKey = "com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel"
|
||||
if (copy.contains(permanentNavModelKey)) {
|
||||
copy[permanentNavModelKey] =
|
||||
copy.buildNavModel(permanentNavModelKey)
|
||||
}
|
||||
Timber.d("Saving nav state: $copy")
|
||||
// Store the last nav state in a global variable so that it can be attached to crash reports if the app crashes before the next save happens.
|
||||
lastCapturedNavState = copy.toString()
|
||||
result
|
||||
},
|
||||
restore = { state -> createNode(savedStateMap = state) },
|
||||
),
|
||||
) {
|
||||
mutableStateOf(createNode(savedStateMap = null))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.architecture.appyx
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import com.bumble.appyx.core.composable.Child
|
||||
import com.bumble.appyx.core.composable.ChildRenderer
|
||||
import com.bumble.appyx.core.composable.ChildTransitionScope
|
||||
import com.bumble.appyx.core.navigation.NavKey
|
||||
import com.bumble.appyx.core.navigation.NavModel
|
||||
import com.bumble.appyx.core.navigation.transition.JumpToEndTransitionHandler
|
||||
import com.bumble.appyx.core.navigation.transition.TransitionBounds
|
||||
import com.bumble.appyx.core.navigation.transition.TransitionDescriptor
|
||||
import com.bumble.appyx.core.navigation.transition.TransitionHandler
|
||||
import com.bumble.appyx.core.navigation.transition.TransitionParams
|
||||
import com.bumble.appyx.core.node.LocalMovableContentMap
|
||||
import com.bumble.appyx.core.node.LocalNodeTargetVisibility
|
||||
import com.bumble.appyx.core.node.LocalSharedElementScope
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
import io.element.android.libraries.core.coroutine.withPreviousValue
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import timber.log.Timber
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// All the components in this file come from Appyx, and they've been modified to fix an issue with
|
||||
// the saved state. The parts that are modified are marked.
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Immutable
|
||||
class SafeChildrenTransitionScope<T : Any, S>(
|
||||
val transitionHandler: TransitionHandler<T, S>,
|
||||
val transitionParams: TransitionParams,
|
||||
val navModel: NavModel<T, S>
|
||||
) {
|
||||
|
||||
@Composable
|
||||
inline fun <reified V : T> ParentNode<T>.children(
|
||||
noinline block: @Composable ChildTransitionScope<S>.(
|
||||
child: ChildRenderer,
|
||||
transitionDescriptor: TransitionDescriptor<T, S>
|
||||
) -> Unit,
|
||||
) {
|
||||
safeChildren(V::class, block)
|
||||
}
|
||||
|
||||
@Composable
|
||||
inline fun <reified V : T> ParentNode<T>.children(
|
||||
noinline block: @Composable ChildTransitionScope<S>.(child: ChildRenderer) -> Unit,
|
||||
) {
|
||||
safeChildren(V::class, block)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@SuppressLint("ComposableNaming")
|
||||
fun ParentNode<T>.safeChildren(
|
||||
clazz: KClass<out T>,
|
||||
block: @Composable ChildTransitionScope<S>.(ChildRenderer) -> Unit,
|
||||
) {
|
||||
_safeChildren(clazz) { scope, child, _ ->
|
||||
scope.block(child)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@SuppressLint("ComposableNaming")
|
||||
fun ParentNode<T>.safeChildren(
|
||||
clazz: KClass<out T>,
|
||||
block: @Composable ChildTransitionScope<S>.(
|
||||
ChildRenderer,
|
||||
TransitionDescriptor<T, S>
|
||||
) -> Unit,
|
||||
) {
|
||||
_safeChildren(clazz) { scope, child, descriptor ->
|
||||
scope.block(
|
||||
child,
|
||||
descriptor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
private fun ParentNode<T>._safeChildren(
|
||||
clazz: KClass<out T>,
|
||||
block: @Composable (
|
||||
transitionScope: ChildTransitionScope<S>,
|
||||
child: ChildRenderer,
|
||||
transitionDescriptor: TransitionDescriptor<T, S>
|
||||
) -> Unit
|
||||
) {
|
||||
val saveableStateHolder = rememberSaveableStateHolder()
|
||||
|
||||
val disposedNavKeys = remember { mutableSetOf<NavKey<T>>() }
|
||||
|
||||
LaunchedEffect(navModel) {
|
||||
navModel
|
||||
.removedElementKeys()
|
||||
.map { list ->
|
||||
list.filter { clazz.isInstance(it.navTarget) }
|
||||
}
|
||||
////////// MODIFIED ////////////
|
||||
.filter { it.isNotEmpty() }
|
||||
.collect { deletedKeys ->
|
||||
deletedKeys.forEach { navKey ->
|
||||
// Wait for the NavKey to be disposed before removing its key from saveableStateHolder:
|
||||
// Otherwise, the child SaveableStateRegistry will be removed but not the `SavedState`, which will accumulate
|
||||
// and may cause TransactionTooLargeExceptions
|
||||
while (!disposedNavKeys.contains(navKey)) {
|
||||
delay(10)
|
||||
}
|
||||
disposedNavKeys.remove(navKey)
|
||||
Timber.v("Removed NavKey ${navKey} from saveableStateHolder. NavTarget: ${navKey.navTarget}")
|
||||
saveableStateHolder.removeState(navKey)
|
||||
}
|
||||
}
|
||||
////////// END OF MODIFIED ////////////
|
||||
}
|
||||
|
||||
val screenStateFlow = remember {
|
||||
this@SafeChildrenTransitionScope
|
||||
.navModel
|
||||
.screenState
|
||||
}
|
||||
|
||||
val children by screenStateFlow.collectAsState()
|
||||
|
||||
children
|
||||
.onScreen
|
||||
.filter { clazz.isInstance(it.key.navTarget) }
|
||||
.forEach { navElement ->
|
||||
key(navElement.key.id) {
|
||||
CompositionLocalProvider(
|
||||
LocalNodeTargetVisibility provides
|
||||
children.onScreenWithVisibleTargetState.contains(navElement)
|
||||
) {
|
||||
Child(
|
||||
navElement,
|
||||
saveableStateHolder,
|
||||
transitionParams,
|
||||
transitionHandler,
|
||||
block
|
||||
)
|
||||
|
||||
////////// MODIFIED ////////////
|
||||
DisposableEffect(navElement.key) {
|
||||
onDispose {
|
||||
Timber.v("Disposed NavKey ${navElement.key}. NavTarget: ${navElement.key.navTarget}")
|
||||
disposedNavKeys.add(navElement.key)
|
||||
}
|
||||
}
|
||||
////////// END OF MODIFIED ////////////
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
inline fun <reified NavTarget : Any, State> ParentNode<NavTarget>.SafeChildren(
|
||||
navModel: NavModel<NavTarget, State>,
|
||||
modifier: Modifier = Modifier,
|
||||
transitionHandler: TransitionHandler<NavTarget, State> = remember { JumpToEndTransitionHandler() },
|
||||
withSharedElementTransition: Boolean = false,
|
||||
withMovableContent: Boolean = false,
|
||||
noinline block: @Composable SafeChildrenTransitionScope<NavTarget, State>.() -> Unit = {
|
||||
children<NavTarget> { child ->
|
||||
child()
|
||||
}
|
||||
}
|
||||
) {
|
||||
val density = LocalDensity.current.density
|
||||
var transitionBounds by remember { mutableStateOf(IntSize(0, 0)) }
|
||||
val transitionParams by remember(transitionBounds) {
|
||||
derivedStateOf {
|
||||
TransitionParams(
|
||||
bounds = TransitionBounds(
|
||||
width = Dp(transitionBounds.width / density),
|
||||
height = Dp(transitionBounds.height / density)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (withSharedElementTransition) {
|
||||
SharedTransitionLayout(modifier = modifier
|
||||
.onSizeChanged {
|
||||
transitionBounds = it
|
||||
}
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
/** LocalSharedElementScope will be consumed by children UI to apply shareElement modifier */
|
||||
LocalSharedElementScope provides this,
|
||||
LocalMovableContentMap provides if (withMovableContent) mutableMapOf() else null
|
||||
) {
|
||||
block(
|
||||
SafeChildrenTransitionScope(
|
||||
transitionHandler = transitionHandler,
|
||||
transitionParams = transitionParams,
|
||||
navModel = navModel
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(modifier = modifier
|
||||
.onSizeChanged {
|
||||
transitionBounds = it
|
||||
}
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
/** If sharedElement is not supported for this Node - provide null otherwise children
|
||||
* can consume ascendant's LocalSharedElementScope */
|
||||
LocalSharedElementScope provides null,
|
||||
LocalMovableContentMap provides if (withMovableContent) mutableMapOf() else null
|
||||
) {
|
||||
block(
|
||||
SafeChildrenTransitionScope(
|
||||
transitionHandler = transitionHandler,
|
||||
transitionParams = transitionParams,
|
||||
navModel = navModel
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun <T: Any, S> NavModel<T, S>.removedElementKeys(): Flow<List<NavKey<T>>> {
|
||||
return this.elements.withPreviousValue()
|
||||
.map { (previous, current) ->
|
||||
val previousKeys = previous?.map { it.key }.orEmpty()
|
||||
val currentKeys = current.map { it.key }
|
||||
previousKeys.filter { element ->
|
||||
!currentKeys.contains(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,10 +39,17 @@ class DefaultAudioFocus(
|
|||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
// Do nothing
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS,
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
// Permanent focus loss (e.g., phone call) — always stop/pause.
|
||||
onFocusLost()
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
onFocusLost()
|
||||
// For recording, ignore transient focus losses (e.g., notification sounds).
|
||||
// The AudioRecord API keeps capturing regardless.
|
||||
if (requester != AudioFocusRequester.RecordVoiceMessage) {
|
||||
onFocusLost()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,7 +58,12 @@ class DefaultAudioFocus(
|
|||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(requester.toAudioUsage())
|
||||
.build()
|
||||
val request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||
val focusGain = if (requester == AudioFocusRequester.RecordVoiceMessage) {
|
||||
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
|
||||
} else {
|
||||
AudioManager.AUDIOFOCUS_GAIN
|
||||
}
|
||||
val request = AudioFocusRequest.Builder(focusGain)
|
||||
.setAudioAttributes(audioAttributes)
|
||||
.setOnAudioFocusChangeListener(listener)
|
||||
.setWillPauseWhenDucked(requester.willPausedWhenDucked())
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fa16f659aa3e7d05fa03a51d52faddc0c40c3ab52231687f8c6c8a4ba81ff6f0
|
||||
size 219813
|
||||
oid sha256:37f6acca46890e98087ece62e2716fa60791479fab02999406050517e3b79307
|
||||
size 240187
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72fb457dc50bf1a2261502fc1da15c01ab415344e9070354d38dc7b74234d790
|
||||
size 232095
|
||||
oid sha256:a2de5e6d24dcbe0baa75a69485f5a308466fa599625bcbdb0cb96e9bc5a1b708
|
||||
size 253233
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24cfe760717881ee71f36fae1fb201e74b2c32a2f9a5aef71ef21dab69ea5366
|
||||
size 233212
|
||||
oid sha256:ae1cb46d82acbb23cc172f41e20a41bbe88c350ab53c20e5b2a91f2c16590fbf
|
||||
size 254525
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c846cd10b83361c368bdbb31ed6220cc22693c3cbf52791fb369841af1e9ea48
|
||||
size 327701
|
||||
oid sha256:a9334d37f010d4e520b11dbd16d664fbb4413497d371dc8b0af0157faf870451
|
||||
size 323086
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05b35fedbd53dec2cc5c4c211a8db1a56055963de69425ddae2cab5aff7e3e75
|
||||
size 325750
|
||||
oid sha256:9e016ef5e07de6f6e86e5e6104d78502f5ee15ecb39d1533f020cf94ac087603
|
||||
size 320821
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d98e64eda5d6333067ccc599e99636f618331397207bb7534595e2756edb75e
|
||||
size 309312
|
||||
oid sha256:93df69ddd7a1571abcb868495edb9914b5d832c1e55f1520a1c04a71de59577f
|
||||
size 302213
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5ccbf1234065b182939f001eb65eca0a62adae41a2d91ef0307d27b059407178
|
||||
size 309084
|
||||
oid sha256:b1eb3a0283e42d2e2d1083c95fd2bbd2e338fcc5f318c07386f04cfb97e6fed7
|
||||
size 301963
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a699170cabca6fb912d034a588b45961485afe6ef6d2c24f0ab79f10ae00c168
|
||||
size 85629
|
||||
oid sha256:8a8a9b6e61758a40d01028a4edb4a4d21b845b83b3e0793ed0934e48f3d9eea0
|
||||
size 94637
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dd4b2a40fcf02d6db29cb0bc371d93236b4a0be6d4446bab86358692cddb53f5
|
||||
size 91692
|
||||
oid sha256:7f29d225df71587fefe07ec8739b84f1a0469786c6b1d6778da0bad33d19574e
|
||||
size 101183
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -123,6 +123,7 @@ private fun getSemanticColors(): ImmutableMap<String, Color> {
|
|||
"bgBadgeAccent" to bgBadgeAccent,
|
||||
"bgBadgeDefault" to bgBadgeDefault,
|
||||
"bgBadgeInfo" to bgBadgeInfo,
|
||||
"bgBadgePrimary" to bgBadgePrimary,
|
||||
"bgCanvasDefault" to bgCanvasDefault,
|
||||
"bgCanvasDefaultLevel1" to bgCanvasDefaultLevel1,
|
||||
"bgCanvasDisabled" to bgCanvasDisabled,
|
||||
|
|
@ -156,12 +157,10 @@ private fun getSemanticColors(): ImmutableMap<String, Color> {
|
|||
"gradientActionStop2" to gradientActionStop2,
|
||||
"gradientActionStop3" to gradientActionStop3,
|
||||
"gradientActionStop4" to gradientActionStop4,
|
||||
"gradientCriticalStop1" to gradientCriticalStop1,
|
||||
"gradientCriticalStop2" to gradientCriticalStop2,
|
||||
"gradientInfoStop1" to gradientInfoStop1,
|
||||
"gradientInfoStop2" to gradientInfoStop2,
|
||||
"gradientInfoStop3" to gradientInfoStop3,
|
||||
"gradientInfoStop4" to gradientInfoStop4,
|
||||
"gradientInfoStop5" to gradientInfoStop5,
|
||||
"gradientInfoStop6" to gradientInfoStop6,
|
||||
"gradientSubtleStop1" to gradientSubtleStop1,
|
||||
"gradientSubtleStop2" to gradientSubtleStop2,
|
||||
"gradientSubtleStop3" to gradientSubtleStop3,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
@ -25,6 +25,9 @@ object CompoundIcons {
|
|||
@Composable fun Admin(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_admin)
|
||||
}
|
||||
@Composable fun AdvancedSettings(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_advanced_settings)
|
||||
}
|
||||
@Composable fun ArrowDown(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_arrow_down)
|
||||
}
|
||||
|
|
@ -64,6 +67,9 @@ object CompoundIcons {
|
|||
@Composable fun Bold(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_bold)
|
||||
}
|
||||
@Composable fun Bug(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_bug)
|
||||
}
|
||||
@Composable fun Calendar(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_calendar)
|
||||
}
|
||||
|
|
@ -142,6 +148,9 @@ object CompoundIcons {
|
|||
@Composable fun Delete(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_delete)
|
||||
}
|
||||
@Composable fun DevicePasskey(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_device_passkey)
|
||||
}
|
||||
@Composable fun Devices(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_devices)
|
||||
}
|
||||
|
|
@ -220,6 +229,9 @@ object CompoundIcons {
|
|||
@Composable fun Filter(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_filter)
|
||||
}
|
||||
@Composable fun Folder(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_folder)
|
||||
}
|
||||
@Composable fun Forward(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_forward)
|
||||
}
|
||||
|
|
@ -460,6 +472,9 @@ object CompoundIcons {
|
|||
@Composable fun RaisedHandSolid(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_raised_hand_solid)
|
||||
}
|
||||
@Composable fun ReOrder(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_re_order)
|
||||
}
|
||||
@Composable fun Reaction(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_reaction)
|
||||
}
|
||||
|
|
@ -478,9 +493,18 @@ object CompoundIcons {
|
|||
@Composable fun Room(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_room)
|
||||
}
|
||||
@Composable fun RotateLeft(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_rotate_left)
|
||||
}
|
||||
@Composable fun RotateRight(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_rotate_right)
|
||||
}
|
||||
@Composable fun Search(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_search)
|
||||
}
|
||||
@Composable fun Section(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_section)
|
||||
}
|
||||
@Composable fun Send(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_send)
|
||||
}
|
||||
|
|
@ -535,6 +559,12 @@ object CompoundIcons {
|
|||
@Composable fun Sticker(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_sticker)
|
||||
}
|
||||
@Composable fun Stop(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_stop)
|
||||
}
|
||||
@Composable fun StopSolid(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_stop_solid)
|
||||
}
|
||||
@Composable fun Strikethrough(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_strikethrough)
|
||||
}
|
||||
|
|
@ -550,6 +580,9 @@ object CompoundIcons {
|
|||
@Composable fun TextFormatting(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_text_formatting)
|
||||
}
|
||||
@Composable fun Theme(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_theme)
|
||||
}
|
||||
@Composable fun Threads(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_threads)
|
||||
}
|
||||
|
|
@ -559,6 +592,12 @@ object CompoundIcons {
|
|||
@Composable fun Time(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_time)
|
||||
}
|
||||
@Composable fun Translate(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_translate)
|
||||
}
|
||||
@Composable fun Tree(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_tree)
|
||||
}
|
||||
@Composable fun Underline(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_underline)
|
||||
}
|
||||
|
|
@ -607,6 +646,9 @@ object CompoundIcons {
|
|||
@Composable fun VideoCallOffSolid(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_video_call_off_solid)
|
||||
}
|
||||
@Composable fun VideoCallOutgoingSolid(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_video_call_outgoing_solid)
|
||||
}
|
||||
@Composable fun VideoCallSolid(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_video_call_solid)
|
||||
}
|
||||
|
|
@ -619,6 +661,15 @@ object CompoundIcons {
|
|||
@Composable fun VoiceCall(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call)
|
||||
}
|
||||
@Composable fun VoiceCallDeclinedSolid(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_declined_solid)
|
||||
}
|
||||
@Composable fun VoiceCallMissedSolid(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_missed_solid)
|
||||
}
|
||||
@Composable fun VoiceCallOutgoingSolid(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_outgoing_solid)
|
||||
}
|
||||
@Composable fun VoiceCallSolid(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_voice_call_solid)
|
||||
}
|
||||
|
|
@ -643,9 +694,16 @@ object CompoundIcons {
|
|||
@Composable fun Windows(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_windows)
|
||||
}
|
||||
@Composable fun ZoomIn(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_zoom_in)
|
||||
}
|
||||
@Composable fun ZoomOut(): ImageVector {
|
||||
return ImageVector.vectorResource(R.drawable.ic_compound_zoom_out)
|
||||
}
|
||||
|
||||
val all @Composable get() = persistentListOf<ImageVector>(
|
||||
Admin(),
|
||||
AdvancedSettings(),
|
||||
ArrowDown(),
|
||||
ArrowLeft(),
|
||||
ArrowRight(),
|
||||
|
|
@ -659,6 +717,7 @@ object CompoundIcons {
|
|||
BackspaceSolid(),
|
||||
Block(),
|
||||
Bold(),
|
||||
Bug(),
|
||||
Calendar(),
|
||||
Chart(),
|
||||
Chat(),
|
||||
|
|
@ -685,6 +744,7 @@ object CompoundIcons {
|
|||
Copy(),
|
||||
DarkMode(),
|
||||
Delete(),
|
||||
DevicePasskey(),
|
||||
Devices(),
|
||||
DialPad(),
|
||||
Document(),
|
||||
|
|
@ -711,6 +771,7 @@ object CompoundIcons {
|
|||
FileError(),
|
||||
Files(),
|
||||
Filter(),
|
||||
Folder(),
|
||||
Forward(),
|
||||
FullScreen(),
|
||||
Grid(),
|
||||
|
|
@ -791,13 +852,17 @@ object CompoundIcons {
|
|||
QrCode(),
|
||||
Quote(),
|
||||
RaisedHandSolid(),
|
||||
ReOrder(),
|
||||
Reaction(),
|
||||
ReactionAdd(),
|
||||
ReactionSolid(),
|
||||
Reply(),
|
||||
Restart(),
|
||||
Room(),
|
||||
RotateLeft(),
|
||||
RotateRight(),
|
||||
Search(),
|
||||
Section(),
|
||||
Send(),
|
||||
SendSolid(),
|
||||
Settings(),
|
||||
|
|
@ -816,14 +881,19 @@ object CompoundIcons {
|
|||
Spotlight(),
|
||||
SpotlightView(),
|
||||
Sticker(),
|
||||
Stop(),
|
||||
StopSolid(),
|
||||
Strikethrough(),
|
||||
SwitchCameraSolid(),
|
||||
TakePhoto(),
|
||||
TakePhotoSolid(),
|
||||
TextFormatting(),
|
||||
Theme(),
|
||||
Threads(),
|
||||
ThreadsSolid(),
|
||||
Time(),
|
||||
Translate(),
|
||||
Tree(),
|
||||
Underline(),
|
||||
Unknown(),
|
||||
UnknownSolid(),
|
||||
|
|
@ -840,10 +910,14 @@ object CompoundIcons {
|
|||
VideoCallMissedSolid(),
|
||||
VideoCallOff(),
|
||||
VideoCallOffSolid(),
|
||||
VideoCallOutgoingSolid(),
|
||||
VideoCallSolid(),
|
||||
VisibilityOff(),
|
||||
VisibilityOn(),
|
||||
VoiceCall(),
|
||||
VoiceCallDeclinedSolid(),
|
||||
VoiceCallMissedSolid(),
|
||||
VoiceCallOutgoingSolid(),
|
||||
VoiceCallSolid(),
|
||||
VolumeOff(),
|
||||
VolumeOffSolid(),
|
||||
|
|
@ -852,10 +926,13 @@ object CompoundIcons {
|
|||
Warning(),
|
||||
WebBrowser(),
|
||||
Windows(),
|
||||
ZoomIn(),
|
||||
ZoomOut(),
|
||||
)
|
||||
|
||||
val allResIds get() = persistentListOf(
|
||||
R.drawable.ic_compound_admin,
|
||||
R.drawable.ic_compound_advanced_settings,
|
||||
R.drawable.ic_compound_arrow_down,
|
||||
R.drawable.ic_compound_arrow_left,
|
||||
R.drawable.ic_compound_arrow_right,
|
||||
|
|
@ -869,6 +946,7 @@ object CompoundIcons {
|
|||
R.drawable.ic_compound_backspace_solid,
|
||||
R.drawable.ic_compound_block,
|
||||
R.drawable.ic_compound_bold,
|
||||
R.drawable.ic_compound_bug,
|
||||
R.drawable.ic_compound_calendar,
|
||||
R.drawable.ic_compound_chart,
|
||||
R.drawable.ic_compound_chat,
|
||||
|
|
@ -895,6 +973,7 @@ object CompoundIcons {
|
|||
R.drawable.ic_compound_copy,
|
||||
R.drawable.ic_compound_dark_mode,
|
||||
R.drawable.ic_compound_delete,
|
||||
R.drawable.ic_compound_device_passkey,
|
||||
R.drawable.ic_compound_devices,
|
||||
R.drawable.ic_compound_dial_pad,
|
||||
R.drawable.ic_compound_document,
|
||||
|
|
@ -921,6 +1000,7 @@ object CompoundIcons {
|
|||
R.drawable.ic_compound_file_error,
|
||||
R.drawable.ic_compound_files,
|
||||
R.drawable.ic_compound_filter,
|
||||
R.drawable.ic_compound_folder,
|
||||
R.drawable.ic_compound_forward,
|
||||
R.drawable.ic_compound_full_screen,
|
||||
R.drawable.ic_compound_grid,
|
||||
|
|
@ -1001,13 +1081,17 @@ object CompoundIcons {
|
|||
R.drawable.ic_compound_qr_code,
|
||||
R.drawable.ic_compound_quote,
|
||||
R.drawable.ic_compound_raised_hand_solid,
|
||||
R.drawable.ic_compound_re_order,
|
||||
R.drawable.ic_compound_reaction,
|
||||
R.drawable.ic_compound_reaction_add,
|
||||
R.drawable.ic_compound_reaction_solid,
|
||||
R.drawable.ic_compound_reply,
|
||||
R.drawable.ic_compound_restart,
|
||||
R.drawable.ic_compound_room,
|
||||
R.drawable.ic_compound_rotate_left,
|
||||
R.drawable.ic_compound_rotate_right,
|
||||
R.drawable.ic_compound_search,
|
||||
R.drawable.ic_compound_section,
|
||||
R.drawable.ic_compound_send,
|
||||
R.drawable.ic_compound_send_solid,
|
||||
R.drawable.ic_compound_settings,
|
||||
|
|
@ -1026,14 +1110,19 @@ object CompoundIcons {
|
|||
R.drawable.ic_compound_spotlight,
|
||||
R.drawable.ic_compound_spotlight_view,
|
||||
R.drawable.ic_compound_sticker,
|
||||
R.drawable.ic_compound_stop,
|
||||
R.drawable.ic_compound_stop_solid,
|
||||
R.drawable.ic_compound_strikethrough,
|
||||
R.drawable.ic_compound_switch_camera_solid,
|
||||
R.drawable.ic_compound_take_photo,
|
||||
R.drawable.ic_compound_take_photo_solid,
|
||||
R.drawable.ic_compound_text_formatting,
|
||||
R.drawable.ic_compound_theme,
|
||||
R.drawable.ic_compound_threads,
|
||||
R.drawable.ic_compound_threads_solid,
|
||||
R.drawable.ic_compound_time,
|
||||
R.drawable.ic_compound_translate,
|
||||
R.drawable.ic_compound_tree,
|
||||
R.drawable.ic_compound_underline,
|
||||
R.drawable.ic_compound_unknown,
|
||||
R.drawable.ic_compound_unknown_solid,
|
||||
|
|
@ -1050,10 +1139,14 @@ object CompoundIcons {
|
|||
R.drawable.ic_compound_video_call_missed_solid,
|
||||
R.drawable.ic_compound_video_call_off,
|
||||
R.drawable.ic_compound_video_call_off_solid,
|
||||
R.drawable.ic_compound_video_call_outgoing_solid,
|
||||
R.drawable.ic_compound_video_call_solid,
|
||||
R.drawable.ic_compound_visibility_off,
|
||||
R.drawable.ic_compound_visibility_on,
|
||||
R.drawable.ic_compound_voice_call,
|
||||
R.drawable.ic_compound_voice_call_declined_solid,
|
||||
R.drawable.ic_compound_voice_call_missed_solid,
|
||||
R.drawable.ic_compound_voice_call_outgoing_solid,
|
||||
R.drawable.ic_compound_voice_call_solid,
|
||||
R.drawable.ic_compound_volume_off,
|
||||
R.drawable.ic_compound_volume_off_solid,
|
||||
|
|
@ -1062,5 +1155,7 @@ object CompoundIcons {
|
|||
R.drawable.ic_compound_warning,
|
||||
R.drawable.ic_compound_web_browser,
|
||||
R.drawable.ic_compound_windows,
|
||||
R.drawable.ic_compound_zoom_in,
|
||||
R.drawable.ic_compound_zoom_out,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
@ -49,12 +49,12 @@ data class SemanticColors(
|
|||
val bgActionTertiaryRest: Color,
|
||||
/** Background colour for tertiary actions. State: Selected */
|
||||
val bgActionTertiarySelected: Color,
|
||||
/** Badge accent background colour */
|
||||
val bgBadgeAccent: Color,
|
||||
/** Badge default background colour */
|
||||
val bgBadgeCritical: Color,
|
||||
val bgBadgeDefault: Color,
|
||||
/** Badge info background colour */
|
||||
val bgBadgeInfo: Color,
|
||||
val bgBadgePrimary: Color,
|
||||
val bgBadgeSecondary: Color,
|
||||
/** Default global background for the user interface. Elevation: Default (Level 0) */
|
||||
val bgCanvasDefault: Color,
|
||||
/** Default global background for the user interface. Elevation: Level 1. */
|
||||
|
|
@ -121,18 +121,14 @@ data class SemanticColors(
|
|||
val gradientActionStop3: Color,
|
||||
/** Background gradient stop for super and send buttons */
|
||||
val gradientActionStop4: Color,
|
||||
/** Subtle background gradient stop for critical */
|
||||
val gradientCriticalStop1: Color,
|
||||
/** Subtle background gradient stop for critical */
|
||||
val gradientCriticalStop2: Color,
|
||||
/** Subtle background gradient stop for info */
|
||||
val gradientInfoStop1: Color,
|
||||
/** Subtle background gradient stop for info */
|
||||
val gradientInfoStop2: Color,
|
||||
/** Subtle background gradient stop for info */
|
||||
val gradientInfoStop3: Color,
|
||||
/** Subtle background gradient stop for info */
|
||||
val gradientInfoStop4: Color,
|
||||
/** Subtle background gradient stop for info */
|
||||
val gradientInfoStop5: Color,
|
||||
/** Subtle background gradient stop for info */
|
||||
val gradientInfoStop6: Color,
|
||||
/** Subtle background gradient stop for message highlight and bloom */
|
||||
val gradientSubtleStop1: Color,
|
||||
/** Subtle background gradient stop for message highlight and bloom */
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
@ -37,9 +37,12 @@ val compoundColorsDark = SemanticColors(
|
|||
bgActionTertiaryHovered = DarkColorTokens.colorGray300,
|
||||
bgActionTertiaryRest = DarkColorTokens.colorThemeBg,
|
||||
bgActionTertiarySelected = DarkColorTokens.colorGray400,
|
||||
bgBadgeAccent = DarkColorTokens.colorAlphaGreen500,
|
||||
bgBadgeDefault = DarkColorTokens.colorAlphaGray500,
|
||||
bgBadgeInfo = DarkColorTokens.colorAlphaBlue500,
|
||||
bgBadgeAccent = DarkColorTokens.colorGreen400,
|
||||
bgBadgeCritical = DarkColorTokens.colorRed300,
|
||||
bgBadgeDefault = DarkColorTokens.colorThemeBg,
|
||||
bgBadgeInfo = DarkColorTokens.colorBlue400,
|
||||
bgBadgePrimary = DarkColorTokens.colorGray1400,
|
||||
bgBadgeSecondary = DarkColorTokens.colorGray400,
|
||||
bgCanvasDefault = DarkColorTokens.colorThemeBg,
|
||||
bgCanvasDefaultLevel1 = DarkColorTokens.colorGray300,
|
||||
bgCanvasDisabled = DarkColorTokens.colorGray200,
|
||||
|
|
@ -73,12 +76,10 @@ val compoundColorsDark = SemanticColors(
|
|||
gradientActionStop2 = DarkColorTokens.colorGreen900,
|
||||
gradientActionStop3 = DarkColorTokens.colorGreen700,
|
||||
gradientActionStop4 = DarkColorTokens.colorGreen500,
|
||||
gradientInfoStop1 = DarkColorTokens.colorAlphaBlue500,
|
||||
gradientInfoStop2 = DarkColorTokens.colorAlphaBlue400,
|
||||
gradientInfoStop3 = DarkColorTokens.colorAlphaBlue300,
|
||||
gradientInfoStop4 = DarkColorTokens.colorAlphaBlue200,
|
||||
gradientInfoStop5 = DarkColorTokens.colorAlphaBlue100,
|
||||
gradientInfoStop6 = DarkColorTokens.colorTransparent,
|
||||
gradientCriticalStop1 = DarkColorTokens.colorRed200,
|
||||
gradientCriticalStop2 = DarkColorTokens.colorThemeBg,
|
||||
gradientInfoStop1 = DarkColorTokens.colorBlue200,
|
||||
gradientInfoStop2 = DarkColorTokens.colorThemeBg,
|
||||
gradientSubtleStop1 = DarkColorTokens.colorAlphaGreen500,
|
||||
gradientSubtleStop2 = DarkColorTokens.colorAlphaGreen400,
|
||||
gradientSubtleStop3 = DarkColorTokens.colorAlphaGreen300,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
@ -37,9 +37,12 @@ val compoundColorsHcDark = SemanticColors(
|
|||
bgActionTertiaryHovered = DarkHcColorTokens.colorGray300,
|
||||
bgActionTertiaryRest = DarkHcColorTokens.colorThemeBg,
|
||||
bgActionTertiarySelected = DarkHcColorTokens.colorGray400,
|
||||
bgBadgeAccent = DarkHcColorTokens.colorAlphaGreen500,
|
||||
bgBadgeDefault = DarkHcColorTokens.colorAlphaGray500,
|
||||
bgBadgeInfo = DarkHcColorTokens.colorAlphaBlue500,
|
||||
bgBadgeAccent = DarkHcColorTokens.colorGreen400,
|
||||
bgBadgeCritical = DarkHcColorTokens.colorRed300,
|
||||
bgBadgeDefault = DarkHcColorTokens.colorThemeBg,
|
||||
bgBadgeInfo = DarkHcColorTokens.colorBlue400,
|
||||
bgBadgePrimary = DarkHcColorTokens.colorGray1400,
|
||||
bgBadgeSecondary = DarkHcColorTokens.colorGray400,
|
||||
bgCanvasDefault = DarkHcColorTokens.colorThemeBg,
|
||||
bgCanvasDefaultLevel1 = DarkHcColorTokens.colorGray300,
|
||||
bgCanvasDisabled = DarkHcColorTokens.colorGray200,
|
||||
|
|
@ -73,12 +76,10 @@ val compoundColorsHcDark = SemanticColors(
|
|||
gradientActionStop2 = DarkHcColorTokens.colorGreen900,
|
||||
gradientActionStop3 = DarkHcColorTokens.colorGreen700,
|
||||
gradientActionStop4 = DarkHcColorTokens.colorGreen500,
|
||||
gradientInfoStop1 = DarkHcColorTokens.colorAlphaBlue500,
|
||||
gradientInfoStop2 = DarkHcColorTokens.colorAlphaBlue400,
|
||||
gradientInfoStop3 = DarkHcColorTokens.colorAlphaBlue300,
|
||||
gradientInfoStop4 = DarkHcColorTokens.colorAlphaBlue200,
|
||||
gradientInfoStop5 = DarkHcColorTokens.colorAlphaBlue100,
|
||||
gradientInfoStop6 = DarkHcColorTokens.colorTransparent,
|
||||
gradientCriticalStop1 = DarkHcColorTokens.colorRed200,
|
||||
gradientCriticalStop2 = DarkHcColorTokens.colorThemeBg,
|
||||
gradientInfoStop1 = DarkHcColorTokens.colorBlue200,
|
||||
gradientInfoStop2 = DarkHcColorTokens.colorThemeBg,
|
||||
gradientSubtleStop1 = DarkHcColorTokens.colorAlphaGreen500,
|
||||
gradientSubtleStop2 = DarkHcColorTokens.colorAlphaGreen400,
|
||||
gradientSubtleStop3 = DarkHcColorTokens.colorAlphaGreen300,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
@ -37,9 +37,12 @@ val compoundColorsLight = SemanticColors(
|
|||
bgActionTertiaryHovered = LightColorTokens.colorGray300,
|
||||
bgActionTertiaryRest = LightColorTokens.colorThemeBg,
|
||||
bgActionTertiarySelected = LightColorTokens.colorGray400,
|
||||
bgBadgeAccent = LightColorTokens.colorAlphaGreen400,
|
||||
bgBadgeDefault = LightColorTokens.colorAlphaGray400,
|
||||
bgBadgeInfo = LightColorTokens.colorAlphaBlue400,
|
||||
bgBadgeAccent = LightColorTokens.colorGreen400,
|
||||
bgBadgeCritical = LightColorTokens.colorRed300,
|
||||
bgBadgeDefault = LightColorTokens.colorThemeBg,
|
||||
bgBadgeInfo = LightColorTokens.colorBlue400,
|
||||
bgBadgePrimary = LightColorTokens.colorGray1400,
|
||||
bgBadgeSecondary = LightColorTokens.colorGray400,
|
||||
bgCanvasDefault = LightColorTokens.colorThemeBg,
|
||||
bgCanvasDefaultLevel1 = LightColorTokens.colorThemeBg,
|
||||
bgCanvasDisabled = LightColorTokens.colorGray200,
|
||||
|
|
@ -73,12 +76,10 @@ val compoundColorsLight = SemanticColors(
|
|||
gradientActionStop2 = LightColorTokens.colorGreen700,
|
||||
gradientActionStop3 = LightColorTokens.colorGreen900,
|
||||
gradientActionStop4 = LightColorTokens.colorGreen1100,
|
||||
gradientInfoStop1 = LightColorTokens.colorAlphaBlue500,
|
||||
gradientInfoStop2 = LightColorTokens.colorAlphaBlue400,
|
||||
gradientInfoStop3 = LightColorTokens.colorAlphaBlue300,
|
||||
gradientInfoStop4 = LightColorTokens.colorAlphaBlue200,
|
||||
gradientInfoStop5 = LightColorTokens.colorAlphaBlue100,
|
||||
gradientInfoStop6 = LightColorTokens.colorTransparent,
|
||||
gradientCriticalStop1 = LightColorTokens.colorRed200,
|
||||
gradientCriticalStop2 = LightColorTokens.colorThemeBg,
|
||||
gradientInfoStop1 = LightColorTokens.colorBlue200,
|
||||
gradientInfoStop2 = LightColorTokens.colorThemeBg,
|
||||
gradientSubtleStop1 = LightColorTokens.colorAlphaGreen500,
|
||||
gradientSubtleStop2 = LightColorTokens.colorAlphaGreen400,
|
||||
gradientSubtleStop3 = LightColorTokens.colorAlphaGreen300,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
@ -37,9 +37,12 @@ val compoundColorsHcLight = SemanticColors(
|
|||
bgActionTertiaryHovered = LightHcColorTokens.colorGray300,
|
||||
bgActionTertiaryRest = LightHcColorTokens.colorThemeBg,
|
||||
bgActionTertiarySelected = LightHcColorTokens.colorGray400,
|
||||
bgBadgeAccent = LightHcColorTokens.colorAlphaGreen400,
|
||||
bgBadgeDefault = LightHcColorTokens.colorAlphaGray400,
|
||||
bgBadgeInfo = LightHcColorTokens.colorAlphaBlue400,
|
||||
bgBadgeAccent = LightHcColorTokens.colorGreen400,
|
||||
bgBadgeCritical = LightHcColorTokens.colorRed300,
|
||||
bgBadgeDefault = LightHcColorTokens.colorThemeBg,
|
||||
bgBadgeInfo = LightHcColorTokens.colorBlue400,
|
||||
bgBadgePrimary = LightHcColorTokens.colorGray1400,
|
||||
bgBadgeSecondary = LightHcColorTokens.colorGray400,
|
||||
bgCanvasDefault = LightHcColorTokens.colorThemeBg,
|
||||
bgCanvasDefaultLevel1 = LightHcColorTokens.colorThemeBg,
|
||||
bgCanvasDisabled = LightHcColorTokens.colorGray200,
|
||||
|
|
@ -73,12 +76,10 @@ val compoundColorsHcLight = SemanticColors(
|
|||
gradientActionStop2 = LightHcColorTokens.colorGreen700,
|
||||
gradientActionStop3 = LightHcColorTokens.colorGreen900,
|
||||
gradientActionStop4 = LightHcColorTokens.colorGreen1100,
|
||||
gradientInfoStop1 = LightHcColorTokens.colorAlphaBlue500,
|
||||
gradientInfoStop2 = LightHcColorTokens.colorAlphaBlue400,
|
||||
gradientInfoStop3 = LightHcColorTokens.colorAlphaBlue300,
|
||||
gradientInfoStop4 = LightHcColorTokens.colorAlphaBlue200,
|
||||
gradientInfoStop5 = LightHcColorTokens.colorAlphaBlue100,
|
||||
gradientInfoStop6 = LightHcColorTokens.colorTransparent,
|
||||
gradientCriticalStop1 = LightHcColorTokens.colorRed200,
|
||||
gradientCriticalStop2 = LightHcColorTokens.colorThemeBg,
|
||||
gradientInfoStop1 = LightHcColorTokens.colorBlue200,
|
||||
gradientInfoStop2 = LightHcColorTokens.colorThemeBg,
|
||||
gradientSubtleStop1 = LightHcColorTokens.colorAlphaGreen500,
|
||||
gradientSubtleStop2 = LightHcColorTokens.colorAlphaGreen400,
|
||||
gradientSubtleStop3 = LightHcColorTokens.colorAlphaGreen300,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2m0,18c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M13.49,11.38c0.43,-1.22 0.17,-2.64 -0.81,-3.62a3.47,3.47 0,0 0,-4.1 -0.59l2.35,2.35 -1.41,1.41 -2.35,-2.35c-0.71,1.32 -0.52,2.99 0.59,4.1 0.98,0.98 2.4,1.24 3.62,0.81l3.41,3.41c0.2,0.2 0.51,0.2 0.71,0l1.4,-1.4c0.2,-0.2 0.2,-0.51 0,-0.71z"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M9.5,2 L11,6.5 15.5,8 11,9.5 9.5,14 8,9.5 3.5,8 8,6.5zM19,11 L20,14 23,15 20,16 19,19 18,16 15,15 18,14zM12,17 L12.75,19.25L15,20l-2.25,0.75L12,23l-0.75,-2.25L9,20l2.25,-0.75z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -4,11 +4,11 @@
|
|||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24H0z"/>
|
||||
<path
|
||||
android:pathData="M21.167,3.75L7.417,3.75c-0.633,0 -1.128,0.32 -1.458,0.807L1,12l4.96,7.434c0.33,0.486 0.824,0.816 1.457,0.816h13.75A1.84,1.84 0,0 0,23 18.417L23,5.583a1.84,1.84 0,0 0,-1.833 -1.833m0,14.667L7.48,18.417L3.2,12l4.272,-6.417h13.695zM10.542,16.583 L13.833,13.293 17.124,16.583 18.417,15.291L15.126,12l3.29,-3.29 -1.292,-1.293 -3.29,3.29 -3.291,-3.29L9.25,8.709 12.54,12l-3.29,3.29z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M15.043,8.457a1,1 0,0 1,1.414 1.414l-2.043,2.043 2.129,2.129a1,1 0,1 1,-1.414 1.414l-2.13,-2.129 -2.127,2.129a1,1 0,0 1,-1.415 -1.414l2.129,-2.129 -2.043,-2.043a1,1 0,0 1,1.414 -1.414L13,10.5z"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M20,4a2,2 0,0 1,2 2v12a2,2 0,0 1,-2 2H7.28a2,2 0,0 1,-1.655 -0.877l-4.072,-6a2,2 0,0 1,0 -2.246l4.072,-6A2,2 0,0 1,7.28 4zM3.208,12l4.072,6H20V6H7.28z"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M21.15,4H7.283c-0.638,0 -1.137,0.311 -1.47,0.782l-4.66,6.73a0.87,0.87 0,0 0,0 0.986l4.66,6.72c0.333,0.462 0.832,0.782 1.47,0.782h13.869C22.168,20 23,19.2 23,18.222V5.778C23,4.8 22.168,4 21.15,4m-3.42,11.822a0.947,0.947 0,0 1,-1.304 0l-2.672,-2.569 -2.672,2.57a0.947,0.947 0,0 1,-1.303 0,0.86 0.86,0 0,1 0,-1.254L12.45,12 9.779,9.431a0.86,0.86 0,0 1,0 -1.253,0.947 0.947,0 0,1 1.303,0l2.672,2.569 2.672,-2.57a0.947,0.947 0,0 1,1.304 0c0.36,0.347 0.36,0.907 0,1.254L15.058,12l2.672,2.569a0.877,0.877 0,0 1,0 1.253"
|
||||
android:fillColor="#FF000000"/>
|
||||
android:pathData="M20,4a2,2 0,0 1,2 2v12a2,2 0,0 1,-2 2L7.33,20a2,2 0,0 1,-1.673 -0.902l-3.937,-6a2,2 0,0 1,0 -2.196l3.937,-6A2,2 0,0 1,7.33 4zM16.457,8.457a1,1 0,0 0,-1.414 0L13,10.5l-2.043,-2.043a1,1 0,0 0,-1.414 1.414l2.043,2.043 -2.129,2.129a1,1 0,0 0,1.414 1.414l2.13,-2.129 2.128,2.129a1,1 0,0 0,1.414 -1.414l-2.129,-2.129 2.043,-2.043a1,1 0,0 0,0 -1.414"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M19,8h-1.81a6,6 0,0 0,-1.82 -1.96l0.93,-0.93a0.996,0.996 0,1 0,-1.41 -1.41l-1.47,1.47C12.96,5.06 12.49,5 12,5s-0.96,0.06 -1.41,0.17L9.11,3.7A0.996,0.996 0,1 0,7.7 5.11l0.92,0.93C7.88,6.55 7.26,7.22 6.81,8H5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h1.09c-0.05,0.33 -0.09,0.66 -0.09,1v1H5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h1v1c0,0.34 0.04,0.67 0.09,1H5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h1.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1h-1.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h1c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1h-1v-1c0,-0.34 -0.04,-0.67 -0.09,-1H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1m-6,8h-2c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h2c0.55,0 1,0.45 1,1s-0.45,1 -1,1m0,-4h-2c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h2c0.55,0 1,0.45 1,1s-0.45,1 -1,1"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M19.5,9.385q1.458,0 2.48,1.021Q23,11.426 23,12.885q0,1.122 -0.642,2.027a3.4,3.4 0,0 1,-1.691 1.269v0.204l0.758,0.759q0.088,0.087 0.124,0.189a0.7,0.7 0,0 1,0.036 0.219q0,0.116 -0.036,0.218a0.5,0.5 0,0 1,-0.124 0.19l-0.758,0.759 0.802,1.02a0.6,0.6 0,0 1,0.11 0.197,0.5 0.5,0 0,1 0.02,0.226 0.57,0.57 0,0 1,-0.204 0.38l-1.501,1.313a0.62,0.62 0,0 1,-0.409 0.145,0.5 0.5,0 0,1 -0.212,-0.044 0.6,0.6 0,0 1,-0.167 -0.116l-0.598,-0.599a0.58,0.58 0,0 1,-0.175 -0.423v-4.637a3.54,3.54 0,0 1,-1.875 -1.553A3.36,3.36 0,0 1,16 12.885q0,-1.458 1.02,-2.479 1.022,-1.02 2.48,-1.021m0,2.334q-0.481,0 -0.824,0.343a1.12,1.12 0,0 0,-0.343 0.823q0,0.48 0.343,0.824 0.342,0.343 0.824,0.343t0.824,-0.343q0.343,-0.342 0.343,-0.824 0,-0.48 -0.343,-0.823a1.12,1.12 0,0 0,-0.824 -0.343"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M11,13.125q1.532,0 3.025,0.298a5.25,5.25 0,0 0,1.054 2.737q0.542,0.714 1.254,1.217v3.441q0,0.092 0.007,0.182H3.125q-0.478,0 -0.802,-0.323A1.1,1.1 0,0 1,2 19.875V17.85q0,-0.957 0.492,-1.758A3.3,3.3 0,0 1,3.8 14.869a16.7,16.7 0,0 1,3.544 -1.309A15.5,15.5 0,0 1,11 13.125M11,3q1.857,0 3.178,1.322Q15.5,5.644 15.5,7.5t-1.322,3.178T11,12t-3.178,-1.322T6.5,7.5t1.322,-3.178T11,3"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4,20q-0.824,0 -1.412,-0.587A1.93,1.93 0,0 1,2 18L2,6q0,-0.824 0.587,-1.412A1.93,1.93 0,0 1,4 4h6l2,2h8q0.824,0 1.413,0.588Q22,7.175 22,8v10q0,0.824 -0.587,1.413A1.93,1.93 0,0 1,20 20zM4,18h16L20,8h-8.825l-2,-2L4,6z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M16.099,2.4a4.1,4.1 0,0 1,-1.057 3.074c-0.747,0.862 -1.878,1.358 -3.07,1.347 -0.075,-1.081 0.315,-2.146 1.085,-2.96 0.78,-0.825 1.866,-1.346 3.042,-1.461m3.767,6.54c-1.37,0.783 -2.213,2.163 -2.234,3.657 0.002,1.69 1.092,3.215 2.768,3.873a9.4,9.4 0,0 1,-1.44 2.723c-0.848,1.178 -1.737,2.329 -3.149,2.35 -0.67,0.015 -1.124,-0.165 -1.596,-0.351 -0.493,-0.195 -1.006,-0.398 -1.809,-0.398 -0.851,0 -1.388,0.21 -1.905,0.412 -0.447,0.174 -0.88,0.343 -1.49,0.367 -1.343,0.046 -2.37,-1.258 -3.25,-2.425 -1.756,-2.383 -3.124,-6.716 -1.29,-9.664 0.861,-1.437 2.471,-2.349 4.241,-2.402 0.763,-0.015 1.494,0.258 2.136,0.497 0.49,0.183 0.928,0.347 1.286,0.347 0.315,0 0.74,-0.157 1.237,-0.34 0.78,-0.288 1.737,-0.64 2.71,-0.545 1.514,0.044 2.917,0.748 3.785,1.9"
|
||||
android:pathData="M16.099,2.4a4.1,4.1 0,0 1,-1.057 3.073c-0.747,0.863 -1.878,1.36 -3.07,1.348 -0.075,-1.081 0.315,-2.146 1.085,-2.96 0.78,-0.825 1.866,-1.346 3.042,-1.461m3.767,6.54c-1.37,0.783 -2.214,2.163 -2.234,3.657 0.002,1.69 1.092,3.215 2.768,3.873a9.4,9.4 0,0 1,-1.44 2.723c-0.848,1.178 -1.737,2.329 -3.149,2.35 -0.671,0.015 -1.124,-0.165 -1.596,-0.351 -0.493,-0.195 -1.006,-0.398 -1.809,-0.398 -0.852,0 -1.388,0.21 -1.905,0.412 -0.447,0.174 -0.88,0.343 -1.49,0.367 -1.343,0.046 -2.37,-1.258 -3.25,-2.425 -1.756,-2.383 -3.124,-6.716 -1.29,-9.664 0.86,-1.437 2.471,-2.349 4.241,-2.402 0.763,-0.015 1.494,0.258 2.135,0.497 0.49,0.183 0.929,0.347 1.287,0.347 0.315,0 0.74,-0.157 1.237,-0.34 0.78,-0.288 1.737,-0.64 2.71,-0.545 1.514,0.044 2.917,0.748 3.785,1.9"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M9,4c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2m0,6c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2m0,6c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2m6,0c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2m0,-6c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2m0,-6c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M6.56,7.98C6.1,7.52 5.31,7.6 5,8.17c-0.28,0.51 -0.5,1.03 -0.67,1.58 -0.19,0.63 0.31,1.25 0.96,1.25h0.01c0.43,0 0.82,-0.28 0.94,-0.7q0.18,-0.6 0.48,-1.17c0.22,-0.37 0.15,-0.84 -0.16,-1.15M5.31,13h-0.02c-0.65,0 -1.15,0.62 -0.96,1.25 0.16,0.54 0.38,1.07 0.66,1.58 0.31,0.57 1.11,0.66 1.57,0.2 0.3,-0.31 0.38,-0.77 0.17,-1.15 -0.2,-0.37 -0.36,-0.76 -0.48,-1.16a0.97,0.97 0,0 0,-0.94 -0.72m2.85,6.02q0.765,0.42 1.59,0.66c0.62,0.18 1.24,-0.32 1.24,-0.96v-0.03c0,-0.43 -0.28,-0.82 -0.7,-0.94 -0.4,-0.12 -0.78,-0.28 -1.15,-0.48a0.97,0.97 0,0 0,-1.16 0.17l-0.03,0.03c-0.45,0.45 -0.36,1.24 0.21,1.55M13,4.07v-0.66c0,-0.89 -1.08,-1.34 -1.71,-0.71L9.17,4.83c-0.4,0.4 -0.4,1.04 0,1.43l2.13,2.08c0.63,0.62 1.7,0.17 1.7,-0.72V6.09c2.84,0.48 5,2.94 5,5.91 0,2.73 -1.82,5.02 -4.32,5.75a0.97,0.97 0,0 0,-0.68 0.94v0.02c0,0.65 0.61,1.14 1.23,0.96A7.976,7.976 0,0 0,20 12c0,-4.08 -3.05,-7.44 -7,-7.93"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24H0z"/>
|
||||
<path
|
||||
android:pathData="M14.83,4.83 L12.7,2.7c-0.62,-0.62 -1.7,-0.18 -1.7,0.71v0.66C7.06,4.56 4,7.92 4,12c0,3.64 2.43,6.71 5.77,7.68 0.62,0.18 1.23,-0.32 1.23,-0.96v-0.03a0.97,0.97 0,0 0,-0.68 -0.94A5.98,5.98 0,0 1,6 12c0,-2.97 2.16,-5.43 5,-5.91v1.53c0,0.89 1.07,1.33 1.7,0.71l2.13,-2.08a0.99,0.99 0,0 0,0 -1.42m4.84,4.93q-0.24,-0.825 -0.66,-1.59c-0.31,-0.57 -1.1,-0.66 -1.56,-0.2l-0.01,0.01c-0.31,0.31 -0.38,0.78 -0.17,1.16 0.2,0.37 0.36,0.76 0.48,1.16 0.12,0.42 0.51,0.7 0.94,0.7h0.02c0.65,0 1.15,-0.62 0.96,-1.24M13,18.68v0.02c0,0.65 0.62,1.14 1.24,0.96q0.825,-0.24 1.59,-0.66c0.57,-0.31 0.66,-1.1 0.2,-1.56l-0.02,-0.02a0.97,0.97 0,0 0,-1.16 -0.17c-0.37,0.21 -0.76,0.37 -1.16,0.49 -0.41,0.12 -0.69,0.51 -0.69,0.94m4.44,-2.65c0.46,0.46 1.25,0.37 1.56,-0.2 0.28,-0.51 0.5,-1.04 0.67,-1.59 0.18,-0.62 -0.31,-1.24 -0.96,-1.24h-0.02c-0.44,0 -0.82,0.28 -0.94,0.7q-0.18,0.6 -0.48,1.17c-0.21,0.38 -0.13,0.86 0.17,1.16"
|
||||
android:fillColor="#FF000000"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M16,7a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,15 8q0,0.424 0.287,0.713Q15.576,9 16,9t0.712,-0.287A0.97,0.97 0,0 0,17 8a0.97,0.97 0,0 0,-0.288 -0.713A0.97,0.97 0,0 0,16 7m0,4a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,15 12q0,0.424 0.287,0.713 0.288,0.287 0.713,0.287 0.424,0 0.712,-0.287A0.97,0.97 0,0 0,17 12a0.97,0.97 0,0 0,-0.288 -0.713A0.97,0.97 0,0 0,16 11m0,4a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,15 16q0,0.424 0.287,0.712 0.288,0.288 0.713,0.288 0.424,0 0.712,-0.288A0.97,0.97 0,0 0,17 16a0.97,0.97 0,0 0,-0.288 -0.713A0.97,0.97 0,0 0,16 15m-4,-8L8,7a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,7 8q0,0.424 0.287,0.713Q7.576,9 8,9h4q0.424,0 0.713,-0.287A0.97,0.97 0,0 0,13 8a0.97,0.97 0,0 0,-0.287 -0.713A0.97,0.97 0,0 0,12 7m0,4L8,11a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,7 12q0,0.424 0.287,0.713Q7.576,13 8,13h4q0.424,0 0.713,-0.287A0.97,0.97 0,0 0,13 12a0.97,0.97 0,0 0,-0.287 -0.713A0.97,0.97 0,0 0,12 11m0,4L8,15a0.97,0.97 0,0 0,-0.713 0.287A0.97,0.97 0,0 0,7 16q0,0.424 0.287,0.712Q7.576,17 8,17h4q0.424,0 0.713,-0.288A0.97,0.97 0,0 0,13 16a0.97,0.97 0,0 0,-0.287 -0.713A0.97,0.97 0,0 0,12 15m7,-12q0.824,0 1.413,0.587Q21,4.176 21,5v14q0,0.824 -0.587,1.413A1.93,1.93 0,0 1,19 21L5,21q-0.824,0 -1.412,-0.587A1.93,1.93 0,0 1,3 19L3,5q0,-0.824 0.587,-1.412A1.93,1.93 0,0 1,5 3zM19,5L5,5v14h14z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M16,18v2L8,20v-2zM18,16L18,8a2,2 0,0 0,-2 -2L8,6a2,2 0,0 0,-2 2v8a2,2 0,0 0,2 2v2a4,4 0,0 1,-4 -4L4,8a4,4 0,0 1,4 -4h8a4,4 0,0 1,4 4v8a4,4 0,0 1,-4 4v-2a2,2 0,0 0,2 -2"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4,8a4,4 0,0 1,4 -4h8a4,4 0,0 1,4 4v8a4,4 0,0 1,-4 4H8a4,4 0,0 1,-4 -4z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,22c5.52,0 10,-4.48 10,-10S17.52,2 12,2 2,6.48 2,12s4.48,10 10,10m1,-17.93c3.94,0.49 7,3.85 7,7.93s-3.05,7.44 -7,7.93z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M13,2a2,2 0,0 1,2 2v4h6a2,2 0,0 1,2 2v12.586c0,0.89 -1.077,1.337 -1.707,0.707L19,21h-8a2,2 0,0 1,-2 -2v-4L5,15l-2.293,2.293c-0.63,0.63 -1.707,0.184 -1.707,-0.707L1,4a2,2 0,0 1,2 -2zM15.5,12.125L12,12.125v1.25h4.37c-0.031,0.73 -0.325,1.457 -0.871,2.151a4.4,4.4 0,0 1,-0.613 -1.026h-1.33c0.202,0.69 0.57,1.335 1.067,1.932 -0.524,0.448 -1.162,0.873 -1.912,1.263l0.578,1.11a11.3,11.3 0,0 0,2.21 -1.483c0.633,0.553 1.382,1.05 2.212,1.483l0.578,-1.11c-0.75,-0.39 -1.388,-0.815 -1.912,-1.263 0.758,-0.912 1.213,-1.939 1.245,-3.057L20,13.375v-1.25h-3.25L16.75,10.25L15.5,10.25zM3,14.172l0.586,-0.586A2,2 0,0 1,5 13h4v-2.47L6.96,10.53L6.563,12L5,12l2.031,-7L8.97,5l0.96,3.312A2,2 0,0 1,11 8h2L13,4L3,4zM7.306,9.245h1.386l-0.67,-2.481h-0.047z"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M9.01,5v1H11c0.333,0 1,0 1.5,0.5S13,7.667 13,8v7.01c0,0.54 0.45,0.99 0.99,0.99H15v-1a2,2 0,0 1,2 -2h3a2,2 0,0 1,2 2v4a2,2 0,0 1,-2 2h-3a2,2 0,0 1,-2 -2v-1h-1.01C12.34,18 11,16.66 11,15.01V9c0,-1 0,-1 -1,-1H9v1a2,2 0,0 1,-2 2H4a2,2 0,0 1,-2 -2V5c0,-1.1 0.9,-2 2,-2h3.01a2,2 0,0 1,2 2"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M16,4a2,2 0,0 1,2 2v4.286l3.35,-2.871a1,1 0,0 1,1.65 0.759v7.652a1,1 0,0 1,-1.65 0.759L18,13.714V18a2,2 0,0 1,-2 2H6a4,4 0,0 1,-4 -4V8a4,4 0,0 1,4 -4zM9.55,9l-0.103,0.005a1,1 0,0 0,0 1.99L9.55,11h0.571l-2.828,2.828a1,1 0,0 0,1.414 1.414L11.55,12.4v0.6l0.005,0.102a1,1 0,0 0,1.99 0L13.55,13v-3l-0.005,-0.103A1,1 0,0 0,12.55 9z"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
@ -4,12 +4,8 @@
|
|||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24H0z"/>
|
||||
<path
|
||||
android:pathData="M8.929,15.1a13.6,13.6 0,0 0,4.654 3.066q2.62,1.036 5.492,0.923h0.008l0.003,-0.004 0.003,-0.002 -0.034,-3.124 -3.52,-0.483 -1.791,1.792 -0.645,-0.322a13.5,13.5 0,0 1,-3.496 -2.52,13.4 13.4,0 0,1 -2.52,-3.496l-0.322,-0.644 1.792,-1.792 -0.483,-3.519 -3.123,-0.034 -0.003,0.002 -0.003,0.004v0.002a13.65,13.65 0,0 0,0.932 5.492A13.4,13.4 0,0 0,8.93 15.1m3.92,4.926a15.6,15.6 0,0 1,-5.334 -3.511,15.4 15.4,0 0,1 -3.505,-5.346 15.6,15.6 0,0 1,-1.069 -6.274,1.93 1.93,0 0,1 0.589,-1.366c0.366,-0.366 0.84,-0.589 1.386,-0.589h0.01l3.163,0.035a1.96,1.96 0,0 1,1.958 1.694v0.005l0.487,3.545v0.003c0.043,0.297 0.025,0.605 -0.076,0.907a2,2 0,0 1,-0.485 0.773l-0.762,0.762a11.4,11.4 0,0 0,3.206 3.54q0.457,0.33 0.948,0.614l0.762,-0.761a2,2 0,0 1,0.774 -0.486c0.302,-0.1 0.61,-0.118 0.907,-0.076l3.553,0.487a1.96,1.96 0,0 1,1.694 1.958l0.034,3.174c0,0.546 -0.223,1.02 -0.588,1.386 -0.361,0.36 -0.827,0.582 -1.363,0.588a15.3,15.3 0,0 1,-6.29 -1.062"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M8.929,15.1a13.6,13.6 0,0 0,4.654 3.066q2.62,1.036 5.492,0.923h0.008l0.003,-0.004 0.003,-0.002 -0.034,-3.124 -3.52,-0.483 -1.791,1.792 -0.645,-0.322a13.5,13.5 0,0 1,-3.496 -2.52,13.4 13.4,0 0,1 -2.52,-3.496l-0.322,-0.645 1.792,-1.791 -0.483,-3.52 -3.123,-0.033 -0.003,0.002 -0.003,0.004v0.002a13.65,13.65 0,0 0,0.932 5.492A13.4,13.4 0,0 0,8.93 15.1m3.92,4.926a15.6,15.6 0,0 1,-5.334 -3.511,15.4 15.4,0 0,1 -3.505,-5.346 15.6,15.6 0,0 1,-1.069 -6.274,1.93 1.93,0 0,1 0.589,-1.366c0.366,-0.366 0.84,-0.589 1.386,-0.589h0.01l3.163,0.035a1.96,1.96 0,0 1,1.958 1.694v0.005l0.487,3.545v0.003c0.043,0.297 0.025,0.605 -0.076,0.907a2,2 0,0 1,-0.485 0.773l-0.762,0.762a11.3,11.3 0,0 0,1.806 2.348,11.4 11.4,0 0,0 2.348,1.806l0.762,-0.762a2,2 0,0 1,0.774 -0.485c0.302,-0.1 0.61,-0.118 0.907,-0.076l3.553,0.487a1.96,1.96 0,0 1,1.694 1.958l0.034,3.174c0,0.546 -0.223,1.02 -0.588,1.386 -0.36,0.36 -0.827,0.582 -1.363,0.588a15.3,15.3 0,0 1,-6.29 -1.062"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M7.623,3.04a1.07,1.07 0,0 1,1.086 0.929l0.542,3.954q0.039,0.27 -0.038,0.504a1.1,1.1 0,0 1,-0.272 0.427l-1.64,1.64Q7.806,11.5 8.456,12.4c0.433,0.601 1.444,1.697 1.444,1.697 0.013,0.012 1.098,1.014 1.696,1.444q0.9,0.65 1.909,1.153l1.64,-1.64q0.194,-0.194 0.426,-0.27a1.1,1.1 0,0 1,0.504 -0.04l3.953,0.543q0.407,0.058 0.67,0.358 0.26,0.301 0.26,0.728l0.04,3.527q0,0.427 -0.33,0.756 -0.33,0.33 -0.756,0.33a16,16 0,0 1,-6.57 -1.105,16.2 16.2,0 0,1 -5.563,-3.663 16.1,16.1 0,0 1,-3.653 -5.573,16.3 16.3,0 0,1 -1.116,-6.56q0,-0.426 0.329,-0.756Q3.67,3 4.095,3zM20.25,3q0.405,0 0.707,0.3 0.3,0.301 0.3,0.708t-0.3,0.707l-1.414,1.414 1.414,1.414q0.3,0.3 0.3,0.707t-0.3,0.707 -0.707,0.3 -0.707,-0.3l-1.414,-1.414 -1.414,1.414q-0.3,0.3 -0.707,0.3t-0.707,-0.3T15,8.25q0,-0.406 0.3,-0.707l1.415,-1.414L15.3,4.715q-0.3,-0.3 -0.301,-0.707 0,-0.407 0.3,-0.707t0.71,-0.301q0.405,0 0.707,0.3l1.414,1.415L19.543,3.3q0.3,-0.3 0.707,-0.301"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M7.623,3.04a1.07,1.07 0,0 1,1.086 0.929l0.542,3.954q0.039,0.27 -0.038,0.504a1.1,1.1 0,0 1,-0.272 0.427l-1.64,1.64Q7.806,11.5 8.456,12.4c0.433,0.601 1.444,1.697 1.444,1.697 0.013,0.012 1.098,1.014 1.696,1.444q0.9,0.65 1.909,1.153l1.64,-1.64q0.194,-0.194 0.426,-0.27a1.1,1.1 0,0 1,0.504 -0.04l3.953,0.543q0.407,0.058 0.67,0.358 0.26,0.301 0.26,0.728l0.04,3.527q0,0.427 -0.33,0.756 -0.33,0.33 -0.756,0.33a16,16 0,0 1,-6.57 -1.105,16.2 16.2,0 0,1 -5.563,-3.663 16.1,16.1 0,0 1,-3.653 -5.573,16.3 16.3,0 0,1 -1.116,-6.56q0,-0.426 0.329,-0.756Q3.67,3 4.095,3z"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M16,5q0.425,0 0.713,0.287Q17,5.575 17,6a0.97,0.97 0,0 1,-0.287 0.713A0.97,0.97 0,0 1,16 7h-0.5l2.2,2.15 2.4,-2.4a0.95,0.95 0,0 1,0.7 -0.275,0.95 0.95,0 0,1 0.7,0.275q0.3,0.3 0.3,0.7a0.92,0.92 0,0 1,-0.275 0.675l-3.125,3.15a0.8,0.8 0,0 1,-0.312 0.225,1.04 1.04,0 0,1 -0.776,0 0.9,0.9 0,0 1,-0.312 -0.2l-3,-3V9a0.97,0.97 0,0 1,-0.287 0.713A0.97,0.97 0,0 1,13 10a0.97,0.97 0,0 1,-0.713 -0.287A0.97,0.97 0,0 1,12 9V6q0,-0.425 0.287,-0.713A0.97,0.97 0,0 1,13 5z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M7.623,3.04a1.07,1.07 0,0 1,1.086 0.929l0.542,3.954q0.039,0.27 -0.038,0.504a1.1,1.1 0,0 1,-0.272 0.427l-1.64,1.64Q7.806,11.5 8.456,12.4c0.433,0.601 1.444,1.697 1.444,1.697 0.013,0.012 1.098,1.014 1.696,1.444q0.9,0.65 1.909,1.153l1.64,-1.64q0.194,-0.194 0.426,-0.27a1.1,1.1 0,0 1,0.504 -0.04l3.953,0.543q0.407,0.058 0.67,0.358 0.26,0.301 0.26,0.728l0.04,3.527q0,0.427 -0.33,0.756 -0.33,0.33 -0.756,0.33a16,16 0,0 1,-6.57 -1.105,16.2 16.2,0 0,1 -5.563,-3.663 16.1,16.1 0,0 1,-3.653 -5.573,16.3 16.3,0 0,1 -1.116,-6.56q0,-0.426 0.329,-0.756Q3.67,3 4.095,3z"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M19.964,3a1,1 0,0 1,0.995 0.897l0.005,0.103v3l-0.005,0.103a1,1 0,0 1,-1.99 0L18.964,7v-0.605l-4.05,4.02A1,1 0,0 1,13.5 9l4.03,-4h-0.566l-0.103,-0.005a1,1 0,0 1,0 -1.99L16.964,3z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24H0z"/>
|
||||
<path
|
||||
android:pathData="M10.5,6.5q0.425,0 0.713,0.287 0.288,0.288 0.287,0.713v2h2q0.425,0 0.713,0.287 0.288,0.288 0.287,0.713a0.97,0.97 0,0 1,-0.287 0.713,0.97 0.97,0 0,1 -0.713,0.287h-2v2a0.97,0.97 0,0 1,-0.287 0.713,0.97 0.97,0 0,1 -0.713,0.287 0.97,0.97 0,0 1,-0.713 -0.287,0.97 0.97,0 0,1 -0.287,-0.713v-2h-2a0.97,0.97 0,0 1,-0.713 -0.287,0.97 0.97,0 0,1 -0.287,-0.713q0,-0.425 0.287,-0.713A0.97,0.97 0,0 1,7.5 9.5h2v-2q0,-0.425 0.287,-0.713A0.97,0.97 0,0 1,10.5 6.5"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M10.5,3a7.5,7.5 0,0 1,5.963 12.049l3.244,3.244a1,1 0,1 1,-1.414 1.414l-3.244,-3.244A7.5,7.5 0,1 1,10.5 3m0,2a5.5,5.5 0,1 0,0 11,5.5 5.5,0 0,0 0,-11"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M15.05,16.463a7.5,7.5 0,1 1,1.414 -1.414l3.243,3.244a1,1 0,0 1,-1.414 1.414zM16,10.5a5.5,5.5 0,1 0,-11 0,5.5 5.5,0 0,0 11,0"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M7.875,11.375h1.75v1.75q0,0.372 0.252,0.623A0.85,0.85 0,0 0,10.5 14a0.85,0.85 0,0 0,0.623 -0.252,0.85 0.85,0 0,0 0.252,-0.623v-1.75h1.75a0.85,0.85 0,0 0,0.623 -0.252A0.85,0.85 0,0 0,14 10.5a0.85,0.85 0,0 0,-0.252 -0.623,0.85 0.85,0 0,0 -0.623,-0.252h-1.75v-1.75a0.85,0.85 0,0 0,-0.252 -0.623A0.85,0.85 0,0 0,10.5 7a0.85,0.85 0,0 0,-0.623 0.252,0.85 0.85,0 0,0 -0.252,0.623v1.75h-1.75a0.85,0.85 0,0 0,-0.623 0.252A0.85,0.85 0,0 0,7 10.5q0,0.372 0.252,0.623a0.85,0.85 0,0 0,0.623 0.252"
|
||||
android:fillColor="#FF000000"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M13.5,9.5q0.425,0 0.713,0.287 0.288,0.288 0.287,0.713a0.97,0.97 0,0 1,-0.287 0.713,0.97 0.97,0 0,1 -0.713,0.287h-6a0.97,0.97 0,0 1,-0.713 -0.287,0.97 0.97,0 0,1 -0.287,-0.713q0,-0.425 0.287,-0.713A0.97,0.97 0,0 1,7.5 9.5z"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M10.5,3a7.5,7.5 0,0 1,5.963 12.049l3.244,3.244a1,1 0,1 1,-1.414 1.414l-3.244,-3.244A7.5,7.5 0,1 1,10.5 3m0,2a5.5,5.5 0,1 0,0 11,5.5 5.5,0 0,0 0,-11"
|
||||
android:fillColor="#FF000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
@ -6,20 +6,7 @@
|
|||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("java-library")
|
||||
id("com.android.lint")
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = Versions.javaVersion
|
||||
targetCompatibility = Versions.javaVersion
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion = Versions.javaLanguageVersion
|
||||
}
|
||||
id("io.element.jvm-library")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ package io.element.android.libraries.dateformatter.api
|
|||
import java.util.Locale
|
||||
import kotlin.time.Duration
|
||||
|
||||
/**
|
||||
* Formats a duration in a localized, human-readable way.
|
||||
* Uses the largest appropriate unit (hours, minutes, or seconds).
|
||||
*
|
||||
* Examples (in English):
|
||||
* - 2 hours 30 minutes → "3 hours" (rounded)
|
||||
* - 45 minutes → "45 minutes"
|
||||
* - 30 seconds → "30 seconds"
|
||||
*/
|
||||
interface DurationFormatter {
|
||||
fun format(duration: Duration): String
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert milliseconds to human readable duration.
|
||||
* Hours in 1 digit or more.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import android.icu.text.MeasureFormat
|
||||
import android.icu.text.MeasureFormat.FormatWidth
|
||||
import android.icu.util.Measure
|
||||
import android.icu.util.MeasureUnit
|
||||
import android.text.format.DateUtils
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import dev.zacsweers.metro.binding
|
||||
import io.element.android.libraries.dateformatter.api.DurationFormatter
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Formats durations in a localized, human-readable way using Android's MeasureFormat.
|
||||
*
|
||||
* Uses WIDE format for readability (e.g., "5 hours", "3 minutes", "10 seconds").
|
||||
* Rounds to the nearest unit for cleaner display.
|
||||
*/
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class, binding = binding<DurationFormatter>())
|
||||
class DefaultDurationFormatter(
|
||||
localeChangeObserver: LocaleChangeObserver,
|
||||
locale: Locale,
|
||||
) : DurationFormatter, LocaleChangeListener {
|
||||
init {
|
||||
localeChangeObserver.addListener(this)
|
||||
}
|
||||
|
||||
// Cache formatter, recreate only on locale change
|
||||
private var formatter: MeasureFormat = MeasureFormat.getInstance(locale, FormatWidth.WIDE)
|
||||
|
||||
override fun onLocaleChange() {
|
||||
formatter = MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE)
|
||||
}
|
||||
|
||||
override fun format(duration: Duration): String {
|
||||
val millis = duration.inWholeMilliseconds
|
||||
|
||||
return when {
|
||||
duration >= 1.hours -> {
|
||||
// Round to nearest hour (add 30 minutes before dividing)
|
||||
val hours = ((millis + 30 * DateUtils.MINUTE_IN_MILLIS) / DateUtils.HOUR_IN_MILLIS).toInt()
|
||||
formatter.format(Measure(hours, MeasureUnit.HOUR))
|
||||
}
|
||||
duration >= 1.minutes -> {
|
||||
// Round to nearest minute (add 30 seconds before dividing)
|
||||
val minutes = ((millis + 30 * DateUtils.SECOND_IN_MILLIS) / DateUtils.MINUTE_IN_MILLIS).toInt()
|
||||
formatter.format(Measure(minutes, MeasureUnit.MINUTE))
|
||||
}
|
||||
else -> {
|
||||
// Round to nearest second (add 500ms before dividing)
|
||||
val seconds = ((millis + 500) / DateUtils.SECOND_IN_MILLIS).toInt()
|
||||
formatter.format(Measure(seconds, MeasureUnit.SECOND))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import android.os.Build
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(qualifiers = "en", sdk = [Build.VERSION_CODES.TIRAMISU])
|
||||
class DefaultDurationFormatterTest {
|
||||
private fun createDurationFormatter(): DefaultDurationFormatter {
|
||||
return DefaultDurationFormatter(
|
||||
localeChangeObserver = {},
|
||||
locale = Locale.US,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test zero duration`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(0.seconds)).isEqualTo("0 seconds")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 1 second`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(1.seconds)).isEqualTo("1 second")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 30 seconds`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(30.seconds)).isEqualTo("30 seconds")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 59 seconds`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(59.seconds)).isEqualTo("59 seconds")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 1 minute`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(1.minutes)).isEqualTo("1 minute")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 1 minute 29 seconds rounds to 1 minute`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(1.minutes + 29.seconds)).isEqualTo("1 minute")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 1 minute 30 seconds rounds to 2 minutes`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(1.minutes + 30.seconds)).isEqualTo("2 minutes")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 45 minutes`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(45.minutes)).isEqualTo("45 minutes")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 59 minutes`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(59.minutes)).isEqualTo("59 minutes")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 1 hour`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(1.hours)).isEqualTo("1 hour")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 1 hour 29 minutes rounds to 1 hour`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(1.hours + 29.minutes)).isEqualTo("1 hour")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 1 hour 30 minutes rounds to 2 hours`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(1.hours + 30.minutes)).isEqualTo("2 hours")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 2 hours 30 minutes rounds to 3 hours`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(2.hours + 30.minutes)).isEqualTo("3 hours")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 5 hours`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(5.hours)).isEqualTo("5 hours")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test 24 hours`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(24.hours)).isEqualTo("24 hours")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test rounding at seconds threshold - 499ms rounds to 0 seconds`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(499.milliseconds)).isEqualTo("0 seconds")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test rounding at seconds threshold - 500ms rounds to 1 second`() {
|
||||
val formatter = createDurationFormatter()
|
||||
assertThat(formatter.format(500.milliseconds)).isEqualTo("1 second")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.test
|
||||
|
||||
import io.element.android.libraries.dateformatter.api.DurationFormatter
|
||||
import kotlin.time.Duration
|
||||
|
||||
class FakeDurationFormatter(
|
||||
private val formatLambda: (Duration) -> String = { it.toString() },
|
||||
) : DurationFormatter {
|
||||
override fun format(duration: Duration): String {
|
||||
return formatLambda(duration)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,14 +8,19 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.atomic.atoms
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.components.Badge
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
||||
/**
|
||||
* https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=1960-491
|
||||
*/
|
||||
object MatrixBadgeAtom {
|
||||
data class MatrixBadgeData(
|
||||
val text: String,
|
||||
|
|
@ -40,6 +45,12 @@ object MatrixBadgeAtom {
|
|||
Type.Negative -> ElementTheme.colors.bgCriticalSubtle
|
||||
Type.Info -> ElementTheme.colors.bgBadgeInfo
|
||||
}
|
||||
val borderStroke = when (data.type) {
|
||||
Type.Positive -> null
|
||||
Type.Neutral -> BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary)
|
||||
Type.Negative -> null
|
||||
Type.Info -> null
|
||||
}
|
||||
val textColor = when (data.type) {
|
||||
Type.Positive -> ElementTheme.colors.textBadgeAccent
|
||||
Type.Neutral -> ElementTheme.colors.textPrimary
|
||||
|
|
@ -58,6 +69,7 @@ object MatrixBadgeAtom {
|
|||
backgroundColor = backgroundColor,
|
||||
iconColor = iconColor,
|
||||
textColor = textColor,
|
||||
borderStroke = borderStroke,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.colors.gradientCriticalColors
|
||||
import io.element.android.libraries.designsystem.colors.gradientInfoColors
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
|
|
@ -38,13 +40,16 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2392-6721
|
||||
*/
|
||||
@Composable
|
||||
fun ComposerAlertMolecule(
|
||||
avatar: AvatarData?,
|
||||
content: AnnotatedString,
|
||||
onSubmitClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
level: ComposerAlertLevel = ComposerAlertLevel.Default,
|
||||
level: ComposerAlertLevel = ComposerAlertLevel.Info,
|
||||
showIcon: Boolean = false,
|
||||
submitText: String = stringResource(CommonStrings.action_ok),
|
||||
) {
|
||||
|
|
@ -52,20 +57,12 @@ fun ComposerAlertMolecule(
|
|||
modifier.fillMaxWidth()
|
||||
) {
|
||||
val lineColor = when (level) {
|
||||
ComposerAlertLevel.Default -> ElementTheme.colors.borderInfoSubtle
|
||||
ComposerAlertLevel.Info -> ElementTheme.colors.borderInfoSubtle
|
||||
ComposerAlertLevel.Critical -> ElementTheme.colors.borderCriticalSubtle
|
||||
}
|
||||
|
||||
val startColor = when (level) {
|
||||
ComposerAlertLevel.Default -> ElementTheme.colors.bgInfoSubtle
|
||||
ComposerAlertLevel.Info -> ElementTheme.colors.bgInfoSubtle
|
||||
ComposerAlertLevel.Critical -> ElementTheme.colors.bgCriticalSubtle
|
||||
}
|
||||
|
||||
val textColor = when (level) {
|
||||
ComposerAlertLevel.Default -> ElementTheme.colors.textPrimary
|
||||
ComposerAlertLevel.Info -> ElementTheme.colors.textInfoPrimary
|
||||
ComposerAlertLevel.Info -> ElementTheme.colors.textPrimary
|
||||
ComposerAlertLevel.Critical -> ElementTheme.colors.textCriticalPrimary
|
||||
}
|
||||
|
||||
|
|
@ -75,12 +72,13 @@ fun ComposerAlertMolecule(
|
|||
.height(1.dp)
|
||||
.background(lineColor)
|
||||
)
|
||||
val brush = Brush.verticalGradient(
|
||||
listOf(startColor, ElementTheme.colors.bgCanvasDefault),
|
||||
)
|
||||
val gradientColors = when (level) {
|
||||
ComposerAlertLevel.Info -> gradientInfoColors()
|
||||
ComposerAlertLevel.Critical -> gradientCriticalColors()
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(brush)
|
||||
.background(Brush.verticalGradient(gradientColors))
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -96,12 +94,10 @@ fun ComposerAlertMolecule(
|
|||
)
|
||||
} else if (showIcon) {
|
||||
val icon = when (level) {
|
||||
ComposerAlertLevel.Default -> CompoundIcons.Info()
|
||||
ComposerAlertLevel.Info -> CompoundIcons.Info()
|
||||
ComposerAlertLevel.Critical -> CompoundIcons.Error()
|
||||
}
|
||||
val iconTint = when (level) {
|
||||
ComposerAlertLevel.Default -> ElementTheme.colors.iconPrimary
|
||||
ComposerAlertLevel.Info -> ElementTheme.colors.iconInfoPrimary
|
||||
ComposerAlertLevel.Critical -> ElementTheme.colors.iconCriticalPrimary
|
||||
}
|
||||
|
|
@ -131,7 +127,6 @@ fun ComposerAlertMolecule(
|
|||
}
|
||||
|
||||
enum class ComposerAlertLevel {
|
||||
Default,
|
||||
Info,
|
||||
Critical
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ internal data class ComposerAlertMoleculeParams(
|
|||
|
||||
internal class ComposerAlertMoleculeParamsProvider : PreviewParameterProvider<ComposerAlertMoleculeParams> {
|
||||
private val allLevels = sequenceOf(
|
||||
ComposerAlertLevel.Default,
|
||||
ComposerAlertLevel.Info,
|
||||
ComposerAlertLevel.Critical
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,8 +38,11 @@ fun gradientSubtleColors(): List<Color> = listOf(
|
|||
fun gradientInfoColors(): List<Color> = listOf(
|
||||
ElementTheme.colors.gradientInfoStop1,
|
||||
ElementTheme.colors.gradientInfoStop2,
|
||||
ElementTheme.colors.gradientInfoStop3,
|
||||
ElementTheme.colors.gradientInfoStop4,
|
||||
ElementTheme.colors.gradientInfoStop5,
|
||||
ElementTheme.colors.gradientInfoStop6,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun gradientCriticalColors(): List<Color> = listOf(
|
||||
ElementTheme.colors.gradientCriticalStop1,
|
||||
ElementTheme.colors.gradientCriticalStop2,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,419 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.withSave
|
||||
import coil3.Image
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.allowHardware
|
||||
import coil3.toBitmap
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
||||
private val PIN_WIDTH = 42.dp
|
||||
private val PIN_HEIGHT = PIN_WIDTH * 1.2f
|
||||
private val AVATAR_SIZE = PIN_WIDTH - 10.dp
|
||||
private val CONTENT_OFFSET = 5.dp
|
||||
private val DOT_RADIUS = 6.dp
|
||||
private val STROKE_WIDTH = 1.dp
|
||||
|
||||
/**
|
||||
* Variants of location pin markers.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface PinVariant {
|
||||
data class UserLocation(
|
||||
val avatarData: AvatarData,
|
||||
val isLive: Boolean,
|
||||
) : PinVariant
|
||||
|
||||
data object PinnedLocation : PinVariant
|
||||
data object StaleLocation : PinVariant
|
||||
}
|
||||
|
||||
/**
|
||||
* A location pin composable that supports multiple variants.
|
||||
*
|
||||
* Based on Figma design: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=4665-2890&m=dev
|
||||
*/
|
||||
@Composable
|
||||
fun LocationPin(
|
||||
variant: PinVariant,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val image = rememberLocationPinBitmap(variant)
|
||||
Canvas(modifier = modifier.size(PIN_WIDTH, PIN_HEIGHT)) {
|
||||
if (image != null) {
|
||||
drawImage(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a location pin to an [ImageBitmap] using Canvas operations.
|
||||
* @param variant The pin variant to render
|
||||
* @return The rendered [ImageBitmap], or null if still loading
|
||||
*/
|
||||
@Composable
|
||||
fun rememberLocationPinBitmap(variant: PinVariant): ImageBitmap? {
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
val imageLoader = SingletonImageLoader.get(context)
|
||||
val colors = pinColors(variant)
|
||||
val cacheKey = rememberCacheKey(variant)
|
||||
return produceState<ImageBitmap?>(initialValue = null, cacheKey) {
|
||||
val memoryCacheKey = MemoryCache.Key(cacheKey)
|
||||
val cached = imageLoader.memoryCache?.get(memoryCacheKey)
|
||||
if (cached != null) {
|
||||
value = cached.image.toBitmap().asImageBitmap()
|
||||
} else {
|
||||
val dimensions = PinDimensions(density)
|
||||
val bitmap = LocationPinRenderer.renderPin(variant, colors, dimensions, context, imageLoader)
|
||||
imageLoader.memoryCache?.set(memoryCacheKey, MemoryCache.Value(bitmap.asImage()))
|
||||
value = bitmap.asImageBitmap()
|
||||
}
|
||||
}.value
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun pinColors(variant: PinVariant): PinColors {
|
||||
return when (variant) {
|
||||
is PinVariant.UserLocation -> {
|
||||
val avatarColors = AvatarColorsProvider.provide(variant.avatarData.id)
|
||||
if (variant.isLive) {
|
||||
PinColors(
|
||||
fill = ElementTheme.colors.iconAccentPrimary,
|
||||
stroke = Color.Transparent,
|
||||
dot = Color.Transparent,
|
||||
avatarStroke = ElementTheme.colors.bgCanvasDefault,
|
||||
avatarBackground = avatarColors.background,
|
||||
avatarForeground = avatarColors.foreground,
|
||||
)
|
||||
} else {
|
||||
PinColors(
|
||||
fill = ElementTheme.colors.bgCanvasDefault,
|
||||
stroke = ElementTheme.colors.iconQuaternaryAlpha,
|
||||
dot = Color.Transparent,
|
||||
avatarStroke = ElementTheme.colors.iconQuaternaryAlpha,
|
||||
avatarBackground = avatarColors.background,
|
||||
avatarForeground = avatarColors.foreground,
|
||||
)
|
||||
}
|
||||
}
|
||||
PinVariant.PinnedLocation -> PinColors(
|
||||
fill = ElementTheme.colors.bgCanvasDefault,
|
||||
stroke = ElementTheme.colors.iconSecondaryAlpha,
|
||||
avatarStroke = Color.Transparent,
|
||||
avatarBackground = Color.Transparent,
|
||||
avatarForeground = Color.Transparent,
|
||||
dot = ElementTheme.colors.iconPrimary,
|
||||
)
|
||||
PinVariant.StaleLocation -> PinColors(
|
||||
fill = ElementTheme.colors.bgSubtleSecondary,
|
||||
stroke = ElementTheme.colors.iconDisabled,
|
||||
avatarStroke = Color.Transparent,
|
||||
avatarBackground = Color.Transparent,
|
||||
avatarForeground = Color.Transparent,
|
||||
dot = ElementTheme.colors.iconDisabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color configuration for rendering a location pin.
|
||||
*/
|
||||
private data class PinColors(
|
||||
val fill: Color,
|
||||
val stroke: Color,
|
||||
val dot: Color,
|
||||
val avatarStroke: Color,
|
||||
val avatarBackground: Color,
|
||||
val avatarForeground: Color,
|
||||
)
|
||||
|
||||
/**
|
||||
* Pre-calculated pixel dimensions for rendering a location pin.
|
||||
*/
|
||||
private class PinDimensions(density: Density) {
|
||||
val pinWidth = with(density) { PIN_WIDTH.toPx() }
|
||||
val pinHeight = with(density) { PIN_HEIGHT.toPx() }
|
||||
val avatarSize: Float = with(density) { AVATAR_SIZE.toPx() }
|
||||
val avatarOffset: Float = with(density) { CONTENT_OFFSET.toPx() }
|
||||
val dotRadius: Float = with(density) { DOT_RADIUS.toPx() }
|
||||
val strokeWidth: Float = with(density) { STROKE_WIDTH.toPx() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders location pins to bitmaps using Canvas operations.
|
||||
* Uses Coil for avatar loading.
|
||||
* Paint objects are shared across all renders.
|
||||
*/
|
||||
private object LocationPinRenderer {
|
||||
// Shared Paint objects to avoid allocations
|
||||
private val fillPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val strokePaint = Paint().apply {
|
||||
style = Paint.Style.STROKE
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val textPaint = Paint().apply {
|
||||
textAlign = Paint.Align.CENTER
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a pin variant to bitmap. Suspending for async avatar loading.
|
||||
*/
|
||||
suspend fun renderPin(
|
||||
variant: PinVariant,
|
||||
colors: PinColors,
|
||||
dimensions: PinDimensions,
|
||||
context: Context,
|
||||
imageLoader: ImageLoader,
|
||||
): Bitmap {
|
||||
val bitmap = createBitmap(dimensions.pinWidth.toInt(), dimensions.pinHeight.toInt())
|
||||
val canvas = Canvas(bitmap)
|
||||
canvas.drawPinShape(colors.fill, colors.stroke, dimensions)
|
||||
when (variant) {
|
||||
is PinVariant.UserLocation -> {
|
||||
val avatarImage = loadAvatarImage(variant.avatarData, context, imageLoader)
|
||||
canvas.drawAvatar(
|
||||
avatarImage = avatarImage,
|
||||
avatarData = variant.avatarData,
|
||||
borderColor = colors.avatarStroke,
|
||||
backgroundColor = colors.avatarBackground,
|
||||
foregroundColor = colors.avatarForeground,
|
||||
dimensions = dimensions,
|
||||
)
|
||||
}
|
||||
PinVariant.PinnedLocation,
|
||||
PinVariant.StaleLocation -> canvas.drawDot(colors.dot, dimensions)
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun Canvas.drawPinShape(fillColor: Color, strokeColor: Color, dimensions: PinDimensions) {
|
||||
val path = createPinPath(dimensions)
|
||||
fillPaint.color = fillColor.toArgb()
|
||||
drawPath(path, fillPaint)
|
||||
strokePaint.color = strokeColor.toArgb()
|
||||
strokePaint.strokeWidth = dimensions.strokeWidth
|
||||
drawPath(path, strokePaint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the teardrop-shaped pin path to match dimensions.
|
||||
* Based on SVG path with dimensions 40x48 (ratio 1:1.2).
|
||||
*/
|
||||
private fun createPinPath(dimensions: PinDimensions): Path {
|
||||
val svgWidth = 40f
|
||||
val svgHeight = 48f
|
||||
val inset = dimensions.strokeWidth / 2
|
||||
val scaleX = (dimensions.pinWidth - dimensions.strokeWidth) / svgWidth
|
||||
val scaleY = (dimensions.pinHeight - dimensions.strokeWidth) / svgHeight
|
||||
|
||||
val path = Path().apply {
|
||||
moveTo(20f, 48f)
|
||||
cubicTo(19.4167f, 48f, 18.8333f, 47.8965f, 18.25f, 47.6895f)
|
||||
cubicTo(17.6667f, 47.4825f, 17.1458f, 47.1721f, 16.6875f, 46.7581f)
|
||||
cubicTo(13.9792f, 44.2743f, 11.5833f, 41.8525f, 9.5f, 39.4929f)
|
||||
cubicTo(7.41667f, 37.1332f, 5.67708f, 34.8461f, 4.28125f, 32.6313f)
|
||||
cubicTo(2.88542f, 30.4166f, 1.82292f, 28.2846f, 1.09375f, 26.2354f)
|
||||
cubicTo(0.364583f, 24.1863f, 0f, 22.2303f, 0f, 20.3674f)
|
||||
cubicTo(0f, 14.1578f, 2.01042f, 9.21087f, 6.03125f, 5.52652f)
|
||||
cubicTo(10.0521f, 1.84217f, 14.7083f, 0f, 20f, 0f)
|
||||
cubicTo(25.2917f, 0f, 29.9479f, 1.84217f, 33.9688f, 5.52652f)
|
||||
cubicTo(37.9896f, 9.21087f, 40f, 14.1578f, 40f, 20.3674f)
|
||||
cubicTo(40f, 22.2303f, 39.6354f, 24.1863f, 38.9062f, 26.2354f)
|
||||
cubicTo(38.1771f, 28.2846f, 37.1146f, 30.4166f, 35.7188f, 32.6313f)
|
||||
cubicTo(34.3229f, 34.8461f, 32.5833f, 37.1332f, 30.5f, 39.4929f)
|
||||
cubicTo(28.4167f, 41.8525f, 26.0208f, 44.2743f, 23.3125f, 46.7581f)
|
||||
cubicTo(22.8542f, 47.1721f, 22.3333f, 47.4825f, 21.75f, 47.6895f)
|
||||
cubicTo(21.1667f, 47.8965f, 20.5833f, 48f, 20f, 48f)
|
||||
close()
|
||||
}
|
||||
val matrix = Matrix().apply {
|
||||
setScale(scaleX, scaleY)
|
||||
postTranslate(inset, inset)
|
||||
}
|
||||
path.transform(matrix)
|
||||
return path
|
||||
}
|
||||
|
||||
private suspend fun loadAvatarImage(
|
||||
avatarData: AvatarData,
|
||||
context: Context,
|
||||
imageLoader: ImageLoader,
|
||||
): Image? {
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(avatarData)
|
||||
// Disable hardware rendering for Canvas
|
||||
.allowHardware(false)
|
||||
.build()
|
||||
return imageLoader.execute(request).image
|
||||
}
|
||||
|
||||
private fun Canvas.drawAvatar(
|
||||
avatarImage: Image?,
|
||||
avatarData: AvatarData,
|
||||
borderColor: Color,
|
||||
backgroundColor: Color,
|
||||
foregroundColor: Color,
|
||||
dimensions: PinDimensions,
|
||||
) {
|
||||
val centerX = dimensions.pinWidth / 2
|
||||
val avatarY = dimensions.avatarOffset
|
||||
val avatarRadius = dimensions.avatarSize / 2
|
||||
|
||||
withSave {
|
||||
if (avatarImage != null) {
|
||||
val bitmap = avatarImage.toBitmap()
|
||||
// Calculate centered square crop (ContentScale.Crop behavior)
|
||||
val srcSize = minOf(bitmap.width, bitmap.height)
|
||||
val srcX = (bitmap.width - srcSize) / 2
|
||||
val srcY = (bitmap.height - srcSize) / 2
|
||||
val srcRect = Rect(srcX, srcY, srcX + srcSize, srcY + srcSize)
|
||||
val destRect = RectF(
|
||||
centerX - avatarRadius,
|
||||
avatarY,
|
||||
centerX + avatarRadius,
|
||||
avatarY + dimensions.avatarSize
|
||||
)
|
||||
val clipPath = Path().apply {
|
||||
addCircle(centerX, avatarY + avatarRadius, avatarRadius, Path.Direction.CW)
|
||||
}
|
||||
clipPath(clipPath)
|
||||
drawBitmap(bitmap, srcRect, destRect, null)
|
||||
} else {
|
||||
drawInitialLetterAvatar(
|
||||
initialLetter = avatarData.initialLetter,
|
||||
centerX = centerX,
|
||||
centerY = avatarY + avatarRadius,
|
||||
radius = avatarRadius,
|
||||
foreground = foregroundColor.toArgb(),
|
||||
background = backgroundColor.toArgb()
|
||||
)
|
||||
}
|
||||
}
|
||||
strokePaint.color = borderColor.toArgb()
|
||||
strokePaint.strokeWidth = dimensions.strokeWidth
|
||||
drawCircle(centerX, avatarY + avatarRadius, avatarRadius, strokePaint)
|
||||
}
|
||||
|
||||
private fun Canvas.drawInitialLetterAvatar(
|
||||
initialLetter: String,
|
||||
centerX: Float,
|
||||
centerY: Float,
|
||||
radius: Float,
|
||||
foreground: Int,
|
||||
background: Int,
|
||||
) {
|
||||
fillPaint.color = background
|
||||
drawCircle(centerX, centerY, radius, fillPaint)
|
||||
textPaint.color = foreground
|
||||
textPaint.textSize = radius * 1.2f
|
||||
val textBounds = Rect()
|
||||
textPaint.getTextBounds(initialLetter, 0, 1, textBounds)
|
||||
val textY = centerY + textBounds.height() / 2f
|
||||
drawText(initialLetter, centerX, textY, textPaint)
|
||||
}
|
||||
|
||||
private fun Canvas.drawDot(dotColor: Color, dimensions: PinDimensions) {
|
||||
if (dotColor == Color.Transparent) return
|
||||
val centerX = dimensions.pinWidth / 2
|
||||
val centerY = dimensions.avatarOffset + dimensions.avatarSize / 2
|
||||
fillPaint.color = dotColor.toArgb()
|
||||
drawCircle(centerX, centerY, dimensions.dotRadius, fillPaint)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LocationPinPreview() = ElementPreview {
|
||||
val sampleAvatarData = AvatarData(
|
||||
id = "@alice:matrix.org",
|
||||
name = "Alice",
|
||||
url = null,
|
||||
size = AvatarSize.SelectedUser
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
LocationPin(
|
||||
variant = PinVariant.UserLocation(avatarData = sampleAvatarData, isLive = false),
|
||||
)
|
||||
LocationPin(
|
||||
variant = PinVariant.UserLocation(avatarData = sampleAvatarData, isLive = true),
|
||||
)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
LocationPin(
|
||||
variant = PinVariant.PinnedLocation,
|
||||
)
|
||||
LocationPin(
|
||||
variant = PinVariant.StaleLocation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberCacheKey(variant: PinVariant): String {
|
||||
val isLightTheme = ElementTheme.isLightTheme
|
||||
val density = LocalDensity.current.density
|
||||
return remember(isLightTheme, density, variant) {
|
||||
val pinVariant = when (variant) {
|
||||
PinVariant.PinnedLocation -> "pin_pinned"
|
||||
PinVariant.StaleLocation -> "pin_stale"
|
||||
is PinVariant.UserLocation -> "pin_user_${variant.avatarData.id}_${variant.isLive}"
|
||||
}
|
||||
"${pinVariant}_{$isLightTheme}_{$density}"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-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
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.R
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
|
||||
@Composable
|
||||
fun PinIcon(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(ElementTheme.colors.bgSubtlePrimary)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.width(22.dp),
|
||||
resourceId = R.drawable.pin,
|
||||
contentDescription = null,
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun PinIconPreview() = ElementPreview {
|
||||
PinIcon()
|
||||
}
|
||||
|
|
@ -46,7 +46,8 @@ enum class AvatarSize(val dp: Dp) {
|
|||
RoomInviteItem(52.dp),
|
||||
InviteSender(16.dp),
|
||||
|
||||
EditRoomDetails(68.dp),
|
||||
EditRoomDetails(64.dp),
|
||||
EditSpaceDetails(96.dp),
|
||||
RoomListManageUser(96.dp),
|
||||
|
||||
NotificationsOptIn(32.dp),
|
||||
|
|
@ -75,6 +76,6 @@ enum class AvatarSize(val dp: Dp) {
|
|||
SpaceMember(24.dp),
|
||||
LeaveSpaceRoom(32.dp),
|
||||
SelectParentSpace(32.dp),
|
||||
|
||||
AccountItem(32.dp),
|
||||
LocationPin(32.dp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ fun ListDialog(
|
|||
enabled: Boolean = true,
|
||||
applyPaddingToContents: Boolean = true,
|
||||
destructiveSubmit: Boolean = false,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(16.dp),
|
||||
listItems: LazyListScope.() -> Unit,
|
||||
) {
|
||||
val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
|
||||
|
|
@ -67,6 +68,7 @@ fun ListDialog(
|
|||
listItems = listItems,
|
||||
applyPaddingToContents = applyPaddingToContents,
|
||||
destructiveSubmit = destructiveSubmit,
|
||||
verticalArrangement = verticalArrangement,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +84,7 @@ private fun ListDialogContent(
|
|||
enabled: Boolean,
|
||||
applyPaddingToContents: Boolean,
|
||||
destructiveSubmit: Boolean,
|
||||
verticalArrangement: Arrangement.Vertical,
|
||||
subtitle: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
SimpleAlertDialogContent(
|
||||
|
|
@ -99,7 +102,7 @@ private fun ListDialogContent(
|
|||
val horizontalPadding = if (applyPaddingToContents) 0.dp else 8.dp
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(horizontal = horizontalPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = verticalArrangement,
|
||||
) { listItems() }
|
||||
}
|
||||
}
|
||||
|
|
@ -126,6 +129,7 @@ internal fun ListDialogContentPreview() {
|
|||
enabled = true,
|
||||
destructiveSubmit = false,
|
||||
applyPaddingToContents = true,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import io.element.android.libraries.designsystem.R
|
|||
// All the icons should be defined in Compound.
|
||||
internal val iconsOther = listOf(
|
||||
R.drawable.ic_notification,
|
||||
R.drawable.ic_stop,
|
||||
R.drawable.pin,
|
||||
R.drawable.ic_winner,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ import androidx.compose.ui.geometry.CornerRadius
|
|||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.graphics.AndroidPaint
|
||||
import androidx.compose.ui.graphics.ClipOp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
|
|
@ -55,10 +55,10 @@ fun Modifier.blurredShapeShadow(
|
|||
addRoundRect(RoundRect(Rect(Offset.Zero, size), CornerRadius(cornerRadius.toPx())))
|
||||
}
|
||||
|
||||
val frameworkPaint = android.graphics.Paint()
|
||||
// Draw the blurred shadow, then cut out the shape from it
|
||||
clipPath(path, ClipOp.Difference) {
|
||||
val paint = Paint()
|
||||
val frameworkPaint = paint.asFrameworkPaint()
|
||||
val paint = AndroidPaint(frameworkPaint)
|
||||
if (blurRadius != 0.dp) {
|
||||
frameworkPaint.maskFilter = BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ fun ModalBottomSheet(
|
|||
tonalElevation: Dp = if (ElementTheme.isLightTheme) 0.dp else BottomSheetDefaults.Elevation,
|
||||
scrimColor: Color = BottomSheetDefaults.ScrimColor,
|
||||
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
||||
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
|
||||
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.modalWindowInsets },
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val safeSheetState = if (LocalInspectionMode.current) sheetStateForPreview() else sheetState
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M6,16V8C6,7.45 6.196,6.979 6.588,6.588C6.979,6.196 7.45,6 8,6H16C16.55,6 17.021,6.196 17.413,6.588C17.804,6.979 18,7.45 18,8V16C18,16.55 17.804,17.021 17.413,17.413C17.021,17.804 16.55,18 16,18H8C7.45,18 6.979,17.804 6.588,17.413C6.196,17.021 6,16.55 6,16Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
|
|
@ -7,8 +7,7 @@
|
|||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
id("com.android.lint")
|
||||
id("io.element.jvm-library")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParse
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||
|
|
@ -115,6 +116,10 @@ class DefaultRoomLatestEventFormatter(
|
|||
val message = sp.getString(CommonStrings.common_unsupported_event)
|
||||
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
|
||||
}
|
||||
is LiveLocationContent -> {
|
||||
val message = sp.getString(CommonStrings.common_shared_location)
|
||||
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
|
||||
}
|
||||
is LegacyCallInviteContent -> sp.getString(CommonStrings.common_unsupported_call)
|
||||
is CallNotifyContent -> sp.getString(CommonStrings.common_call_started)
|
||||
}?.take(DEFAULT_SAFE_LENGTH)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LegacyCallInviteContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LiveLocationContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
|
|
@ -69,6 +70,7 @@ class DefaultTimelineEventFormatter(
|
|||
is MessageContent,
|
||||
is FailedToParseMessageLikeContent,
|
||||
is FailedToParseStateContent,
|
||||
is LiveLocationContent,
|
||||
is UnknownContent -> {
|
||||
if (buildMeta.isDebuggable) {
|
||||
error("You should not use this formatter for this event content: $content")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<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_by_you">"Вие забранихте %1$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>
|
||||
|
|
@ -58,5 +59,6 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -1,73 +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_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_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">"%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_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">"%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_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_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_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">"%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">"%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">"%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">"%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_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">"%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>
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ class DefaultPinnedMessagesBannerFormatterTest {
|
|||
ImageMessageType(body, null, null, MediaSource("url"), null),
|
||||
StickerMessageType(body, null, null, MediaSource("url"), null),
|
||||
FileMessageType(body, null, null, MediaSource("url"), null),
|
||||
LocationMessageType(body, "geo:1,2", null),
|
||||
LocationMessageType(body, "geo:1,2", null, null),
|
||||
NoticeMessageType(body, null),
|
||||
EmoteMessageType(body, null),
|
||||
OtherMessageType(msgType = "a_type", body = body),
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ class DefaultRoomLatestEventFormatterTest {
|
|||
ImageMessageType(body, null, null, MediaSource("url"), null),
|
||||
StickerMessageType(body, null, null, MediaSource("url"), null),
|
||||
FileMessageType(body, null, null, MediaSource("url"), null),
|
||||
LocationMessageType(body, "geo:1,2", null),
|
||||
LocationMessageType(body, "geo:1,2", null, null),
|
||||
NoticeMessageType(body, null),
|
||||
EmoteMessageType(body, null),
|
||||
OtherMessageType(msgType = "a_type", body = body),
|
||||
|
|
|
|||
|
|
@ -147,4 +147,19 @@ enum class FeatureFlags(
|
|||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
LiveLocationSharing(
|
||||
key = "feature.liveLocationSharing",
|
||||
title = "Live location sharing",
|
||||
description = "Allow sharing live location in rooms.",
|
||||
defaultValue = { false },
|
||||
isFinished = false,
|
||||
),
|
||||
ValidateNetworkWhenSchedulingNotificationFetching(
|
||||
key = "feature.validate_network_when_scheduling_notification_fetching",
|
||||
title = "validate internet connectivity when scheduling notification fetching",
|
||||
description = "Only fetch events for push notifications when the device has internet connectivity. " +
|
||||
"Enabling this can be problematic in air-gapped environments.",
|
||||
defaultValue = { true },
|
||||
isFinished = false,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-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")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.maplibre.compose"
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
explicitApi()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(libs.maplibre)
|
||||
api(libs.maplibre.ktx)
|
||||
api(libs.maplibre.annotation)
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.maplibre.android.location.modes.CameraMode as InternalCameraMode
|
||||
|
||||
@Immutable
|
||||
public enum class CameraMode {
|
||||
NONE,
|
||||
NONE_COMPASS,
|
||||
NONE_GPS,
|
||||
TRACKING,
|
||||
TRACKING_COMPASS,
|
||||
TRACKING_GPS,
|
||||
TRACKING_GPS_NORTH;
|
||||
|
||||
@InternalCameraMode.Mode
|
||||
internal fun toInternal(): Int = when (this) {
|
||||
NONE -> InternalCameraMode.NONE
|
||||
NONE_COMPASS -> InternalCameraMode.NONE_COMPASS
|
||||
NONE_GPS -> InternalCameraMode.NONE_GPS
|
||||
TRACKING -> InternalCameraMode.TRACKING
|
||||
TRACKING_COMPASS -> InternalCameraMode.TRACKING_COMPASS
|
||||
TRACKING_GPS -> InternalCameraMode.TRACKING_GPS
|
||||
TRACKING_GPS_NORTH -> InternalCameraMode.TRACKING_GPS_NORTH
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
fun fromInternal(@InternalCameraMode.Mode mode: Int): CameraMode = when (mode) {
|
||||
InternalCameraMode.NONE -> NONE
|
||||
InternalCameraMode.NONE_COMPASS -> NONE_COMPASS
|
||||
InternalCameraMode.NONE_GPS -> NONE_GPS
|
||||
InternalCameraMode.TRACKING -> TRACKING
|
||||
InternalCameraMode.TRACKING_COMPASS -> TRACKING_COMPASS
|
||||
InternalCameraMode.TRACKING_GPS -> TRACKING_GPS
|
||||
InternalCameraMode.TRACKING_GPS_NORTH -> TRACKING_GPS_NORTH
|
||||
else -> error("Unknown camera mode: $mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.maplibre.android.maps.MapLibreMap.OnCameraMoveStartedListener.REASON_API_ANIMATION
|
||||
import org.maplibre.android.maps.MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE
|
||||
import org.maplibre.android.maps.MapLibreMap.OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION
|
||||
|
||||
/**
|
||||
* Enumerates the different reasons why the map camera started to move.
|
||||
*
|
||||
* Based on enum values from https://docs.maptiler.com/maplibre-gl-native-android/org.maplibre.android.maps/#oncameramovestartedlistener.
|
||||
*
|
||||
* [NO_MOVEMENT_YET] is used as the initial state before any map movement has been observed.
|
||||
*
|
||||
* [UNKNOWN] is used to represent when an unsupported integer value is provided to [fromInt] - this
|
||||
* may be a new constant value from the Maps SDK that isn't supported by maps-compose yet, in which
|
||||
* case this library should be updated to include a new enum value for that constant.
|
||||
*/
|
||||
@Immutable
|
||||
public enum class CameraMoveStartedReason(public val value: Int) {
|
||||
UNKNOWN(-2),
|
||||
NO_MOVEMENT_YET(-1),
|
||||
GESTURE(REASON_API_GESTURE),
|
||||
API_ANIMATION(REASON_API_ANIMATION),
|
||||
DEVELOPER_ANIMATION(REASON_DEVELOPER_ANIMATION);
|
||||
|
||||
public companion object {
|
||||
/**
|
||||
* Converts from the Maps SDK [org.maplibre.android.maps.MapLibreMap.OnCameraMoveStartedListener]
|
||||
* constants to [CameraMoveStartedReason], or returns [UNKNOWN] if there is no such
|
||||
* [CameraMoveStartedReason] for the given [value].
|
||||
*
|
||||
* See https://docs.maptiler.com/maplibre-gl-native-android/org.maplibre.android.maps/#oncameramovestartedlistener.
|
||||
*/
|
||||
public fun fromInt(value: Int): CameraMoveStartedReason {
|
||||
return values().firstOrNull { it.value == value } ?: return UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
import android.location.Location
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.maplibre.android.camera.CameraPosition
|
||||
import org.maplibre.android.camera.CameraUpdateFactory
|
||||
import org.maplibre.android.maps.MapLibreMap
|
||||
import org.maplibre.android.maps.Projection
|
||||
|
||||
/**
|
||||
* Create and [rememberSaveable] a [CameraPositionState] using [CameraPositionState.Saver].
|
||||
* [init] will be called when the [CameraPositionState] is first created to configure its
|
||||
* initial state.
|
||||
*/
|
||||
@Composable
|
||||
public inline fun rememberCameraPositionState(
|
||||
crossinline init: CameraPositionState.() -> Unit = {}
|
||||
): CameraPositionState = rememberSaveable(saver = CameraPositionState.Saver) {
|
||||
CameraPositionState().apply(init)
|
||||
}
|
||||
|
||||
/**
|
||||
* A state object that can be hoisted to control and observe the map's camera state.
|
||||
* A [CameraPositionState] may only be used by a single [MapLibreMap] composable at a time
|
||||
* as it reflects instance state for a single view of a map.
|
||||
*
|
||||
* @param position the initial camera position
|
||||
* @param cameraMode the initial camera mode
|
||||
*/
|
||||
public class CameraPositionState(
|
||||
position: CameraPosition = CameraPosition.Builder().build(),
|
||||
cameraMode: CameraMode = CameraMode.NONE,
|
||||
) {
|
||||
/**
|
||||
* Whether the camera is currently moving or not. This includes any kind of movement:
|
||||
* panning, zooming, or rotation.
|
||||
*/
|
||||
public var isMoving: Boolean by mutableStateOf(false)
|
||||
internal set
|
||||
|
||||
/**
|
||||
* The reason for the start of the most recent camera moment, or
|
||||
* [CameraMoveStartedReason.NO_MOVEMENT_YET] if the camera hasn't moved yet or
|
||||
* [CameraMoveStartedReason.UNKNOWN] if an unknown constant is received from the Maps SDK.
|
||||
*/
|
||||
public var cameraMoveStartedReason: CameraMoveStartedReason by mutableStateOf(
|
||||
CameraMoveStartedReason.NO_MOVEMENT_YET
|
||||
)
|
||||
internal set
|
||||
|
||||
/**
|
||||
* Returns the current [Projection] to be used for converting between screen
|
||||
* coordinates and lat/lng.
|
||||
*/
|
||||
public val projection: Projection?
|
||||
get() = map?.projection
|
||||
|
||||
/**
|
||||
* Local source of truth for the current camera position.
|
||||
* While [map] is non-null this reflects the current position of [map] as it changes.
|
||||
* While [map] is null it reflects the last known map position, or the last value set by
|
||||
* explicitly setting [position].
|
||||
*/
|
||||
internal var rawPosition by mutableStateOf(position)
|
||||
|
||||
/**
|
||||
* Current position of the camera on the map.
|
||||
*/
|
||||
public var position: CameraPosition
|
||||
get() = rawPosition
|
||||
set(value) {
|
||||
synchronized(lock) {
|
||||
val map = map
|
||||
if (map == null) {
|
||||
rawPosition = value
|
||||
} else {
|
||||
map.moveCamera(CameraUpdateFactory.newCameraPosition(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Local source of truth for the current camera mode.
|
||||
* While [map] is non-null this reflects the current camera mode as it changes.
|
||||
* While [map] is null it reflects the last known camera mode, or the last value set by
|
||||
* explicitly setting [cameraMode].
|
||||
*/
|
||||
internal var rawCameraMode by mutableStateOf(cameraMode)
|
||||
|
||||
/**
|
||||
* Current tracking mode of the camera.
|
||||
*/
|
||||
public var cameraMode: CameraMode
|
||||
get() = rawCameraMode
|
||||
set(value) {
|
||||
synchronized(lock) {
|
||||
val map = map
|
||||
if (map == null) {
|
||||
rawCameraMode = value
|
||||
} else {
|
||||
map.locationComponent.cameraMode = value.toInternal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user's last available location.
|
||||
*/
|
||||
public var location: Location? by mutableStateOf(null)
|
||||
internal set
|
||||
|
||||
// Used to perform side effects thread-safely.
|
||||
// Guards all mutable properties that are not `by mutableStateOf`.
|
||||
private val lock = Unit
|
||||
|
||||
// The map currently associated with this CameraPositionState.
|
||||
// Guarded by `lock`.
|
||||
private var map: MapLibreMap? by mutableStateOf(null)
|
||||
|
||||
// The current map is set and cleared by side effect.
|
||||
// There can be only one associated at a time.
|
||||
internal fun setMap(map: MapLibreMap?) {
|
||||
synchronized(lock) {
|
||||
if (this.map == null && map == null) return
|
||||
if (this.map != null && map != null) {
|
||||
error("CameraPositionState may only be associated with one MapLibreMap at a time")
|
||||
}
|
||||
this.map = map
|
||||
if (map == null) {
|
||||
isMoving = false
|
||||
} else {
|
||||
map.moveCamera(CameraUpdateFactory.newCameraPosition(position))
|
||||
map.locationComponent.cameraMode = cameraMode.toInternal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public companion object {
|
||||
/**
|
||||
* The default saver implementation for [CameraPositionState].
|
||||
*/
|
||||
public val Saver: Saver<CameraPositionState, SaveableCameraPositionData> = Saver(
|
||||
save = { SaveableCameraPositionData(it.position, it.cameraMode.toInternal()) },
|
||||
restore = { CameraPositionState(it.position, CameraMode.fromInternal(it.cameraMode)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides the [CameraPositionState] used by the map. */
|
||||
internal val LocalCameraPositionState = staticCompositionLocalOf { CameraPositionState() }
|
||||
|
||||
/** The current [CameraPositionState] used by the map. */
|
||||
public val currentCameraPositionState: CameraPositionState
|
||||
@[MapLibreMapComposable ReadOnlyComposable Composable]
|
||||
get() = LocalCameraPositionState.current
|
||||
|
||||
@Parcelize
|
||||
public data class SaveableCameraPositionData(
|
||||
val position: CameraPosition,
|
||||
val cameraMode: Int
|
||||
) : Parcelable
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.maplibre.android.style.layers.Property
|
||||
|
||||
@Immutable
|
||||
public enum class IconAnchor {
|
||||
CENTER,
|
||||
LEFT,
|
||||
RIGHT,
|
||||
TOP,
|
||||
BOTTOM,
|
||||
TOP_LEFT,
|
||||
TOP_RIGHT,
|
||||
BOTTOM_LEFT,
|
||||
BOTTOM_RIGHT;
|
||||
|
||||
@Property.ICON_ANCHOR
|
||||
internal fun toInternal(): String = when (this) {
|
||||
CENTER -> Property.ICON_ANCHOR_CENTER
|
||||
LEFT -> Property.ICON_ANCHOR_LEFT
|
||||
RIGHT -> Property.ICON_ANCHOR_RIGHT
|
||||
TOP -> Property.ICON_ANCHOR_TOP
|
||||
BOTTOM -> Property.ICON_ANCHOR_BOTTOM
|
||||
TOP_LEFT -> Property.ICON_ANCHOR_TOP_LEFT
|
||||
TOP_RIGHT -> Property.ICON_ANCHOR_TOP_RIGHT
|
||||
BOTTOM_LEFT -> Property.ICON_ANCHOR_BOTTOM_LEFT
|
||||
BOTTOM_RIGHT -> Property.ICON_ANCHOR_BOTTOM_RIGHT
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
import androidx.compose.runtime.AbstractApplier
|
||||
import org.maplibre.android.maps.MapLibreMap
|
||||
import org.maplibre.android.maps.Style
|
||||
import org.maplibre.android.plugins.annotation.SymbolManager
|
||||
|
||||
internal interface MapNode {
|
||||
fun onAttached() {}
|
||||
fun onRemoved() {}
|
||||
fun onCleared() {}
|
||||
}
|
||||
|
||||
private object MapNodeRoot : MapNode
|
||||
|
||||
internal class MapApplier(
|
||||
val map: MapLibreMap,
|
||||
val style: Style,
|
||||
val symbolManager: SymbolManager,
|
||||
) : AbstractApplier<MapNode>(MapNodeRoot) {
|
||||
private val decorations = mutableListOf<MapNode>()
|
||||
|
||||
override fun onClear() {
|
||||
symbolManager.deleteAll()
|
||||
decorations.forEach { it.onCleared() }
|
||||
decorations.clear()
|
||||
}
|
||||
|
||||
override fun insertBottomUp(index: Int, instance: MapNode) {
|
||||
decorations.add(index, instance)
|
||||
instance.onAttached()
|
||||
}
|
||||
|
||||
override fun insertTopDown(index: Int, instance: MapNode) {
|
||||
// insertBottomUp is preferred
|
||||
}
|
||||
|
||||
override fun move(from: Int, to: Int, count: Int) {
|
||||
decorations.move(from, to, count)
|
||||
}
|
||||
|
||||
override fun remove(index: Int, count: Int) {
|
||||
repeat(count) {
|
||||
decorations[index + it].onRemoved()
|
||||
}
|
||||
decorations.remove(index, count)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Composition
|
||||
import androidx.compose.runtime.CompositionContext
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCompositionContext
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import org.maplibre.android.MapLibre
|
||||
import org.maplibre.android.maps.MapLibreMap
|
||||
import org.maplibre.android.maps.MapView
|
||||
import org.maplibre.android.maps.Style
|
||||
import org.maplibre.android.plugins.annotation.SymbolManager
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* A compose container for a MapLibre [MapView].
|
||||
*
|
||||
* Heavily inspired by https://github.com/googlemaps/android-maps-compose
|
||||
*
|
||||
* @param styleUri a URI where to asynchronously fetch a style for the map
|
||||
* @param modifier Modifier to be applied to the MapLibreMap
|
||||
* @param images images added to the map's style to be later used with [Symbol]
|
||||
* @param cameraPositionState the [CameraPositionState] to be used to control or observe the map's
|
||||
* camera state
|
||||
* @param uiSettings the [MapUiSettings] to be used for UI-specific settings on the map
|
||||
* @param symbolManagerSettings the [MapSymbolManagerSettings] to be used for symbol manager settings
|
||||
* @param locationSettings the [MapLocationSettings] to be used for location settings
|
||||
* @param content the content of the map
|
||||
*/
|
||||
@Composable
|
||||
public fun MapLibreMap(
|
||||
styleUri: String,
|
||||
modifier: Modifier = Modifier,
|
||||
images: ImmutableMap<String, Int> = persistentMapOf(),
|
||||
cameraPositionState: CameraPositionState = rememberCameraPositionState(),
|
||||
uiSettings: MapUiSettings = DefaultMapUiSettings,
|
||||
symbolManagerSettings: MapSymbolManagerSettings = DefaultMapSymbolManagerSettings,
|
||||
locationSettings: MapLocationSettings = DefaultMapLocationSettings,
|
||||
content: (@Composable @MapLibreMapComposable () -> Unit)? = null,
|
||||
) {
|
||||
// When in preview, early return a Box with the received modifier preserving layout
|
||||
if (LocalInspectionMode.current) {
|
||||
@Suppress("ModifierReused") // False positive, the modifier is not reused due to the early return.
|
||||
Box(
|
||||
modifier = modifier.background(Color.DarkGray)
|
||||
) {
|
||||
Text("[Map]", modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val mapView = remember {
|
||||
MapLibre.getInstance(context)
|
||||
MapView(context)
|
||||
}
|
||||
|
||||
@Suppress("ModifierReused")
|
||||
AndroidView(modifier = modifier, factory = { mapView })
|
||||
MapLifecycle(mapView)
|
||||
|
||||
// rememberUpdatedState and friends are used here to make these values observable to
|
||||
// the subcomposition without providing a new content function each recomposition
|
||||
val currentCameraPositionState by rememberUpdatedState(cameraPositionState)
|
||||
val currentUiSettings by rememberUpdatedState(uiSettings)
|
||||
val currentMapLocationSettings by rememberUpdatedState(locationSettings)
|
||||
val currentSymbolManagerSettings by rememberUpdatedState(symbolManagerSettings)
|
||||
|
||||
val parentComposition = rememberCompositionContext()
|
||||
val currentContent by rememberUpdatedState(content)
|
||||
|
||||
LaunchedEffect(styleUri, images) {
|
||||
disposingComposition {
|
||||
parentComposition.newComposition(
|
||||
context = context,
|
||||
mapView = mapView,
|
||||
styleUri = styleUri,
|
||||
images = images,
|
||||
) {
|
||||
MapUpdater(
|
||||
cameraPositionState = currentCameraPositionState,
|
||||
uiSettings = currentUiSettings,
|
||||
locationSettings = currentMapLocationSettings,
|
||||
symbolManagerSettings = currentSymbolManagerSettings,
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalCameraPositionState provides cameraPositionState,
|
||||
) {
|
||||
currentContent?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun disposingComposition(factory: () -> Composition) {
|
||||
val composition = factory()
|
||||
try {
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
composition.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun CompositionContext.newComposition(
|
||||
context: Context,
|
||||
mapView: MapView,
|
||||
styleUri: String,
|
||||
images: ImmutableMap<String, Int>,
|
||||
noinline content: @Composable () -> Unit
|
||||
): Composition {
|
||||
val map = mapView.awaitMap()
|
||||
val style = map.awaitStyle(context, styleUri, images)
|
||||
val symbolManager = SymbolManager(mapView, map, style)
|
||||
return Composition(
|
||||
MapApplier(map, style, symbolManager),
|
||||
this
|
||||
).apply {
|
||||
setContent(content)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun MapView.awaitMap(): MapLibreMap = suspendCoroutine { continuation ->
|
||||
getMapAsync { map ->
|
||||
continuation.resume(map)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun MapLibreMap.awaitStyle(
|
||||
context: Context,
|
||||
styleUri: String,
|
||||
images: ImmutableMap<String, Int>,
|
||||
): Style = suspendCoroutine { continuation ->
|
||||
setStyle(
|
||||
Style.Builder().apply {
|
||||
fromUri(styleUri)
|
||||
images.forEach { (id, drawableRes) ->
|
||||
withImage(id, checkNotNull(context.getDrawable(drawableRes)) {
|
||||
"Drawable resource $drawableRes with id $id not found"
|
||||
})
|
||||
}
|
||||
}
|
||||
) { style ->
|
||||
continuation.resume(style)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers lifecycle observers to the local [MapView].
|
||||
*/
|
||||
@Composable
|
||||
private fun MapLifecycle(mapView: MapView) {
|
||||
val context = LocalContext.current
|
||||
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||
val previousState = remember { mutableStateOf(Lifecycle.Event.ON_CREATE) }
|
||||
DisposableEffect(context, lifecycle, mapView) {
|
||||
val mapLifecycleObserver = mapView.lifecycleObserver(previousState)
|
||||
val callbacks = mapView.componentCallbacks()
|
||||
|
||||
lifecycle.addObserver(mapLifecycleObserver)
|
||||
context.registerComponentCallbacks(callbacks)
|
||||
|
||||
onDispose {
|
||||
lifecycle.removeObserver(mapLifecycleObserver)
|
||||
context.unregisterComponentCallbacks(callbacks)
|
||||
}
|
||||
}
|
||||
DisposableEffect(mapView) {
|
||||
onDispose {
|
||||
mapView.onDestroy()
|
||||
mapView.removeAllViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MapView.lifecycleObserver(previousState: MutableState<Lifecycle.Event>): LifecycleEventObserver =
|
||||
LifecycleEventObserver { _, event ->
|
||||
event.targetState
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_CREATE -> {
|
||||
// Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in
|
||||
// this case the MapLibreMap composable also doesn't leave the composition. So,
|
||||
// recreating the map does not restore state properly which must be avoided.
|
||||
if (previousState.value != Lifecycle.Event.ON_STOP) {
|
||||
this.onCreate(Bundle())
|
||||
}
|
||||
}
|
||||
Lifecycle.Event.ON_START -> this.onStart()
|
||||
Lifecycle.Event.ON_RESUME -> this.onResume()
|
||||
Lifecycle.Event.ON_PAUSE -> this.onPause()
|
||||
Lifecycle.Event.ON_STOP -> this.onStop()
|
||||
Lifecycle.Event.ON_DESTROY -> {
|
||||
// handled in onDispose
|
||||
}
|
||||
Lifecycle.Event.ON_ANY -> error("ON_ANY should never be used")
|
||||
}
|
||||
previousState.value = event
|
||||
}
|
||||
|
||||
private fun MapView.componentCallbacks(): ComponentCallbacks2 =
|
||||
object : ComponentCallbacks2 {
|
||||
override fun onConfigurationChanged(config: Configuration) = Unit
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
override fun onLowMemory() = Unit
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
// We call the `MapView.onLowMemory` method for any memory trim level
|
||||
this@componentCallbacks.onLowMemory()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
import androidx.compose.runtime.ComposableTargetMarker
|
||||
|
||||
/**
|
||||
* An annotation that can be used to mark a composable function as being expected to be use in a
|
||||
* composable function that is also marked or inferred to be marked as a [MapLibreMapComposable].
|
||||
*
|
||||
* This will produce build warnings when [MapLibreMapComposable] composable functions are used outside
|
||||
* of a [MapLibreMapComposable] content lambda, and vice versa.
|
||||
*/
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@ComposableTargetMarker(description = "MapLibre Map Composable")
|
||||
@Target(
|
||||
AnnotationTarget.FILE,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.TYPE,
|
||||
AnnotationTarget.TYPE_PARAMETER,
|
||||
)
|
||||
public annotation class MapLibreMapComposable
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
internal val DefaultMapLocationSettings = MapLocationSettings()
|
||||
|
||||
/**
|
||||
* Data class for UI-related settings on the map.
|
||||
*
|
||||
* Note: Should not be a data class if in need of maintaining binary compatibility
|
||||
* on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/
|
||||
*/
|
||||
public data class MapLocationSettings(
|
||||
public val locationEnabled: Boolean = false,
|
||||
public val backgroundTintColor: Color = Color.Unspecified,
|
||||
public val foregroundTintColor: Color = Color.Unspecified,
|
||||
public val backgroundStaleTintColor: Color = Color.Unspecified,
|
||||
public val foregroundStaleTintColor: Color = Color.Unspecified,
|
||||
public val accuracyColor: Color = Color.Unspecified,
|
||||
public val pulseEnabled: Boolean = false,
|
||||
public val pulseColor: Color = Color.Unspecified
|
||||
)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
internal val DefaultMapSymbolManagerSettings = MapSymbolManagerSettings()
|
||||
|
||||
/**
|
||||
* Data class for UI-related settings on the map.
|
||||
*
|
||||
* Note: Should not be a data class if in need of maintaining binary compatibility
|
||||
* on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/
|
||||
*/
|
||||
public data class MapSymbolManagerSettings(
|
||||
public val iconAllowOverlap: Boolean = false,
|
||||
)
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
import android.view.Gravity
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
internal val DefaultMapUiSettings = MapUiSettings()
|
||||
|
||||
/**
|
||||
* Data class for UI-related settings on the map.
|
||||
*
|
||||
* Note: Should not be a data class if in need of maintaining binary compatibility
|
||||
* on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/
|
||||
*/
|
||||
public data class MapUiSettings(
|
||||
public val compassEnabled: Boolean = true,
|
||||
public val rotationGesturesEnabled: Boolean = true,
|
||||
public val scrollGesturesEnabled: Boolean = true,
|
||||
public val tiltGesturesEnabled: Boolean = true,
|
||||
public val zoomGesturesEnabled: Boolean = true,
|
||||
public val logoGravity: Int = Gravity.BOTTOM,
|
||||
public val attributionGravity: Int = Gravity.BOTTOM,
|
||||
public val attributionTintColor: Color = Color.Unspecified,
|
||||
)
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
@file:Suppress("MatchingDeclarationName")
|
||||
|
||||
package io.element.android.libraries.maplibre.compose
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ComposeNode
|
||||
import androidx.compose.runtime.currentComposer
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import org.maplibre.android.location.LocationComponentActivationOptions
|
||||
import org.maplibre.android.location.LocationComponentOptions
|
||||
import org.maplibre.android.location.OnCameraTrackingChangedListener
|
||||
import org.maplibre.android.location.engine.LocationEngineRequest
|
||||
import org.maplibre.android.maps.MapLibreMap
|
||||
import org.maplibre.android.maps.Style
|
||||
|
||||
private const val LOCATION_REQUEST_INTERVAL = 750L
|
||||
|
||||
internal class MapPropertiesNode(
|
||||
val map: MapLibreMap,
|
||||
style: Style,
|
||||
context: Context,
|
||||
cameraPositionState: CameraPositionState,
|
||||
locationSettings: MapLocationSettings,
|
||||
) : MapNode {
|
||||
init {
|
||||
map.locationComponent.activateLocationComponent(
|
||||
LocationComponentActivationOptions.Builder(context, style)
|
||||
.locationComponentOptions(
|
||||
LocationComponentOptions.builder(context)
|
||||
.backgroundTintColor(locationSettings.backgroundTintColor.toArgb())
|
||||
.foregroundTintColor(locationSettings.foregroundTintColor.toArgb())
|
||||
.backgroundStaleTintColor(locationSettings.backgroundStaleTintColor.toArgb())
|
||||
.foregroundStaleTintColor(locationSettings.foregroundStaleTintColor.toArgb())
|
||||
.accuracyColor(locationSettings.accuracyColor.toArgb())
|
||||
.pulseEnabled(locationSettings.pulseEnabled)
|
||||
.pulseColor(locationSettings.pulseColor.toArgb())
|
||||
.build()
|
||||
)
|
||||
.locationEngineRequest(
|
||||
LocationEngineRequest.Builder(LOCATION_REQUEST_INTERVAL)
|
||||
.setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)
|
||||
.setFastestInterval(LOCATION_REQUEST_INTERVAL)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
cameraPositionState.setMap(map)
|
||||
}
|
||||
|
||||
var cameraPositionState = cameraPositionState
|
||||
set(value) {
|
||||
if (value == field) return
|
||||
field.setMap(null)
|
||||
field = value
|
||||
value.setMap(map)
|
||||
}
|
||||
|
||||
override fun onAttached() {
|
||||
map.addOnCameraIdleListener {
|
||||
cameraPositionState.isMoving = false
|
||||
// addOnCameraIdleListener is only invoked when the camera position
|
||||
// is changed via .animate(). To handle updating state when .move()
|
||||
// is used, it's necessary to set the camera's position here as well
|
||||
cameraPositionState.rawPosition = map.cameraPosition
|
||||
// Updating user location on every camera move due to lack of a better location updates API.
|
||||
cameraPositionState.location = map.locationComponent.lastKnownLocation
|
||||
}
|
||||
map.addOnCameraMoveCancelListener {
|
||||
cameraPositionState.isMoving = false
|
||||
}
|
||||
map.addOnCameraMoveStartedListener {
|
||||
cameraPositionState.cameraMoveStartedReason = CameraMoveStartedReason.fromInt(it)
|
||||
cameraPositionState.isMoving = true
|
||||
}
|
||||
map.addOnCameraMoveListener {
|
||||
cameraPositionState.rawPosition = map.cameraPosition
|
||||
// Updating user location on every camera move due to lack of a better location updates API.
|
||||
cameraPositionState.location = map.locationComponent.lastKnownLocation
|
||||
}
|
||||
map.locationComponent.addOnCameraTrackingChangedListener(object : OnCameraTrackingChangedListener {
|
||||
override fun onCameraTrackingDismissed() {}
|
||||
|
||||
override fun onCameraTrackingChanged(currentMode: Int) {
|
||||
cameraPositionState.rawCameraMode = CameraMode.fromInternal(currentMode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onRemoved() {
|
||||
cameraPositionState.setMap(null)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
cameraPositionState.setMap(null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to keep the primary map properties up to date. This should never leave the map composition.
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
@Composable
|
||||
internal inline fun MapUpdater(
|
||||
cameraPositionState: CameraPositionState,
|
||||
locationSettings: MapLocationSettings,
|
||||
uiSettings: MapUiSettings,
|
||||
symbolManagerSettings: MapSymbolManagerSettings,
|
||||
) {
|
||||
val mapApplier = currentComposer.applier as MapApplier
|
||||
val map = mapApplier.map
|
||||
val style = mapApplier.style
|
||||
val symbolManager = mapApplier.symbolManager
|
||||
val context = LocalContext.current
|
||||
ComposeNode<MapPropertiesNode, MapApplier>(
|
||||
factory = {
|
||||
MapPropertiesNode(
|
||||
map = map,
|
||||
style = style,
|
||||
context = context,
|
||||
cameraPositionState = cameraPositionState,
|
||||
locationSettings = locationSettings,
|
||||
)
|
||||
},
|
||||
update = {
|
||||
set(locationSettings.locationEnabled) { map.locationComponent.isLocationComponentEnabled = it }
|
||||
|
||||
set(uiSettings.compassEnabled) { map.uiSettings.isCompassEnabled = it }
|
||||
set(uiSettings.rotationGesturesEnabled) { map.uiSettings.isRotateGesturesEnabled = it }
|
||||
set(uiSettings.scrollGesturesEnabled) { map.uiSettings.isScrollGesturesEnabled = it }
|
||||
set(uiSettings.tiltGesturesEnabled) { map.uiSettings.isTiltGesturesEnabled = it }
|
||||
set(uiSettings.zoomGesturesEnabled) { map.uiSettings.isZoomGesturesEnabled = it }
|
||||
set(uiSettings.logoGravity) { map.uiSettings.logoGravity = it }
|
||||
set(uiSettings.attributionGravity) { map.uiSettings.attributionGravity = it }
|
||||
set(uiSettings.attributionTintColor) { map.uiSettings.setAttributionTintColor(it.toArgb()) }
|
||||
|
||||
set(symbolManagerSettings.iconAllowOverlap) { symbolManager.iconAllowOverlap = it }
|
||||
|
||||
update(cameraPositionState) { this.cameraPositionState = it }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* 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.maplibre.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ComposeNode
|
||||
import androidx.compose.runtime.currentComposer
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import org.maplibre.android.geometry.LatLng
|
||||
import org.maplibre.android.plugins.annotation.Symbol
|
||||
import org.maplibre.android.plugins.annotation.SymbolManager
|
||||
import org.maplibre.android.plugins.annotation.SymbolOptions
|
||||
|
||||
internal class SymbolNode(
|
||||
val symbolManager: SymbolManager,
|
||||
val symbol: Symbol,
|
||||
) : MapNode {
|
||||
override fun onRemoved() {
|
||||
symbolManager.delete(symbol)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
symbolManager.delete(symbol)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A state object that can be hoisted to control and observe the symbol state.
|
||||
*
|
||||
* @param position the initial symbol position
|
||||
*/
|
||||
public class SymbolState(
|
||||
position: LatLng
|
||||
) {
|
||||
/**
|
||||
* Current position of the symbol.
|
||||
*/
|
||||
public var position: LatLng by mutableStateOf(position)
|
||||
|
||||
public companion object {
|
||||
/**
|
||||
* The default saver implementation for [SymbolState].
|
||||
*/
|
||||
public val Saver: Saver<SymbolState, LatLng> = Saver(
|
||||
save = { it.position },
|
||||
restore = { SymbolState(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
public fun rememberSymbolState(
|
||||
position: LatLng = LatLng(0.0, 0.0)
|
||||
): SymbolState = rememberSaveable(saver = SymbolState.Saver) {
|
||||
SymbolState(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable for a symbol on the map.
|
||||
*
|
||||
* @param iconId an id of an image from the current [Style]
|
||||
* @param state the [SymbolState] to be used to control or observe the symbol
|
||||
* state such as its position and info window
|
||||
* @param iconAnchor the anchor for the symbol image
|
||||
*/
|
||||
@Composable
|
||||
@MapLibreMapComposable
|
||||
public fun Symbol(
|
||||
iconId: String,
|
||||
state: SymbolState = rememberSymbolState(),
|
||||
iconAnchor: IconAnchor? = null,
|
||||
) {
|
||||
val mapApplier = currentComposer.applier as MapApplier
|
||||
val symbolManager = mapApplier.symbolManager
|
||||
ComposeNode<SymbolNode, MapApplier>(
|
||||
factory = {
|
||||
SymbolNode(
|
||||
symbolManager = symbolManager,
|
||||
symbol = symbolManager.create(
|
||||
SymbolOptions().apply {
|
||||
withLatLng(state.position)
|
||||
withIconImage(iconId)
|
||||
iconAnchor?.let { withIconAnchor(it.toInternal()) }
|
||||
}
|
||||
),
|
||||
)
|
||||
},
|
||||
update = {
|
||||
update(state.position) {
|
||||
this.symbol.latLng = it
|
||||
symbolManager.update(this.symbol)
|
||||
}
|
||||
update(iconId) {
|
||||
this.symbol.iconImage = it
|
||||
symbolManager.update(this.symbol)
|
||||
}
|
||||
update(iconAnchor) {
|
||||
this.symbol.iconAnchor = it?.toInternal()
|
||||
symbolManager.update(this.symbol)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -20,5 +20,6 @@ sealed class QrLoginException : Exception() {
|
|||
data object OtherDeviceNotSignedIn : QrLoginException()
|
||||
data object CheckCodeAlreadySent : QrLoginException()
|
||||
data object CheckCodeCannotBeSent : QrLoginException()
|
||||
data object UnsupportedQrCodeType : QrLoginException()
|
||||
data object Unknown : QrLoginException()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ sealed class NotificationResolverException : Exception() {
|
|||
*/
|
||||
data object EventFilteredOut : NotificationResolverException()
|
||||
|
||||
/**
|
||||
* The event was found but it has been redacted.
|
||||
*/
|
||||
data object EventRedacted : NotificationResolverException()
|
||||
|
||||
/**
|
||||
* An unexpected error occurred while trying to resolve the event.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ sealed class ErrorType(message: String) : Exception(message) {
|
|||
*/
|
||||
class UnsupportedProtocol(message: String) : ErrorType(message)
|
||||
|
||||
/**
|
||||
* The QR code type is not supported by the client.
|
||||
*/
|
||||
class UnsupportedQrCodeType(message: String) : ErrorType(message)
|
||||
|
||||
/**
|
||||
* Secrets backup not set up properly.
|
||||
*/
|
||||
|
|
@ -33,13 +38,33 @@ sealed class ErrorType(message: String) : Exception(message) {
|
|||
*/
|
||||
class NotFound(message: String) : ErrorType(message)
|
||||
|
||||
/**
|
||||
* The device could not be created.
|
||||
*/
|
||||
class UnableToCreateDevice(message: String) : ErrorType(message)
|
||||
|
||||
/**
|
||||
* An unknown error has happened.
|
||||
*/
|
||||
class Unknown(message: String) : ErrorType(message)
|
||||
|
||||
/**
|
||||
* The requested device was not returned by the homeserver.
|
||||
*/
|
||||
class DeviceNotFound(message: String) : ErrorType(message)
|
||||
|
||||
/**
|
||||
* The other device is already signed in and so does not need to sign in.
|
||||
*/
|
||||
class OtherDeviceAlreadySignedIn(message: String) : ErrorType(message)
|
||||
|
||||
/**
|
||||
* The sign in was cancelled.
|
||||
*/
|
||||
class Cancelled(message: String) : ErrorType(message)
|
||||
|
||||
/**
|
||||
* The sign in was not completed in the required time.
|
||||
*/
|
||||
class Expired(message: String) : ErrorType(message)
|
||||
|
||||
/**
|
||||
* A secure connection could not have been established between the two devices.
|
||||
*/
|
||||
class ConnectionInsecure(message: String) : ErrorType(message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
package io.element.android.libraries.matrix.api.media
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
|
|
@ -16,9 +17,20 @@ data class MediaSource(
|
|||
/**
|
||||
* Url of the media.
|
||||
*/
|
||||
val url: String,
|
||||
private val url: String,
|
||||
/**
|
||||
* This is used to hold data for encrypted media.
|
||||
*/
|
||||
val json: String? = null,
|
||||
) : Parcelable
|
||||
) : Parcelable {
|
||||
/**
|
||||
* A URL with invalid parts (like `#fragment`, if it's an MXC url) removed.
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val safeUrl = if (url.startsWith("mxc")) {
|
||||
// We've seen some MXC urls in the wild having some `mxc://foo/bar#auto` fragment suffix, which is invalid
|
||||
url.substringBefore("#")
|
||||
} else {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ sealed interface NotificationContent {
|
|||
data class RtcNotification(
|
||||
val senderId: UserId,
|
||||
val type: RtcNotificationType,
|
||||
val callIntent: CallIntent,
|
||||
val expirationTimestampMillis: Long
|
||||
) : MessageLike
|
||||
|
||||
|
|
@ -127,3 +128,8 @@ enum class RtcNotificationType {
|
|||
RING,
|
||||
NOTIFY
|
||||
}
|
||||
|
||||
enum class CallIntent {
|
||||
AUDIO,
|
||||
VIDEO
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import io.element.android.libraries.matrix.api.core.DeviceId
|
|||
|
||||
sealed interface AccountManagementAction {
|
||||
data object Profile : AccountManagementAction
|
||||
data object SessionsList : AccountManagementAction
|
||||
data class SessionView(val deviceId: DeviceId) : AccountManagementAction
|
||||
data class SessionEnd(val deviceId: DeviceId) : AccountManagementAction
|
||||
data object DevicesList : AccountManagementAction
|
||||
data class DeviceView(val deviceId: DeviceId) : AccountManagementAction
|
||||
data class DeviceDelete(val deviceId: DeviceId) : AccountManagementAction
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||
|
||||
/**
|
||||
* Represents the consensus state of [CallIntent] among room members.
|
||||
* Call members can advertise their intent to use audio or video, clients can
|
||||
* use this in the UI and also to decide to start camera or not when joining.
|
||||
*
|
||||
* This enum distinguishes between full consensus (all members advertise and
|
||||
* agree), partial consensus (only some members advertise, but those who do
|
||||
* agree), and no consensus (either no one advertises or advertisers disagree).
|
||||
*/
|
||||
sealed interface CallIntentConsensus {
|
||||
/**
|
||||
* All members advertise and agree on the same [callIntent].
|
||||
*/
|
||||
data class Full(val callIntent: CallIntent) : CallIntentConsensus
|
||||
|
||||
/**
|
||||
* Some members advertise and agree on the same [callIntent], but not all of them.
|
||||
*/
|
||||
data class Partial(
|
||||
/** The call intent that the agreeing members have advertised. */
|
||||
val callIntent: CallIntent,
|
||||
/** The number of members who advertise and agree on the same [callIntent]. */
|
||||
val agreeingCount: Int,
|
||||
/** The total number of members in the call. */
|
||||
val totalCount: Int,
|
||||
) : CallIntentConsensus
|
||||
|
||||
/**
|
||||
* No consensus. No one advertises or advertisers disagree.
|
||||
*/
|
||||
data object None : CallIntentConsensus
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
|||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||
|
|
@ -182,4 +183,30 @@ interface JoinedRoom : BaseRoom {
|
|||
* Subscribe to a [Flow] of [SendQueueUpdate] related to this room.
|
||||
*/
|
||||
fun subscribeToSendQueueUpdates(): Flow<SendQueueUpdate>
|
||||
|
||||
/**
|
||||
* Subscribe to live location shares in this room.
|
||||
* @return Flow of list of active live location shares.
|
||||
*/
|
||||
fun subscribeToLiveLocationShares(): Flow<List<LiveLocationShare>>
|
||||
|
||||
/**
|
||||
* Start sharing live location in this room.
|
||||
* @param durationMillis How long to share location (in milliseconds).
|
||||
* @return Result indicating success or failure.
|
||||
*/
|
||||
suspend fun startLiveLocationShare(durationMillis: Long): Result<Unit>
|
||||
|
||||
/**
|
||||
* Stop sharing live location in this room.
|
||||
* @return Result indicating success or failure.
|
||||
*/
|
||||
suspend fun stopLiveLocationShare(): Result<Unit>
|
||||
|
||||
/**
|
||||
* Send a live location update while a live location share is active.
|
||||
* @param geoUri The geo URI (e.g., "geo:51.5074,-0.1278").
|
||||
* @return Result indicating success or failure.
|
||||
*/
|
||||
suspend fun sendLiveLocation(geoUri: String): Result<Unit>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ data class RoomInfo(
|
|||
val roomVersion: String?,
|
||||
val privilegedCreatorRole: Boolean,
|
||||
val isLowPriority: Boolean,
|
||||
val activeCallIntentConsensus: CallIntentConsensus,
|
||||
) {
|
||||
val aliases: List<RoomAlias>
|
||||
get() = listOfNotNull(canonicalAlias) + alternativeAliases
|
||||
|
|
|
|||
|
|
@ -10,5 +10,6 @@ package io.element.android.libraries.matrix.api.room.location
|
|||
|
||||
enum class AssetType {
|
||||
SENDER,
|
||||
PIN
|
||||
PIN,
|
||||
UNKNOWN
|
||||
}
|
||||
|
|
|
|||
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