Pillify permalinks (#8242)

This commit is contained in:
Yoan Pintas 2023-03-21 22:31:45 +01:00 committed by GitHub
parent b85a06422c
commit 9fd1a22e10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 178 additions and 62 deletions

1
changelog.d/8219.feature Normal file
View File

@ -0,0 +1 @@
Permalinks to a room/space are pillified

1
changelog.d/8220.feature Normal file
View File

@ -0,0 +1 @@
Permalinks to a matrix user are pillified

1
changelog.d/8221.feature Normal file
View File

@ -0,0 +1 @@
Permalinks to messages are pillified

View File

@ -3539,4 +3539,11 @@
<string name="settings_access_token">Access Token</string>
<string name="settings_access_token_summary">Your access token gives full access to your account. Do not share it with anyone.</string>
<!-- Pills -->
<string name="pill_message_from_user">Message from %s</string>
<string name="pill_message_from_unknown_user">Message</string>
<string name="pill_message_in_room">Message in %s</string>
<string name="pill_message_in_unknown_room">Message in room</string>
<string name="pill_message_unknown_room_or_space">Room/Space</string>
</resources>

View File

@ -18,8 +18,8 @@
<item name="dialog_width_ratio" format="float" type="dimen">0.75</item>
<dimen name="pill_avatar_size">16dp</dimen>
<dimen name="pill_min_height">20dp</dimen>
<dimen name="pill_avatar_size">20sp</dimen>
<dimen name="pill_min_height">26sp</dimen>
<dimen name="pill_text_padding">4dp</dimen>
<dimen name="call_pip_height">128dp</dimen>

View File

@ -65,27 +65,14 @@ object MatrixPatterns {
private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/"
const val SEP_REGEX = "/"
private const val LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = LINK_TO_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE)
private const val LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = LINK_TO_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
private const val LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = LINK_TO_APP_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE)
private const val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = LINK_TO_APP_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK = PERMALINK_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE)
private val PATTERN_CONTAIN_APP_PERMALINK = APP_BASE_REGEX.toRegex(RegexOption.IGNORE_CASE)
// ascii characters in the range \x20 (space) to \x7E (~)
val ORDER_STRING_REGEX = "[ -~]+".toRegex()
// list of patterns to find some matrix item.
val MATRIX_PATTERNS = listOf(
PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID,
PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS,
PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID,
PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS,
PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER,
PATTERN_CONTAIN_MATRIX_ALIAS,
PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER,
@ -146,6 +133,12 @@ object MatrixPatterns {
return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
}
fun isPermalink(str: String?): Boolean {
return str != null &&
(PATTERN_CONTAIN_MATRIX_TO_PERMALINK.containsMatchIn(str) ||
PATTERN_CONTAIN_APP_PERMALINK.containsMatchIn(str))
}
/**
* Extract server name from a matrix id.
*

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.permalinks
import android.text.Spannable
import android.util.Patterns
import org.matrix.android.sdk.api.MatrixPatterns
/**
@ -44,18 +45,21 @@ object MatrixLinkify {
}
val text = spannable.toString()
var hasMatch = false
for (pattern in MatrixPatterns.MATRIX_PATTERNS) {
for (pattern in listOf(Patterns.WEB_URL.toRegex()).plus(MatrixPatterns.MATRIX_PATTERNS)) {
for (match in pattern.findAll(spannable)) {
hasMatch = true
val startPos = match.range.first
if (startPos == 0 || text[startPos - 1] != '/') {
val endPos = match.range.last + 1
var url = text.substring(match.range)
if (MatrixPatterns.isUserId(url) ||
val isPermalink = MatrixPatterns.isPermalink(url)
if (isPermalink ||
MatrixPatterns.isUserId(url) ||
MatrixPatterns.isRoomAlias(url) ||
MatrixPatterns.isRoomId(url) ||
MatrixPatterns.isGroupId(url) ||
MatrixPatterns.isEventId(url)) {
if (!isPermalink) {
url = PermalinkService.MATRIX_TO_URL_BASE + url
}
val span = MatrixPermalinkSpan(url, callback)
@ -63,6 +67,7 @@ object MatrixLinkify {
}
}
}
}
return hasMatch
// return false

View File

@ -76,7 +76,8 @@ sealed class MatrixItem(
data class RoomItem(
override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null
override val avatarUrl: String? = null,
val roomDisplayName: String? = null
) :
MatrixItem(id, displayName, avatarUrl) {
init {
@ -102,7 +103,8 @@ sealed class MatrixItem(
data class RoomAliasItem(
override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null
override val avatarUrl: String? = null,
val roomDisplayName: String? = null
) :
MatrixItem(id, displayName, avatarUrl) {
init {
@ -136,6 +138,8 @@ sealed class MatrixItem(
val displayName = when (this) {
// use the room display name for the notify everyone item
is EveryoneInRoomItem -> roomDisplayName
is RoomItem -> roomDisplayName ?: displayName
is RoomAliasItem -> roomDisplayName ?: displayName
else -> displayName
}
return (displayName?.takeIf { it.isNotBlank() } ?: id)

View File

@ -20,7 +20,7 @@ import org.matrix.android.sdk.api.util.MatrixItem
fun MatrixItem.getBestName(): String {
// Note: this code is copied from [DisplayNameResolver] in the SDK
return if (this is MatrixItem.RoomAliasItem) {
return if (this is MatrixItem.RoomAliasItem && displayName.isNullOrBlank()) {
// Best name is the id, and we keep the displayName of the room for the case we need the first letter
id
} else {

View File

@ -163,7 +163,6 @@ import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet
import im.vector.app.features.home.room.threads.ThreadsManager
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.invite.VectorInviteView
import im.vector.app.features.location.LocationSharingMode
import im.vector.app.features.location.toLocationData
@ -247,7 +246,6 @@ class TimelineFragment :
@Inject lateinit var matrixItemColorProvider: MatrixItemColorProvider
@Inject lateinit var imageContentRenderer: ImageContentRenderer
@Inject lateinit var roomDetailPendingActionStore: RoomDetailPendingActionStore
@Inject lateinit var pillsPostProcessorFactory: PillsPostProcessor.Factory
@Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var audioMessagePlaybackTracker: AudioMessagePlaybackTracker
@Inject lateinit var shareIntentHandler: ShareIntentHandler

View File

@ -20,21 +20,30 @@ import android.content.Context
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.util.Patterns
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.html.PillImageSpan
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
class EventTextRenderer @AssistedInject constructor(
@Assisted private val roomId: String?,
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val activeSessionHolder: ActiveSessionHolder,
private val sessionHolder: ActiveSessionHolder,
) {
@AssistedFactory
@ -46,7 +55,8 @@ class EventTextRenderer @AssistedInject constructor(
* @param text the text to be rendered
*/
fun render(text: CharSequence): CharSequence {
return renderNotifyEveryone(text)
val formattedText = renderPermalinks(text)
return renderNotifyEveryone(formattedText)
}
private fun renderNotifyEveryone(text: CharSequence): CharSequence {
@ -59,8 +69,18 @@ class EventTextRenderer @AssistedInject constructor(
}
}
private fun renderPermalinks(text: CharSequence): CharSequence {
return if (roomId != null) {
SpannableStringBuilder(text).apply {
addPermalinksSpans(this)
}
} else {
text
}
}
private fun addNotifyEveryoneSpans(text: Spannable, roomId: String) {
val room: RoomSummary? = activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(roomId)
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(roomId)
val matrixItem = MatrixItem.EveryoneInRoomItem(
id = roomId,
avatarUrl = room?.avatarUrl,
@ -76,6 +96,23 @@ class EventTextRenderer @AssistedInject constructor(
}
}
private fun addPermalinksSpans(text: Spannable) {
for (match in Patterns.WEB_URL.toRegex().findAll(text)) {
val url = text.substring(match.range)
val matrixItem = if (MatrixPatterns.isPermalink(url)) {
when (val permalinkData = PermalinkParser.parse(url)) {
is PermalinkData.UserLink -> permalinkData.toMatrixItem()
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
else -> null
}
} else null
if (matrixItem != null) {
addPillSpan(text, createPillImageSpan(matrixItem), match.range.first, match.range.last + 1)
}
}
}
private fun createPillImageSpan(matrixItem: MatrixItem) =
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
@ -87,4 +124,46 @@ class EventTextRenderer @AssistedInject constructor(
) {
renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
private fun PermalinkData.UserLink.toMatrixItem(): MatrixItem? =
roomId?.let { sessionHolder.getSafeActiveSession()?.roomService()?.getRoomMember(userId, it)?.toMatrixItem() }
?: sessionHolder.getSafeActiveSession()?.getUserOrDefault(userId)?.toMatrixItem()
private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem =
if (eventId.isNullOrEmpty()) {
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomIdOrAlias)
when {
isRoomAlias -> MatrixItem.RoomAliasItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
room == null -> MatrixItem.RoomItem(roomIdOrAlias, context.getString(R.string.pill_message_unknown_room_or_space))
room.roomType == RoomType.SPACE -> MatrixItem.SpaceItem(roomIdOrAlias, room.displayName, room.avatarUrl)
else -> MatrixItem.RoomItem(roomIdOrAlias, room.displayName, room.avatarUrl)
}
} else {
if (roomIdOrAlias == roomId) {
val session = sessionHolder.getSafeActiveSession()
val event = session?.eventService()?.getEventFromCache(roomId, eventId!!)
val user = event?.senderId?.let { session.roomService().getRoomMember(it, roomId) }
val text = user?.let {
context.getString(R.string.pill_message_from_user, user.displayName)
} ?: context.getString(R.string.pill_message_from_unknown_user)
MatrixItem.RoomItem(roomIdOrAlias, text, user?.avatarUrl, user?.displayName)
} else {
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomIdOrAlias)
when {
isRoomAlias -> MatrixItem.RoomAliasItem(
roomIdOrAlias,
context.getString(R.string.pill_message_in_room, room?.displayName ?: roomIdOrAlias),
room?.avatarUrl,
room?.displayName
)
room != null -> MatrixItem.RoomItem(
roomIdOrAlias,
context.getString(R.string.pill_message_in_room, room.displayName),
room.avatarUrl,
room.displayName
)
else -> MatrixItem.RoomItem(roomIdOrAlias, context.getString(R.string.pill_message_in_unknown_room))
}
}
}
}

View File

@ -44,12 +44,11 @@ fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillI
}
fun CharSequence.linkify(callback: TimelineEventController.UrlClickCallback?): CharSequence {
val text = this.toString()
// SpannableStringBuilder is used to avoid Epoxy throwing ImmutableModelException
val spannable = SpannableStringBuilder(this)
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
override fun onUrlClicked(url: String) {
callback?.onUrlClicked(url, text)
callback?.onUrlClicked(url, this.toString())
}
})
VectorLinkify.addLinks(spannable, true)

View File

@ -26,14 +26,17 @@ import android.graphics.drawable.Drawable
import android.text.style.ReplacementSpan
import android.widget.TextView
import androidx.annotation.UiThread
import androidx.core.content.ContextCompat
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.ChipDrawable
import im.vector.app.R
import im.vector.app.core.extensions.isMatrixId
import im.vector.app.core.glide.GlideRequests
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem
import java.lang.ref.WeakReference
@ -111,11 +114,29 @@ class PillImageSpan(
private fun createChipDrawable(): ChipDrawable {
val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
val icon = try {
val icon = when {
matrixItem is MatrixItem.RoomAliasItem && matrixItem.avatarUrl.isNullOrEmpty() &&
matrixItem.displayName == context.getString(R.string.pill_message_in_room, matrixItem.id) -> {
ContextCompat.getDrawable(context, R.drawable.ic_permalink_round)
}
matrixItem is MatrixItem.RoomItem && matrixItem.avatarUrl.isNullOrEmpty() && (
matrixItem.displayName == context.getString(R.string.pill_message_in_unknown_room) ||
matrixItem.displayName == context.getString(R.string.pill_message_unknown_room_or_space) ||
matrixItem.displayName == context.getString(R.string.pill_message_from_unknown_user)
) -> {
ContextCompat.getDrawable(context, R.drawable.ic_permalink_round)
}
matrixItem is MatrixItem.UserItem && matrixItem.avatarUrl.isNullOrEmpty() && matrixItem.displayName?.isMatrixId().orTrue() -> {
ContextCompat.getDrawable(context, R.drawable.ic_user_round)
}
else -> {
try {
avatarRenderer.getCachedDrawable(glideRequests, matrixItem)
} catch (exception: Exception) {
avatarRenderer.getPlaceholderDrawable(matrixItem)
}
}
}
return ChipDrawable.createFromResource(context, R.xml.pill_view).apply {
text = matrixItem.getBestName()

View File

@ -27,7 +27,7 @@ import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.AvatarRenderer
import io.noties.markwon.core.spans.LinkSpan
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.getUser
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -56,15 +56,15 @@ class PillsPostProcessor @AssistedInject constructor(
* ========================================================================================== */
override fun afterRender(renderedText: Spannable) {
addPillSpans(renderedText, roomId)
addPillSpans(renderedText)
}
/* ==========================================================================================
* Helper methods
* ========================================================================================== */
private fun addPillSpans(renderedText: Spannable, roomId: String?) {
addLinkSpans(renderedText, roomId)
private fun addPillSpans(renderedText: Spannable) {
addLinkSpans(renderedText)
}
private fun addPillSpan(
@ -76,11 +76,11 @@ class PillsPostProcessor @AssistedInject constructor(
renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
private fun addLinkSpans(renderedText: Spannable, roomId: String?) {
private fun addLinkSpans(renderedText: Spannable) {
// We let markdown handle links and then we add PillImageSpan if needed.
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
linkSpans.forEach { linkSpan ->
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
val pillSpan = linkSpan.createPillSpan() ?: return@forEach
val startSpan = renderedText.getSpanStart(linkSpan)
val endSpan = renderedText.getSpanEnd(linkSpan)
// GlideImagesPlugin causes duplicated pills if we have a nested spans in the pill span,
@ -104,21 +104,18 @@ class PillsPostProcessor @AssistedInject constructor(
private fun createPillImageSpan(matrixItem: MatrixItem) =
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? {
private fun LinkSpan.createPillSpan(): PillImageSpan? {
val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) {
is PermalinkData.UserLink -> permalinkData.toMatrixItem(roomId)
is PermalinkData.UserLink -> permalinkData.toMatrixItem()
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
else -> null
} ?: return null
return createPillImageSpan(matrixItem)
}
private fun PermalinkData.UserLink.toMatrixItem(roomId: String?): MatrixItem? =
if (roomId == null) {
sessionHolder.getSafeActiveSession()?.getUserOrDefault(userId)?.toMatrixItem()
} else {
sessionHolder.getSafeActiveSession()?.roomService()?.getRoomMember(userId, roomId)?.toMatrixItem()
}
private fun PermalinkData.UserLink.toMatrixItem(): MatrixItem? =
roomId?.let { sessionHolder.getSafeActiveSession()?.roomService()?.getRoomMember(userId, it)?.toMatrixItem() }
?: sessionHolder.getSafeActiveSession()?.getUser(userId)?.toMatrixItem()
private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem? =
if (eventId == null) {

View File

@ -110,13 +110,8 @@ class PermalinkHandler @Inject constructor(
val rootThreadEventId = permalinkData.eventId?.let { eventId ->
val room = roomId?.let { session?.getRoom(it) }
val rootThreadEventId = room?.getTimelineEvent(eventId)?.root?.getRootThreadEventId()
rootThreadEventId ?: if (room?.getTimelineEvent(eventId)?.isRootThread() == true) {
eventId
} else {
null
}
val event = room?.getTimelineEvent(eventId)
event?.root?.getRootThreadEventId() ?: eventId.takeIf { event?.isRootThread() == true }
}
openRoom(
navigationInterceptor,

View File

@ -0,0 +1,15 @@
<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,12m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0"
android:fillColor="@color/element_name_01"/>
<path
android:pathData="m12.378,8.101 l0.356,-0.356c0.984,-0.984 2.57,-0.994 3.543,-0.021 0.973,0.972 0.963,2.559 -0.021,3.543l-1.693,1.693c-0.984,0.984 -2.57,0.994 -3.543,0.021m0.603,2.919 l-0.356,0.356c-0.984,0.984 -2.57,0.994 -3.543,0.021 -0.973,-0.973 -0.963,-2.559 0.021,-3.543l1.693,-1.693c0.984,-0.984 2.57,-0.994 3.543,-0.021"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/palette_white"
android:strokeLineCap="round"/>
</vector>

View File

@ -4,15 +4,15 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M17.5911,20.2922C15.9951,21.3704 14.0711,22 12,22C9.7488,22 7.6713,21.2561 6,20.0007C3.5711,18.1763 2,15.2716 2,12C2,6.4771 6.4771,2 12,2C17.5228,2 22,6.4771 22,12C22,15.4518 20.2511,18.4951 17.5911,20.2922ZM12,12.5C13.6569,12.5 15,11.0449 15,9.25C15,7.4551 13.6569,6 12,6C10.3431,6 9,7.4551 9,9.25C9,11.0449 10.3431,12.5 12,12.5ZM12,20C14.162,20 16.1236,19.1424 17.5634,17.7488C16.673,15.5506 14.5176,14 12,14C9.4824,14 7.327,15.5506 6.4366,17.7488C7.8763,19.1424 9.838,20 12,20Z"
android:fillColor="#C1C6CD"
android:pathData="M18.709,21.951C16.794,23.244 14.485,24 12,24C9.299,24 6.806,23.107 4.8,21.601C1.885,19.412 0,15.926 0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,16.142 21.901,19.794 18.709,21.951ZM12,12.6C13.988,12.6 15.6,10.854 15.6,8.7C15.6,6.546 13.988,4.8 12,4.8C10.012,4.8 8.4,6.546 8.4,8.7C8.4,10.854 10.012,12.6 12,12.6ZM12,21.6C14.594,21.6 16.948,20.571 18.676,18.899C17.608,16.261 15.021,14.4 12,14.4C8.979,14.4 6.392,16.261 5.324,18.899C7.052,20.571 9.406,21.6 12,21.6Z"
android:fillColor="?vctr_content_secondary"
android:fillType="evenOdd"/>
<group>
<clip-path
android:pathData="M17.5911,20.2922C15.9951,21.3704 14.0711,22 12,22C9.7488,22 7.6713,21.2561 6,20.0007C3.5711,18.1763 2,15.2716 2,12C2,6.4771 6.4771,2 12,2C17.5228,2 22,6.4771 22,12C22,15.4518 20.2511,18.4951 17.5911,20.2922ZM12,12.5C13.6569,12.5 15,11.0449 15,9.25C15,7.4551 13.6569,6 12,6C10.3431,6 9,7.4551 9,9.25C9,11.0449 10.3431,12.5 12,12.5ZM12,20C14.162,20 16.1236,19.1424 17.5634,17.7488C16.673,15.5506 14.5176,14 12,14C9.4824,14 7.327,15.5506 6.4366,17.7488C7.8763,19.1424 9.838,20 12,20Z"
android:pathData="M18.709,21.951C16.794,23.244 14.485,24 12,24C9.299,24 6.806,23.107 4.8,21.601C1.885,19.412 0,15.926 0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,16.142 21.901,19.794 18.709,21.951ZM12,12.6C13.988,12.6 15.6,10.854 15.6,8.7C15.6,6.546 13.988,4.8 12,4.8C10.012,4.8 8.4,6.546 8.4,8.7C8.4,10.854 10.012,12.6 12,12.6ZM12,21.6C14.594,21.6 16.948,20.571 18.676,18.899C17.608,16.261 15.021,14.4 12,14.4C8.979,14.4 6.392,16.261 5.324,18.899C7.052,20.571 9.406,21.6 12,21.6Z"
android:fillType="evenOdd"/>
<path
android:pathData="M17.5911,20.2922L16.4715,18.6349L17.5911,20.2922ZM6,20.0007L4.7989,21.5999L4.7989,21.5999L6,20.0007ZM17.5634,17.7488L18.9544,19.1859L19.9234,18.2479L19.4171,16.998L17.5634,17.7488ZM6.4366,17.7488L4.5829,16.998L4.0766,18.2479L5.0456,19.1859L6.4366,17.7488ZM12,24C14.4825,24 16.7945,23.244 18.7107,21.9494L16.4715,18.6349C15.1957,19.4968 13.6596,20 12,20V24ZM4.7989,21.5999C6.8046,23.1065 9.3008,24 12,24V20C10.1967,20 8.538,19.4058 7.2011,18.4016L4.7989,21.5999ZM0,12C0,15.9273 1.8887,19.414 4.7989,21.5999L7.2011,18.4016C5.2535,16.9387 4,14.616 4,12H0ZM12,0C5.3726,0 0,5.3726 0,12H4C4,7.5817 7.5817,4 12,4V0ZM24,12C24,5.3726 18.6274,0 12,0V4C16.4183,4 20,7.5817 20,12H24ZM18.7107,21.9494C21.8977,19.7963 24,16.144 24,12H20C20,14.7596 18.6045,17.1939 16.4715,18.6349L18.7107,21.9494ZM13,9.25C13,10.0941 12.4046,10.5 12,10.5V14.5C14.9091,14.5 17,11.9958 17,9.25H13ZM12,8C12.4046,8 13,8.4059 13,9.25H17C17,6.5043 14.9091,4 12,4V8ZM11,9.25C11,8.4059 11.5954,8 12,8V4C9.0909,4 7,6.5043 7,9.25H11ZM12,10.5C11.5954,10.5 11,10.0941 11,9.25H7C7,11.9958 9.0909,14.5 12,14.5V10.5ZM16.1724,16.3118C15.0906,17.3588 13.6223,18 12,18V22C14.7017,22 17.1567,20.926 18.9544,19.1859L16.1724,16.3118ZM12,16C13.6752,16 15.1146,17.0305 15.7097,18.4996L19.4171,16.998C18.2314,14.0707 15.3599,12 12,12V16ZM8.2903,18.4996C8.8854,17.0305 10.3248,16 12,16V12C8.6401,12 5.7686,14.0707 4.5829,16.998L8.2903,18.4996ZM12,18C10.3777,18 8.9094,17.3588 7.8276,16.3118L5.0456,19.1859C6.8433,20.926 9.2983,22 12,22V18Z"
android:fillColor="#C1C6CD"/>
android:pathData="M18.709,21.951L19.564,23.216L18.709,21.951ZM4.8,21.601L3.883,22.822H3.883L4.8,21.601ZM18.676,18.899L19.738,19.996L20.478,19.28L20.092,18.325L18.676,18.899ZM5.324,18.899L3.908,18.325L3.522,19.28L4.262,19.996L5.324,18.899ZM12,25.527C14.8,25.527 17.404,24.675 19.564,23.216L17.854,20.685C16.184,21.814 14.171,22.473 12,22.473V25.527ZM3.883,22.822C6.144,24.52 8.956,25.527 12,25.527V22.473C9.641,22.473 7.467,21.694 5.717,20.38L3.883,22.822ZM-1.527,12C-1.527,16.427 0.601,20.357 3.883,22.822L5.717,20.38C3.17,18.466 1.527,15.425 1.527,12H-1.527ZM12,-1.527C4.529,-1.527 -1.527,4.529 -1.527,12H1.527C1.527,6.216 6.216,1.527 12,1.527V-1.527ZM25.527,12C25.527,4.529 19.471,-1.527 12,-1.527V1.527C17.784,1.527 22.473,6.216 22.473,12H25.527ZM19.564,23.216C23.159,20.788 25.527,16.671 25.527,12H22.473C22.473,15.613 20.644,18.8 17.854,20.685L19.564,23.216ZM14.073,8.7C14.073,10.128 13.032,11.073 12,11.073V14.127C14.944,14.127 17.127,11.58 17.127,8.7H14.073ZM12,6.327C13.032,6.327 14.073,7.272 14.073,8.7H17.127C17.127,5.82 14.944,3.273 12,3.273V6.327ZM9.927,8.7C9.927,7.272 10.968,6.327 12,6.327V3.273C9.055,3.273 6.873,5.82 6.873,8.7H9.927ZM12,11.073C10.968,11.073 9.927,10.128 9.927,8.7H6.873C6.873,11.58 9.055,14.127 12,14.127V11.073ZM17.614,17.801C16.16,19.209 14.182,20.073 12,20.073V23.127C15.007,23.127 17.737,21.933 19.738,19.996L17.614,17.801ZM12,15.927C14.378,15.927 16.417,17.391 17.26,19.472L20.092,18.325C18.798,15.131 15.664,12.873 12,12.873V15.927ZM6.74,19.472C7.582,17.391 9.622,15.927 12,15.927V12.873C8.336,12.873 5.202,15.131 3.908,18.325L6.74,19.472ZM12,20.073C9.818,20.073 7.84,19.209 6.386,17.801L4.262,19.996C6.263,21.933 8.993,23.127 12,23.127V20.073Z"
android:fillColor="?vctr_content_secondary"/>
</group>
</vector>