Merge branch 'develop' into feature/fga/draft_support
This commit is contained in:
commit
a77e5edef4
485 changed files with 2939 additions and 1591 deletions
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
- run: |
|
- run: |
|
||||||
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
npm install --save-dev @babel/plugin-transform-flow-strip-types
|
||||||
- name: Danger
|
- name: Danger
|
||||||
uses: danger/danger-js@12.3.2
|
uses: danger/danger-js@12.3.3
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile.js"
|
args: "--dangerfile ./tools/danger/dangerfile.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
|
|
@ -245,7 +245,7 @@ jobs:
|
||||||
yarn add danger-plugin-lint-report --dev
|
yarn add danger-plugin-lint-report --dev
|
||||||
- name: Danger lint
|
- name: Danger lint
|
||||||
if: always()
|
if: always()
|
||||||
uses: danger/danger-js@12.3.2
|
uses: danger/danger-js@12.3.3
|
||||||
with:
|
with:
|
||||||
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,14 @@ import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
const val SEND_QUEUES_RETRY_DELAY_MILLIS = 1500L
|
const val SEND_QUEUES_RETRY_DELAY_MILLIS = 500L
|
||||||
|
|
||||||
@SingleIn(SessionScope::class)
|
@SingleIn(SessionScope::class)
|
||||||
class SendQueues @Inject constructor(
|
class SendQueues @Inject constructor(
|
||||||
|
|
@ -45,14 +45,12 @@ class SendQueues @Inject constructor(
|
||||||
}
|
}
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
matrixClient.sendQueueDisabledFlow()
|
matrixClient.sendQueueDisabledFlow()
|
||||||
.onEach { roomId: RoomId ->
|
.debounce(SEND_QUEUES_RETRY_DELAY_MILLIS)
|
||||||
Timber.d("Send queue disabled for room $roomId")
|
.onEach { _: RoomId ->
|
||||||
if (networkMonitor.connectivity.value == NetworkStatus.Online) {
|
if (networkMonitor.connectivity.value == NetworkStatus.Online) {
|
||||||
delay(SEND_QUEUES_RETRY_DELAY_MILLIS)
|
matrixClient.setAllSendQueuesEnabled(enabled = true)
|
||||||
matrixClient.getRoom(roomId)?.use { room ->
|
|
||||||
room.setSendQueueEnabled(enabled = true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.appnav.di.MatrixClientsHolder
|
||||||
import io.element.android.features.login.api.LoginUserStory
|
import io.element.android.features.login.api.LoginUserStory
|
||||||
import io.element.android.features.preferences.api.CacheService
|
import io.element.android.features.preferences.api.CacheService
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
@ -39,6 +40,7 @@ class RootNavStateFlowFactory @Inject constructor(
|
||||||
private val authenticationService: MatrixAuthenticationService,
|
private val authenticationService: MatrixAuthenticationService,
|
||||||
private val cacheService: CacheService,
|
private val cacheService: CacheService,
|
||||||
private val matrixClientsHolder: MatrixClientsHolder,
|
private val matrixClientsHolder: MatrixClientsHolder,
|
||||||
|
private val imageLoaderHolder: ImageLoaderHolder,
|
||||||
private val loginUserStory: LoginUserStory,
|
private val loginUserStory: LoginUserStory,
|
||||||
) {
|
) {
|
||||||
private var currentCacheIndex = 0
|
private var currentCacheIndex = 0
|
||||||
|
|
@ -69,6 +71,8 @@ class RootNavStateFlowFactory @Inject constructor(
|
||||||
return cacheService.clearedCacheEventFlow
|
return cacheService.clearedCacheEventFlow
|
||||||
.onEach { sessionId ->
|
.onEach { sessionId ->
|
||||||
matrixClientsHolder.remove(sessionId)
|
matrixClientsHolder.remove(sessionId)
|
||||||
|
// Ensure image loader will be recreated with the new MatrixClient
|
||||||
|
imageLoaderHolder.remove(sessionId)
|
||||||
}
|
}
|
||||||
.toIndexFlow(initialCacheIndex)
|
.toIndexFlow(initialCacheIndex)
|
||||||
.onEach { cacheIndex ->
|
.onEach { cacheIndex ->
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,13 @@ import org.junit.Test
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
||||||
assert(setAllSendQueuesEnabledLambda)
|
assert(setAllSendQueuesEnabledLambda)
|
||||||
.isCalledOnce()
|
.isCalledExactly(2)
|
||||||
.with(value(true))
|
.withSequence(
|
||||||
|
listOf(value(true)),
|
||||||
|
listOf(value(true)),
|
||||||
|
)
|
||||||
|
|
||||||
assert(setRoomSendQueueEnabledLambda)
|
assert(setRoomSendQueueEnabledLambda).isNeverCalled()
|
||||||
.isCalledOnce()
|
|
||||||
.with(value(true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
1
changelog.d/2916.misc
Normal file
1
changelog.d/2916.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Use a more natural date format for day dividers in the timeline. Also improve the time format for last messages in the room list.
|
||||||
1
changelog.d/3051.misc
Normal file
1
changelog.d/3051.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Resolve display names in mentions in real time, also send mentions with user ids as the fallback text for the link representation of the mentions.
|
||||||
1
changelog.d/3081.bugfix
Normal file
1
changelog.d/3081.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Let roles and permissions screens work for invited room members too.
|
||||||
1
changelog.d/3082.bugfix
Normal file
1
changelog.d/3082.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fix image rendering after clear cache
|
||||||
1
changelog.d/3083.bugfix
Normal file
1
changelog.d/3083.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Improve room filters behavior
|
||||||
1
changelog.d/3085.bugfix
Normal file
1
changelog.d/3085.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Make sure we replace the 'answer call' pending intent on ringing call notifications.
|
||||||
1
changelog.d/3086.bugfix
Normal file
1
changelog.d/3086.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Make sure we don't use the main dispatcher while closing the bug report request, as it can lead to crashes in strict mode.
|
||||||
|
|
@ -35,7 +35,7 @@ internal object IntentProvider {
|
||||||
context,
|
context,
|
||||||
DefaultElementCallEntryPoint.REQUEST_CODE,
|
DefaultElementCallEntryPoint.REQUEST_CODE,
|
||||||
createIntent(context, callType),
|
createIntent(context, callType),
|
||||||
0,
|
PendingIntent.FLAG_CANCEL_CURRENT,
|
||||||
false
|
false
|
||||||
)!!
|
)!!
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@
|
||||||
<string name="call_foreground_service_channel_title_android">"Apel în curs"</string>
|
<string name="call_foreground_service_channel_title_android">"Apel în curs"</string>
|
||||||
<string name="call_foreground_service_message_android">"Atingeți pentru a reveni la apel."</string>
|
<string name="call_foreground_service_message_android">"Atingeți pentru a reveni la apel."</string>
|
||||||
<string name="call_foreground_service_title_android">"☎️ Apel în curs"</string>
|
<string name="call_foreground_service_title_android">"☎️ Apel în curs"</string>
|
||||||
|
<string name="screen_incoming_call_subtitle_android">"Primiți un apel Element Call"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,20 @@
|
||||||
<string name="screen_qr_code_login_connection_note_secure_state_title">"Conexiunea nu este sigură"</string>
|
<string name="screen_qr_code_login_connection_note_secure_state_title">"Conexiunea nu este sigură"</string>
|
||||||
<string name="screen_qr_code_login_device_code_subtitle">"Vi se va cere să introduceți cele două cifre afișate pe acest dispozitiv."</string>
|
<string name="screen_qr_code_login_device_code_subtitle">"Vi se va cere să introduceți cele două cifre afișate pe acest dispozitiv."</string>
|
||||||
<string name="screen_qr_code_login_device_code_title">"Introduceți numărul de mai jos pe celălalt dispozitiv"</string>
|
<string name="screen_qr_code_login_device_code_title">"Introduceți numărul de mai jos pe celălalt dispozitiv"</string>
|
||||||
|
<string name="screen_qr_code_login_device_not_signed_in_scan_state_description">"Autentificați-vă pe celălalt dispozitiv și apoi încercați din nou sau utilizați un alt dispozitiv care este deja conectat."</string>
|
||||||
|
<string name="screen_qr_code_login_device_not_signed_in_scan_state_subtitle">"Celălalt dispozitiv nu este conectat"</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_subtitle">"Autentificarea a fost anulată de pe celălalt dispozitiv."</string>
|
||||||
|
<string name="screen_qr_code_login_error_cancelled_title">"Cererea de autentificare a fost anulată"</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_subtitle">"Autentificarea a fost refuzată pe celălalt dispozitiv."</string>
|
||||||
|
<string name="screen_qr_code_login_error_declined_title">"Autentificarea a fost refuzată"</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_subtitle">"Autentificarea a expirat. Vă rugăm să încercați din nou."</string>
|
||||||
|
<string name="screen_qr_code_login_error_expired_title">"Autentificarea nu a fost finalizată la timp"</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_subtitle">"Celălalt dispozitiv nu acceptă autentificarea la %s cu un cod QR.
|
||||||
|
|
||||||
|
Încercați să vă autentificați manual sau să scanați codul QR cu un alt dispozitiv."</string>
|
||||||
|
<string name="screen_qr_code_login_error_linking_not_suported_title">"Formatul codului QR nu este acceptat."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_subtitle">"Furnizorul dumneavoastră de cont nu acceptă %1$s."</string>
|
||||||
|
<string name="screen_qr_code_login_error_sliding_sync_not_supported_title">"%1$s nu este acceptat"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_button_title">"Gata de scanare"</string>
|
<string name="screen_qr_code_login_initial_state_button_title">"Gata de scanare"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_1">"Deschideți %1$s pe un dispozitiv desktop"</string>
|
<string name="screen_qr_code_login_initial_state_item_1">"Deschideți %1$s pe un dispozitiv desktop"</string>
|
||||||
<string name="screen_qr_code_login_initial_state_item_2">"Faceți clic pe avatarul dumneavoastră"</string>
|
<string name="screen_qr_code_login_initial_state_item_2">"Faceți clic pe avatarul dumneavoastră"</string>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ package io.element.android.features.messages.impl
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.node.node
|
import com.bumble.appyx.core.node.node
|
||||||
|
|
@ -64,12 +66,20 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
||||||
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
|
||||||
|
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanProvider
|
||||||
|
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
|
|
@ -82,6 +92,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
private val createPollEntryPoint: CreatePollEntryPoint,
|
private val createPollEntryPoint: CreatePollEntryPoint,
|
||||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
private val roomMemberProfilesCache: RoomMemberProfilesCache,
|
||||||
|
mentionSpanProviderFactory: MentionSpanProvider.Factory,
|
||||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Messages,
|
initialElement = NavTarget.Messages,
|
||||||
|
|
@ -137,6 +150,18 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
private val callback = plugins<MessagesEntryPoint.Callback>().firstOrNull()
|
private val callback = plugins<MessagesEntryPoint.Callback>().firstOrNull()
|
||||||
|
|
||||||
|
private val mentionSpanProvider = mentionSpanProviderFactory.create(room.sessionId.value)
|
||||||
|
|
||||||
|
override fun onBuilt() {
|
||||||
|
super.onBuilt()
|
||||||
|
|
||||||
|
room.membersStateFlow
|
||||||
|
.onEach { membersState ->
|
||||||
|
roomMemberProfilesCache.replace(membersState.joinedRoomMembers())
|
||||||
|
}
|
||||||
|
.launchIn(lifecycleScope)
|
||||||
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
return when (navTarget) {
|
return when (navTarget) {
|
||||||
is NavTarget.Messages -> {
|
is NavTarget.Messages -> {
|
||||||
|
|
@ -345,6 +370,13 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
BackstackWithOverlayBox(modifier)
|
mentionSpanProvider.updateStyles()
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalRoomMemberProfilesCache provides roomMemberProfilesCache,
|
||||||
|
LocalMentionSpanProvider provides mentionSpanProvider,
|
||||||
|
) {
|
||||||
|
BackstackWithOverlayBox(modifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,14 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.room.canCall
|
import io.element.android.libraries.matrix.ui.room.canCall
|
||||||
import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState
|
import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState
|
||||||
import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState
|
import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -138,6 +140,9 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
val roomAvatar: AsyncData<AvatarData> by remember {
|
val roomAvatar: AsyncData<AvatarData> by remember {
|
||||||
derivedStateOf { roomInfo?.avatarData()?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
derivedStateOf { roomInfo?.avatarData()?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
||||||
}
|
}
|
||||||
|
val heroes by remember {
|
||||||
|
derivedStateOf { roomInfo?.heroes().orEmpty().toPersistentList() }
|
||||||
|
}
|
||||||
|
|
||||||
var hasDismissedInviteDialog by rememberSaveable {
|
var hasDismissedInviteDialog by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
|
|
@ -204,6 +209,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
roomId = room.roomId,
|
roomId = room.roomId,
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomAvatar = roomAvatar,
|
roomAvatar = roomAvatar,
|
||||||
|
heroes = heroes,
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
||||||
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
||||||
|
|
@ -237,6 +243,12 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MatrixRoomInfo.heroes(): List<AvatarData> {
|
||||||
|
return heroes.map { user ->
|
||||||
|
user.getAvatarData(size = AvatarSize.TimelineRoom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.handleTimelineAction(
|
private fun CoroutineScope.handleTimelineAction(
|
||||||
action: TimelineItemAction,
|
action: TimelineItemAction,
|
||||||
targetEvent: TimelineItem.Event,
|
targetEvent: TimelineItem.Event,
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,14 @@ import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class MessagesState(
|
data class MessagesState(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val roomName: AsyncData<String>,
|
val roomName: AsyncData<String>,
|
||||||
val roomAvatar: AsyncData<AvatarData>,
|
val roomAvatar: AsyncData<AvatarData>,
|
||||||
|
val heroes: ImmutableList<AvatarData>,
|
||||||
val userHasPermissionToSendMessage: Boolean,
|
val userHasPermissionToSendMessage: Boolean,
|
||||||
val userHasPermissionToRedactOwn: Boolean,
|
val userHasPermissionToRedactOwn: Boolean,
|
||||||
val userHasPermissionToRedactOther: Boolean,
|
val userHasPermissionToRedactOther: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,8 @@ fun aMessagesState(
|
||||||
userHasPermissionToSendReaction: Boolean = true,
|
userHasPermissionToSendReaction: Boolean = true,
|
||||||
composerState: MessageComposerState = aMessageComposerState(
|
composerState: MessageComposerState = aMessageComposerState(
|
||||||
textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)),
|
textEditorState = TextEditorState.Rich(aRichTextEditorState(initialText = "Hello", initialFocus = true)),
|
||||||
isFullScreen = false,
|
isFullScreen = false,
|
||||||
mode = MessageComposerMode.Normal,
|
mode = MessageComposerMode.Normal,
|
||||||
),
|
),
|
||||||
voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(),
|
voiceMessageComposerState: VoiceMessageComposerState = aVoiceMessageComposerState(),
|
||||||
timelineState: TimelineState = aTimelineState(
|
timelineState: TimelineState = aTimelineState(
|
||||||
|
|
@ -121,6 +121,7 @@ fun aMessagesState(
|
||||||
roomId = RoomId("!id:domain"),
|
roomId = RoomId("!id:domain"),
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomAvatar = roomAvatar,
|
roomAvatar = roomAvatar,
|
||||||
|
heroes = persistentListOf(),
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
userHasPermissionToRedactOwn = userHasPermissionToRedactOwn,
|
||||||
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
userHasPermissionToRedactOther = userHasPermissionToRedactOther,
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,9 @@ import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialogType
|
import io.element.android.libraries.designsystem.components.ProgressDialogType
|
||||||
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.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -187,6 +187,7 @@ fun MessagesView(
|
||||||
MessagesViewTopBar(
|
MessagesViewTopBar(
|
||||||
roomName = state.roomName.dataOrNull(),
|
roomName = state.roomName.dataOrNull(),
|
||||||
roomAvatar = state.roomAvatar.dataOrNull(),
|
roomAvatar = state.roomAvatar.dataOrNull(),
|
||||||
|
heroes = state.heroes,
|
||||||
callState = state.callState,
|
callState = state.callState,
|
||||||
onBackClick = {
|
onBackClick = {
|
||||||
// Since the textfield is now based on an Android view, this is no longer done automatically.
|
// Since the textfield is now based on an Android view, this is no longer done automatically.
|
||||||
|
|
@ -442,6 +443,7 @@ private fun MessagesViewComposerBottomSheetContents(
|
||||||
private fun MessagesViewTopBar(
|
private fun MessagesViewTopBar(
|
||||||
roomName: String?,
|
roomName: String?,
|
||||||
roomAvatar: AvatarData?,
|
roomAvatar: AvatarData?,
|
||||||
|
heroes: ImmutableList<AvatarData>,
|
||||||
callState: RoomCallState,
|
callState: RoomCallState,
|
||||||
onRoomDetailsClick: () -> Unit,
|
onRoomDetailsClick: () -> Unit,
|
||||||
onJoinCallClick: () -> Unit,
|
onJoinCallClick: () -> Unit,
|
||||||
|
|
@ -457,6 +459,7 @@ private fun MessagesViewTopBar(
|
||||||
RoomAvatarAndNameRow(
|
RoomAvatarAndNameRow(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomAvatar = roomAvatar,
|
roomAvatar = roomAvatar,
|
||||||
|
heroes = heroes,
|
||||||
modifier = titleModifier
|
modifier = titleModifier
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -500,13 +503,17 @@ private fun CallMenuItem(
|
||||||
private fun RoomAvatarAndNameRow(
|
private fun RoomAvatarAndNameRow(
|
||||||
roomName: String,
|
roomName: String,
|
||||||
roomAvatar: AvatarData,
|
roomAvatar: AvatarData,
|
||||||
|
heroes: ImmutableList<AvatarData>,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Avatar(roomAvatar)
|
CompositeAvatar(
|
||||||
|
avatarData = roomAvatar,
|
||||||
|
heroes = heroes,
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = roomName,
|
text = roomName,
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,9 @@ class ActionListPresenter @Inject constructor(
|
||||||
is TimelineItemStateContent -> {
|
is TimelineItemStateContent -> {
|
||||||
buildList {
|
buildList {
|
||||||
add(TimelineItemAction.Copy)
|
add(TimelineItemAction.Copy)
|
||||||
add(TimelineItemAction.CopyLink)
|
if (timelineItem.isRemote) {
|
||||||
|
add(TimelineItemAction.CopyLink)
|
||||||
|
}
|
||||||
if (isDeveloperModeEnabled) {
|
if (isDeveloperModeEnabled) {
|
||||||
add(TimelineItemAction.ViewSource)
|
add(TimelineItemAction.ViewSource)
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +130,9 @@ class ActionListPresenter @Inject constructor(
|
||||||
if (timelineItem.content.canBeCopied()) {
|
if (timelineItem.content.canBeCopied()) {
|
||||||
add(TimelineItemAction.Copy)
|
add(TimelineItemAction.Copy)
|
||||||
}
|
}
|
||||||
add(TimelineItemAction.CopyLink)
|
if (timelineItem.isRemote) {
|
||||||
|
add(TimelineItemAction.CopyLink)
|
||||||
|
}
|
||||||
if (isDeveloperModeEnabled) {
|
if (isDeveloperModeEnabled) {
|
||||||
add(TimelineItemAction.ViewSource)
|
add(TimelineItemAction.ViewSource)
|
||||||
}
|
}
|
||||||
|
|
@ -145,8 +149,8 @@ class ActionListPresenter @Inject constructor(
|
||||||
if (timelineItem.isRemote) {
|
if (timelineItem.isRemote) {
|
||||||
add(TimelineItemAction.Reply)
|
add(TimelineItemAction.Reply)
|
||||||
add(TimelineItemAction.Forward)
|
add(TimelineItemAction.Forward)
|
||||||
|
add(TimelineItemAction.CopyLink)
|
||||||
}
|
}
|
||||||
add(TimelineItemAction.CopyLink)
|
|
||||||
if (isDeveloperModeEnabled) {
|
if (isDeveloperModeEnabled) {
|
||||||
add(TimelineItemAction.ViewSource)
|
add(TimelineItemAction.ViewSource)
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +191,9 @@ class ActionListPresenter @Inject constructor(
|
||||||
if (timelineItem.content.canBeCopied()) {
|
if (timelineItem.content.canBeCopied()) {
|
||||||
add(TimelineItemAction.Copy)
|
add(TimelineItemAction.Copy)
|
||||||
}
|
}
|
||||||
add(TimelineItemAction.CopyLink)
|
if (timelineItem.isRemote) {
|
||||||
|
add(TimelineItemAction.CopyLink)
|
||||||
|
}
|
||||||
if (isDeveloperModeEnabled) {
|
if (isDeveloperModeEnabled) {
|
||||||
add(TimelineItemAction.ViewSource)
|
add(TimelineItemAction.ViewSource)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,8 @@ class DefaultComposerDraftService @Inject constructor(
|
||||||
.onSuccess { draft ->
|
.onSuccess { draft ->
|
||||||
room.clearComposerDraft()
|
room.clearComposerDraft()
|
||||||
Timber.d("Loaded composer draft for room $roomId : $draft")
|
Timber.d("Loaded composer draft for room $roomId : $draft")
|
||||||
}.getOrNull()
|
}
|
||||||
|
.getOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,14 +93,14 @@ private fun RoomMemberSuggestionItemView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(modifier = modifier.clickable { onSelectSuggestion(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
Row(modifier = modifier.clickable { onSelectSuggestion(memberSuggestion) }, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
val avatarSize = AvatarSize.TimelineRoom
|
|
||||||
val avatarData = when (memberSuggestion) {
|
val avatarData = when (memberSuggestion) {
|
||||||
is ResolvedMentionSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
|
is ResolvedMentionSuggestion.AtRoom -> roomAvatar?.copy(size = AvatarSize.Suggestion)
|
||||||
|
?: AvatarData(roomId, roomName, null, AvatarSize.Suggestion)
|
||||||
is ResolvedMentionSuggestion.Member -> AvatarData(
|
is ResolvedMentionSuggestion.Member -> AvatarData(
|
||||||
memberSuggestion.roomMember.userId.value,
|
id = memberSuggestion.roomMember.userId.value,
|
||||||
memberSuggestion.roomMember.displayName,
|
name = memberSuggestion.roomMember.displayName,
|
||||||
memberSuggestion.roomMember.avatarUrl,
|
url = memberSuggestion.roomMember.avatarUrl,
|
||||||
avatarSize,
|
size = AvatarSize.Suggestion,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val title = when (memberSuggestion) {
|
val title = when (memberSuggestion) {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.Mention
|
import io.element.android.libraries.matrix.api.room.Mention
|
||||||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
|
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
|
||||||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
|
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
|
||||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
|
|
@ -66,8 +65,8 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
|
||||||
import io.element.android.libraries.permissions.api.PermissionsEvents
|
import io.element.android.libraries.permissions.api.PermissionsEvents
|
||||||
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
import io.element.android.libraries.permissions.api.PermissionsPresenter
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
|
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanProvider
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
||||||
import io.element.android.libraries.textcomposer.mentions.rememberMentionSpanProvider
|
|
||||||
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
|
import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState
|
||||||
import io.element.android.libraries.textcomposer.model.Message
|
import io.element.android.libraries.textcomposer.model.Message
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
|
|
@ -111,7 +110,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val messageComposerContext: DefaultMessageComposerContext,
|
private val messageComposerContext: DefaultMessageComposerContext,
|
||||||
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
private val richTextEditorStateFactory: RichTextEditorStateFactory,
|
||||||
private val currentSessionIdHolder: CurrentSessionIdHolder,
|
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
private val permalinkBuilder: PermalinkBuilder,
|
private val permalinkBuilder: PermalinkBuilder,
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
|
|
@ -208,7 +206,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
val memberSuggestions = remember { mutableStateListOf<ResolvedMentionSuggestion>() }
|
val memberSuggestions = remember { mutableStateListOf<ResolvedMentionSuggestion>() }
|
||||||
LaunchedEffect(isMentionsEnabled) {
|
LaunchedEffect(isMentionsEnabled) {
|
||||||
if (!isMentionsEnabled) return@LaunchedEffect
|
if (!isMentionsEnabled) return@LaunchedEffect
|
||||||
val currentUserId = currentSessionIdHolder.current
|
val currentUserId = room.sessionId
|
||||||
|
|
||||||
suspend fun canSendRoomMention(): Boolean {
|
suspend fun canSendRoomMention(): Boolean {
|
||||||
val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false)
|
val userCanSendAtRoom = room.canUserTriggerRoomNotification(currentUserId).getOrDefault(false)
|
||||||
|
|
@ -258,14 +256,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
loadDraft(markdownTextEditorState, richTextEditorState)
|
loadDraft(markdownTextEditorState, richTextEditorState)
|
||||||
}
|
}
|
||||||
|
|
||||||
val mentionSpanProvider = if (isTesting) {
|
val mentionSpanProvider = LocalMentionSpanProvider.current
|
||||||
null
|
|
||||||
} else {
|
|
||||||
rememberMentionSpanProvider(
|
|
||||||
currentUserId = room.sessionId,
|
|
||||||
permalinkParser = permalinkParser,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleEvents(event: MessageComposerEvents) {
|
fun handleEvents(event: MessageComposerEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
@ -379,19 +370,17 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
richTextEditorState.insertAtRoomMentionAtSuggestion()
|
richTextEditorState.insertAtRoomMentionAtSuggestion()
|
||||||
}
|
}
|
||||||
is ResolvedMentionSuggestion.Member -> {
|
is ResolvedMentionSuggestion.Member -> {
|
||||||
val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value
|
val text = mention.roomMember.userId.value
|
||||||
val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
|
val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
|
||||||
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (markdownTextEditorState.currentMentionSuggestion != null) {
|
} else if (markdownTextEditorState.currentMentionSuggestion != null) {
|
||||||
mentionSpanProvider?.let {
|
markdownTextEditorState.insertMention(
|
||||||
markdownTextEditorState.insertMention(
|
mention = event.mention,
|
||||||
mention = event.mention,
|
mentionSpanProvider = mentionSpanProvider,
|
||||||
mentionSpanProvider = it,
|
permalinkBuilder = permalinkBuilder,
|
||||||
permalinkBuilder = permalinkBuilder,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
suggestionSearchTrigger.value = null
|
suggestionSearchTrigger.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -413,7 +402,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
canCreatePoll = canCreatePoll.value,
|
canCreatePoll = canCreatePoll.value,
|
||||||
attachmentsState = attachmentsState.value,
|
attachmentsState = attachmentsState.value,
|
||||||
memberSuggestions = memberSuggestions.toPersistentList(),
|
memberSuggestions = memberSuggestions.toPersistentList(),
|
||||||
currentUserId = currentSessionIdHolder.current,
|
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ package io.element.android.features.messages.impl.messagecomposer
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
|
|
@ -38,7 +37,6 @@ data class MessageComposerState(
|
||||||
val canCreatePoll: Boolean,
|
val canCreatePoll: Boolean,
|
||||||
val attachmentsState: AttachmentsState,
|
val attachmentsState: AttachmentsState,
|
||||||
val memberSuggestions: ImmutableList<ResolvedMentionSuggestion>,
|
val memberSuggestions: ImmutableList<ResolvedMentionSuggestion>,
|
||||||
val currentUserId: UserId,
|
|
||||||
val eventSink: (MessageComposerEvents) -> Unit,
|
val eventSink: (MessageComposerEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.messages.impl.messagecomposer
|
package io.element.android.features.messages.impl.messagecomposer
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||||
import io.element.android.libraries.textcomposer.aRichTextEditorState
|
import io.element.android.libraries.textcomposer.aRichTextEditorState
|
||||||
|
|
@ -57,6 +56,5 @@ fun aMessageComposerState(
|
||||||
canCreatePoll = canCreatePoll,
|
canCreatePoll = canCreatePoll,
|
||||||
attachmentsState = attachmentsState,
|
attachmentsState = attachmentsState,
|
||||||
memberSuggestions = memberSuggestions,
|
memberSuggestions = memberSuggestions,
|
||||||
currentUserId = UserId("@alice:localhost"),
|
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,6 @@ internal fun MessageComposerView(
|
||||||
onReceiveSuggestion = ::onSuggestionReceived,
|
onReceiveSuggestion = ::onSuggestionReceived,
|
||||||
onError = ::onError,
|
onError = ::onError,
|
||||||
onTyping = ::onTyping,
|
onTyping = ::onTyping,
|
||||||
currentUserId = state.currentUserId,
|
|
||||||
onSelectRichContent = ::sendUri,
|
onSelectRichContent = ::sendUri,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,8 @@ import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
|
||||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||||
import io.element.android.libraries.textcomposer.mentions.rememberMentionSpanProvider
|
import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanProvider
|
||||||
import io.element.android.wysiwyg.compose.StyledHtmlConverter
|
import io.element.android.wysiwyg.compose.StyledHtmlConverter
|
||||||
import io.element.android.wysiwyg.display.MentionDisplayHandler
|
import io.element.android.wysiwyg.display.MentionDisplayHandler
|
||||||
import io.element.android.wysiwyg.display.TextDisplay
|
import io.element.android.wysiwyg.display.TextDisplay
|
||||||
|
|
@ -40,9 +39,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
@SingleIn(SessionScope::class)
|
@SingleIn(SessionScope::class)
|
||||||
class DefaultHtmlConverterProvider @Inject constructor(
|
class DefaultHtmlConverterProvider @Inject constructor() : HtmlConverterProvider {
|
||||||
private val permalinkParser: PermalinkParser,
|
|
||||||
) : HtmlConverterProvider {
|
|
||||||
private val htmlConverter: MutableState<HtmlConverter?> = mutableStateOf(null)
|
private val htmlConverter: MutableState<HtmlConverter?> = mutableStateOf(null)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -53,10 +50,7 @@ class DefaultHtmlConverterProvider @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val editorStyle = ElementRichTextEditorStyle.textStyle()
|
val editorStyle = ElementRichTextEditorStyle.textStyle()
|
||||||
val mentionSpanProvider = rememberMentionSpanProvider(
|
val mentionSpanProvider = LocalMentionSpanProvider.current
|
||||||
currentUserId = currentUserId,
|
|
||||||
permalinkParser = permalinkParser,
|
|
||||||
)
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
||||||
isMine = false,
|
isMine = false,
|
||||||
content = content,
|
content = content,
|
||||||
groupPosition = TimelineItemGroupPosition.Middle,
|
groupPosition = TimelineItemGroupPosition.Middle,
|
||||||
sendState = LocalEventSendState.SendingFailed("Message failed to send"),
|
sendState = LocalEventSendState.SendingFailed.Unrecoverable("Message failed to send"),
|
||||||
),
|
),
|
||||||
aTimelineItemEvent(
|
aTimelineItemEvent(
|
||||||
isMine = false,
|
isMine = false,
|
||||||
|
|
@ -104,7 +104,7 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = content,
|
content = content,
|
||||||
groupPosition = TimelineItemGroupPosition.Middle,
|
groupPosition = TimelineItemGroupPosition.Middle,
|
||||||
sendState = LocalEventSendState.SendingFailed("Message failed to send"),
|
sendState = LocalEventSendState.SendingFailed.Unrecoverable("Message failed to send"),
|
||||||
),
|
),
|
||||||
aTimelineItemEvent(
|
aTimelineItemEvent(
|
||||||
isMine = true,
|
isMine = true,
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,13 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.CompositingStrategy
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
|
@ -40,8 +46,10 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
|
||||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
||||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleStateProvider
|
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleStateProvider
|
||||||
import io.element.android.libraries.core.extensions.to01
|
import io.element.android.libraries.core.extensions.to01
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.text.toPx
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.messageFromMeBackground
|
import io.element.android.libraries.designsystem.theme.messageFromMeBackground
|
||||||
|
|
@ -51,6 +59,7 @@ import io.element.android.libraries.testtags.testTag
|
||||||
|
|
||||||
private val BUBBLE_RADIUS = 12.dp
|
private val BUBBLE_RADIUS = 12.dp
|
||||||
internal val BUBBLE_INCOMING_OFFSET = 16.dp
|
internal val BUBBLE_INCOMING_OFFSET = 16.dp
|
||||||
|
private val avatarRadius = AvatarSize.TimelineSender.dp / 2
|
||||||
|
|
||||||
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 85% now.
|
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 85% now.
|
||||||
private const val BUBBLE_WIDTH_RATIO = 0.85f
|
private const val BUBBLE_WIDTH_RATIO = 0.85f
|
||||||
|
|
@ -66,11 +75,12 @@ fun MessageEventBubble(
|
||||||
content: @Composable () -> Unit = {},
|
content: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
fun bubbleShape(): Shape {
|
fun bubbleShape(): Shape {
|
||||||
|
val topLeftCorner = if (state.cutTopStart) 0.dp else BUBBLE_RADIUS
|
||||||
return when (state.groupPosition) {
|
return when (state.groupPosition) {
|
||||||
TimelineItemGroupPosition.First -> if (state.isMine) {
|
TimelineItemGroupPosition.First -> if (state.isMine) {
|
||||||
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS)
|
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS)
|
||||||
} else {
|
} else {
|
||||||
RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp)
|
RoundedCornerShape(topLeftCorner, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp)
|
||||||
}
|
}
|
||||||
TimelineItemGroupPosition.Middle -> if (state.isMine) {
|
TimelineItemGroupPosition.Middle -> if (state.isMine) {
|
||||||
RoundedCornerShape(BUBBLE_RADIUS, 0.dp, 0.dp, BUBBLE_RADIUS)
|
RoundedCornerShape(BUBBLE_RADIUS, 0.dp, 0.dp, BUBBLE_RADIUS)
|
||||||
|
|
@ -84,7 +94,7 @@ fun MessageEventBubble(
|
||||||
}
|
}
|
||||||
TimelineItemGroupPosition.None ->
|
TimelineItemGroupPosition.None ->
|
||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
BUBBLE_RADIUS,
|
topLeftCorner,
|
||||||
BUBBLE_RADIUS,
|
BUBBLE_RADIUS,
|
||||||
BUBBLE_RADIUS,
|
BUBBLE_RADIUS,
|
||||||
BUBBLE_RADIUS
|
BUBBLE_RADIUS
|
||||||
|
|
@ -106,11 +116,30 @@ fun MessageEventBubble(
|
||||||
else -> ElementTheme.colors.messageFromOtherBackground
|
else -> ElementTheme.colors.messageFromOtherBackground
|
||||||
}
|
}
|
||||||
val bubbleShape = bubbleShape()
|
val bubbleShape = bubbleShape()
|
||||||
|
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
||||||
|
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
|
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(start = avatarRadius, end = 16.dp)
|
||||||
.offsetForItem(),
|
.offsetForItem()
|
||||||
|
.graphicsLayer {
|
||||||
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
|
}
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
if (state.cutTopStart) {
|
||||||
|
drawCircle(
|
||||||
|
color = Color.Black,
|
||||||
|
center = Offset(
|
||||||
|
x = 0f,
|
||||||
|
y = yOffsetPx,
|
||||||
|
),
|
||||||
|
radius = radiusPx,
|
||||||
|
blendMode = BlendMode.Clear,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
// Need to set the contentAlignment again (it's already set in TimelineItemEventRow), for the case
|
// Need to set the contentAlignment again (it's already set in TimelineItemEventRow), for the case
|
||||||
// when content width is low.
|
// when content width is low.
|
||||||
contentAlignment = if (state.isMine) Alignment.CenterEnd else Alignment.CenterStart
|
contentAlignment = if (state.isMine) Alignment.CenterEnd else Alignment.CenterStart
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -29,45 +30,60 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.isEdited
|
import io.element.android.features.messages.impl.timeline.model.event.isEdited
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineEventTimestampView(
|
fun TimelineEventTimestampView(
|
||||||
formattedTime: String,
|
event: TimelineItem.Event,
|
||||||
isMessageEdited: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val formattedTime = event.sentTime
|
||||||
|
val hasUnrecoverableError = event.localSendState is LocalEventSendState.SendingFailed.Unrecoverable
|
||||||
|
val isMessageEdited = event.content.isEdited()
|
||||||
|
val tint = if (hasUnrecoverableError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (isMessageEdited) {
|
if (isMessageEdited) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(CommonStrings.common_edited_suffix),
|
stringResource(CommonStrings.common_edited_suffix),
|
||||||
style = ElementTheme.typography.fontBodyXsRegular,
|
style = ElementTheme.typography.fontBodyXsRegular,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = tint,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
formattedTime,
|
formattedTime,
|
||||||
style = ElementTheme.typography.fontBodyXsRegular,
|
style = ElementTheme.typography.fontBodyXsRegular,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = tint,
|
||||||
)
|
)
|
||||||
|
if (hasUnrecoverableError) {
|
||||||
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = CompoundIcons.Error(),
|
||||||
|
contentDescription = stringResource(id = CommonStrings.common_sending_failed),
|
||||||
|
tint = tint,
|
||||||
|
modifier = Modifier.size(15.dp, 18.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview {
|
internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview {
|
||||||
TimelineEventTimestampView(formattedTime = event.sentTime, isMessageEdited = event.content.isEdited())
|
TimelineEventTimestampView(event = event)
|
||||||
}
|
}
|
||||||
|
|
||||||
object TimelineEventTimestampViewDefaults {
|
object TimelineEventTimestampViewDefaults {
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,15 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<Timel
|
||||||
override val values: Sequence<TimelineItem.Event>
|
override val values: Sequence<TimelineItem.Event>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aTimelineItemEvent(),
|
aTimelineItemEvent(),
|
||||||
// Sending failed
|
// Sending failed recoverable
|
||||||
aTimelineItemEvent().copy(localSendState = LocalEventSendState.SendingFailed("AN_ERROR")),
|
aTimelineItemEvent().copy(localSendState = LocalEventSendState.SendingFailed.Recoverable("AN_ERROR")),
|
||||||
|
// Sending failed unrecoverable
|
||||||
|
aTimelineItemEvent().copy(localSendState = LocalEventSendState.SendingFailed.Unrecoverable("AN_ERROR")),
|
||||||
// Edited
|
// Edited
|
||||||
aTimelineItemEvent().copy(content = aTimelineItemTextContent().copy(isEdited = true)),
|
aTimelineItemEvent().copy(content = aTimelineItemTextContent().copy(isEdited = true)),
|
||||||
// Sending failed + Edited (not sure this is possible IRL, but should be covered by test)
|
// Sending failed + Edited (not sure this is possible IRL, but should be covered by test)
|
||||||
aTimelineItemEvent().copy(
|
aTimelineItemEvent().copy(
|
||||||
localSendState = LocalEventSendState.SendingFailed("AN_ERROR"),
|
localSendState = LocalEventSendState.SendingFailed.Unrecoverable("AN_ERROR"),
|
||||||
content = aTimelineItemTextContent().copy(isEdited = true),
|
content = aTimelineItemTextContent().copy(isEdited = true),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
|
@ -32,11 +31,9 @@ import androidx.compose.foundation.layout.absoluteOffset
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -45,8 +42,6 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
import androidx.compose.ui.platform.ViewConfiguration
|
import androidx.compose.ui.platform.ViewConfiguration
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -55,7 +50,6 @@ import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.invisibleToUser
|
import androidx.compose.ui.semantics.invisibleToUser
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.testTag
|
import androidx.compose.ui.semantics.testTag
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -84,7 +78,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.isEdited
|
|
||||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
|
@ -110,6 +103,13 @@ import kotlinx.coroutines.launch
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
// The bubble has a negative margin to be placed a bit upper regarding the sender
|
||||||
|
// information and overlap the avatar.
|
||||||
|
val NEGATIVE_MARGIN_FOR_BUBBLE = (-8).dp
|
||||||
|
|
||||||
|
// Width of the transparent border around the sender avatar
|
||||||
|
val SENDER_AVATAR_BORDER_WIDTH = 3.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemEventRow(
|
fun TimelineItemEventRow(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
|
|
@ -281,13 +281,11 @@ private fun TimelineItemEventRowContent(
|
||||||
) = createRefs()
|
) = createRefs()
|
||||||
|
|
||||||
// Sender
|
// Sender
|
||||||
val avatarStrokeSize = 3.dp
|
|
||||||
if (event.showSenderInformation && !timelineRoomInfo.isDm) {
|
if (event.showSenderInformation && !timelineRoomInfo.isDm) {
|
||||||
MessageSenderInformation(
|
MessageSenderInformation(
|
||||||
event.senderId,
|
event.senderId,
|
||||||
event.senderProfile,
|
event.senderProfile,
|
||||||
event.senderAvatar,
|
event.senderAvatar,
|
||||||
avatarStrokeSize,
|
|
||||||
Modifier
|
Modifier
|
||||||
.constrainAs(sender) {
|
.constrainAs(sender) {
|
||||||
top.linkTo(parent.top)
|
top.linkTo(parent.top)
|
||||||
|
|
@ -313,7 +311,7 @@ private fun TimelineItemEventRowContent(
|
||||||
MessageEventBubble(
|
MessageEventBubble(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.constrainAs(message) {
|
.constrainAs(message) {
|
||||||
top.linkTo(sender.bottom, margin = -avatarStrokeSize - 8.dp)
|
top.linkTo(sender.bottom, margin = NEGATIVE_MARGIN_FOR_BUBBLE)
|
||||||
this.linkStartOrEnd(event)
|
this.linkStartOrEnd(event)
|
||||||
},
|
},
|
||||||
state = bubbleState,
|
state = bubbleState,
|
||||||
|
|
@ -365,37 +363,17 @@ private fun MessageSenderInformation(
|
||||||
senderId: UserId,
|
senderId: UserId,
|
||||||
senderProfile: ProfileTimelineDetails,
|
senderProfile: ProfileTimelineDetails,
|
||||||
senderAvatar: AvatarData,
|
senderAvatar: AvatarData,
|
||||||
avatarStrokeSize: Dp,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val avatarStrokeColor = MaterialTheme.colorScheme.background
|
|
||||||
val avatarSize = senderAvatar.size.dp
|
|
||||||
val avatarColors = AvatarColorsProvider.provide(senderAvatar.id, ElementTheme.isLightTheme)
|
val avatarColors = AvatarColorsProvider.provide(senderAvatar.id, ElementTheme.isLightTheme)
|
||||||
Box(
|
Row(modifier = modifier) {
|
||||||
modifier = modifier
|
Avatar(senderAvatar)
|
||||||
) {
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
// Background of Avatar, to erase the corner of the message content
|
SenderName(
|
||||||
Canvas(
|
senderId = senderId,
|
||||||
modifier = Modifier
|
senderProfile = senderProfile,
|
||||||
.size(size = avatarSize + avatarStrokeSize)
|
senderNameMode = SenderNameMode.Timeline(avatarColors.foreground),
|
||||||
.clipToBounds()
|
)
|
||||||
) {
|
|
||||||
drawCircle(
|
|
||||||
color = avatarStrokeColor,
|
|
||||||
center = Offset(x = (avatarSize / 2).toPx(), y = (avatarSize / 2).toPx()),
|
|
||||||
radius = (avatarSize / 2 + avatarStrokeSize).toPx()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Content
|
|
||||||
Row {
|
|
||||||
Avatar(senderAvatar)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
SenderName(
|
|
||||||
senderId = senderId,
|
|
||||||
senderProfile = senderProfile,
|
|
||||||
senderNameMode = SenderNameMode.Timeline(avatarColors.foreground),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -451,8 +429,7 @@ private fun MessageEventBubbleContent(
|
||||||
Box(modifier, contentAlignment = Alignment.Center) {
|
Box(modifier, contentAlignment = Alignment.Center) {
|
||||||
content {}
|
content {}
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
formattedTime = event.sentTime,
|
event = event,
|
||||||
isMessageEdited = event.content.isEdited(),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// Outer padding
|
// Outer padding
|
||||||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
.padding(horizontal = 4.dp, vertical = 4.dp)
|
||||||
|
|
@ -472,8 +449,7 @@ private fun MessageEventBubbleContent(
|
||||||
content = { content(this::onContentLayoutChange) },
|
content = { content(this::onContentLayoutChange) },
|
||||||
overlay = {
|
overlay = {
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
formattedTime = event.sentTime,
|
event = event,
|
||||||
isMessageEdited = event.content.isEdited(),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -483,8 +459,7 @@ private fun MessageEventBubbleContent(
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
content {}
|
content {}
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
formattedTime = event.sentTime,
|
event = event,
|
||||||
isMessageEdited = event.content.isEdited(),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,15 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components.event
|
package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
|
@ -33,7 +37,12 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentProvider
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
||||||
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
|
||||||
|
import io.element.android.libraries.textcomposer.mentions.MentionSpan
|
||||||
|
import io.element.android.libraries.textcomposer.mentions.getMentionSpans
|
||||||
import io.element.android.wysiwyg.compose.EditorStyledText
|
import io.element.android.wysiwyg.compose.EditorStyledText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -47,10 +56,8 @@ fun TimelineItemTextView(
|
||||||
LocalContentColor provides ElementTheme.colors.textPrimary,
|
LocalContentColor provides ElementTheme.colors.textPrimary,
|
||||||
LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular
|
LocalTextStyle provides ElementTheme.typography.fontBodyLgRegular
|
||||||
) {
|
) {
|
||||||
val formattedBody = content.formattedBody
|
val body = getTextWithResolvedMentions(content)
|
||||||
val body = SpannableString(formattedBody ?: content.body)
|
Box(modifier.semantics { contentDescription = content.plainText }) {
|
||||||
|
|
||||||
Box(modifier.semantics { contentDescription = body.toString() }) {
|
|
||||||
EditorStyledText(
|
EditorStyledText(
|
||||||
text = body,
|
text = body,
|
||||||
onLinkClickedListener = onLinkClick,
|
onLinkClickedListener = onLinkClick,
|
||||||
|
|
@ -62,6 +69,39 @@ fun TimelineItemTextView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
@Composable
|
||||||
|
internal fun getTextWithResolvedMentions(content: TimelineItemTextBasedContent): CharSequence {
|
||||||
|
val userProfileCache = LocalRoomMemberProfilesCache.current
|
||||||
|
val lastCacheUpdate by userProfileCache.lastCacheUpdate.collectAsState()
|
||||||
|
val formattedBody = remember(content.htmlBody, lastCacheUpdate) {
|
||||||
|
updateMentionSpans(content.formattedBody, userProfileCache)
|
||||||
|
SpannableString(content.formattedBody ?: content.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedBody
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMentionSpans(text: CharSequence?, cache: RoomMemberProfilesCache): Boolean {
|
||||||
|
var changedContents = false
|
||||||
|
if (text != null) {
|
||||||
|
for (mentionSpan in text.getMentionSpans()) {
|
||||||
|
when (mentionSpan.type) {
|
||||||
|
MentionSpan.Type.USER -> {
|
||||||
|
val displayName = cache.getDisplayName(UserId(mentionSpan.rawValue)) ?: mentionSpan.rawValue
|
||||||
|
if (mentionSpan.text != displayName) {
|
||||||
|
changedContents = true
|
||||||
|
mentionSpan.text = displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nothing yet for room mentions
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changedContents
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineItemTextViewPreview(
|
internal fun TimelineItemTextViewPreview(
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,8 @@ fun TimelineItemReadReceiptView(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
when (state.sendState) {
|
when (state.sendState) {
|
||||||
is LocalEventSendState.SendingFailed,
|
LocalEventSendState.NotSentYet,
|
||||||
is LocalEventSendState.NotSentYet -> {
|
is LocalEventSendState.SendingFailed.Recoverable -> {
|
||||||
ReadReceiptsRow(modifier) {
|
ReadReceiptsRow(modifier) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.padding(2.dp),
|
modifier = Modifier.padding(2.dp),
|
||||||
|
|
@ -92,6 +92,9 @@ fun TimelineItemReadReceiptView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is LocalEventSendState.SendingFailed.Unrecoverable -> {
|
||||||
|
// Error? The timestamp is already displayed in red
|
||||||
|
}
|
||||||
null,
|
null,
|
||||||
is LocalEventSendState.Sent -> {
|
is LocalEventSendState.Sent -> {
|
||||||
if (state.isLastOutgoingMessage) {
|
if (state.isLastOutgoingMessage) {
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,7 @@ data class BubbleState(
|
||||||
val isMine: Boolean,
|
val isMine: Boolean,
|
||||||
val isHighlighted: Boolean,
|
val isHighlighted: Boolean,
|
||||||
val timelineRoomInfo: TimelineRoomInfo,
|
val timelineRoomInfo: TimelineRoomInfo,
|
||||||
)
|
) {
|
||||||
|
/** True to cut out the top start corner of the bubble, to give margin for the sender avatar. */
|
||||||
|
val cutTopStart: Boolean = groupPosition.isNew() && !isMine && !timelineRoomInfo.isDm
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,13 +70,11 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.MessageEventType
|
import io.element.android.libraries.matrix.api.room.MessageEventType
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
|
|
@ -780,10 +778,9 @@ class MessagesPresenterTest {
|
||||||
messageComposerContext = DefaultMessageComposerContext(),
|
messageComposerContext = DefaultMessageComposerContext(),
|
||||||
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
||||||
permissionsPresenterFactory = permissionsPresenterFactory,
|
permissionsPresenterFactory = permissionsPresenterFactory,
|
||||||
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
permalinkParser = FakePermalinkParser(),
|
||||||
permalinkBuilder = FakePermalinkBuilder(),
|
permalinkBuilder = FakePermalinkBuilder(),
|
||||||
timelineController = TimelineController(matrixRoom),
|
timelineController = TimelineController(matrixRoom),
|
||||||
permalinkParser = permalinkParser,
|
|
||||||
draftService = FakeComposerDraftService(),
|
draftService = FakeComposerDraftService(),
|
||||||
).apply {
|
).apply {
|
||||||
showTextFormatting = true
|
showTextFormatting = true
|
||||||
|
|
|
||||||
|
|
@ -470,7 +470,9 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
// Cannot use the RichTextEditor, so simulate a LocalInspectionMode
|
// Cannot use the RichTextEditor, so simulate a LocalInspectionMode
|
||||||
CompositionLocalProvider(LocalInspectionMode provides true) {
|
CompositionLocalProvider(
|
||||||
|
LocalInspectionMode provides true
|
||||||
|
) {
|
||||||
MessagesView(
|
MessagesView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
|
|
|
||||||
|
|
@ -615,7 +615,6 @@ class ActionListPresenterTest {
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
TimelineItemAction.Edit,
|
TimelineItemAction.Edit,
|
||||||
TimelineItemAction.Copy,
|
TimelineItemAction.Copy,
|
||||||
TimelineItemAction.CopyLink,
|
|
||||||
TimelineItemAction.Redact,
|
TimelineItemAction.Redact,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -53,19 +53,16 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
|
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
|
||||||
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
|
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
|
|
||||||
import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
|
import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.A_REPLY
|
import io.element.android.libraries.matrix.test.A_REPLY
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
|
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
|
@ -1290,7 +1287,6 @@ class MessageComposerPresenterTest {
|
||||||
analyticsService,
|
analyticsService,
|
||||||
DefaultMessageComposerContext(),
|
DefaultMessageComposerContext(),
|
||||||
TestRichTextEditorStateFactory(),
|
TestRichTextEditorStateFactory(),
|
||||||
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
|
||||||
permalinkParser = FakePermalinkParser(),
|
permalinkParser = FakePermalinkParser(),
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
@ -33,9 +32,7 @@ class DefaultHtmlConverterProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calling provide without calling Update first should throw an exception`() {
|
fun `calling provide without calling Update first should throw an exception`() {
|
||||||
val provider = DefaultHtmlConverterProvider(
|
val provider = DefaultHtmlConverterProvider()
|
||||||
permalinkParser = FakePermalinkParser(),
|
|
||||||
)
|
|
||||||
|
|
||||||
val exception = runCatching { provider.provide() }.exceptionOrNull()
|
val exception = runCatching { provider.provide() }.exceptionOrNull()
|
||||||
|
|
||||||
|
|
@ -44,9 +41,7 @@ class DefaultHtmlConverterProviderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `calling provide after calling Update first should return an HtmlConverter`() {
|
fun `calling provide after calling Update first should return an HtmlConverter`() {
|
||||||
val provider = DefaultHtmlConverterProvider(
|
val provider = DefaultHtmlConverterProvider()
|
||||||
permalinkParser = FakePermalinkParser(),
|
|
||||||
)
|
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
CompositionLocalProvider(LocalInspectionMode provides true) {
|
CompositionLocalProvider(LocalInspectionMode provides true) {
|
||||||
provider.Update(currentUserId = A_USER_ID)
|
provider.Update(currentUserId = A_USER_ID)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
|
import android.text.SpannableString
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.text.inSpans
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache
|
||||||
|
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
||||||
|
import io.element.android.libraries.textcomposer.mentions.MentionSpan
|
||||||
|
import io.element.android.libraries.textcomposer.mentions.getMentionSpans
|
||||||
|
import io.element.android.wysiwyg.view.spans.CustomMentionSpan
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class TimelineTextViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getTextWithResolvedMentions - does nothing for a non spannable CharSequence`() = runTest {
|
||||||
|
val charSequence = "Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>"
|
||||||
|
|
||||||
|
val result = rule.getText(aTextContentWithFormattedBody(charSequence))
|
||||||
|
|
||||||
|
assertThat(result.getMentionSpans()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runTest {
|
||||||
|
val charSequence = SpannableString("Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>")
|
||||||
|
|
||||||
|
val result = rule.getText(aTextContentWithFormattedBody(charSequence))
|
||||||
|
|
||||||
|
assertThat(result.getMentionSpans()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runTest {
|
||||||
|
val charSequence = "Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>"
|
||||||
|
|
||||||
|
val result = rule.getText(aTextContentWithFormattedBody(body = charSequence, formattedBody = null))
|
||||||
|
|
||||||
|
assertThat(result.getMentionSpans()).isEmpty()
|
||||||
|
assertThat(result.toString()).isEqualTo(charSequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getTextWithResolvedMentions - with Room mention does nothing`() = runTest {
|
||||||
|
val charSequence = buildSpannedString {
|
||||||
|
append("Hello ")
|
||||||
|
inSpans(aMentionSpan(rawValue = A_ROOM_ID_2.value, type = MentionSpan.Type.ROOM)) {
|
||||||
|
append(A_ROOM_ID.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = rule.getText(aTextContentWithFormattedBody(charSequence))
|
||||||
|
|
||||||
|
assertThat(result.getMentionSpans().firstOrNull()?.text).isEmpty()
|
||||||
|
assertThat(result).isEqualTo(charSequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runTest {
|
||||||
|
val charSequence = buildSpannedString {
|
||||||
|
append("Hello ")
|
||||||
|
inSpans(aMentionSpan(rawValue = A_USER_ID.value)) {
|
||||||
|
append("@NotAlice")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = rule.getText(aTextContentWithFormattedBody(charSequence))
|
||||||
|
|
||||||
|
assertThat(result.getMentionSpans().firstOrNull()?.text).isEqualTo("alice")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runTest {
|
||||||
|
val charSequence = buildSpannedString {
|
||||||
|
append("Hello ")
|
||||||
|
inSpans(CustomMentionSpan(aMentionSpan(rawValue = A_USER_ID.value))) {
|
||||||
|
append("@NotAlice")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = rule.getText(aTextContentWithFormattedBody(charSequence))
|
||||||
|
|
||||||
|
assertThat(result.getMentionSpans().firstOrNull()?.text).isEqualTo("alice")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getTextWithResolvedMentions - replaces MentionSpan's text with user id if no display name is cached`() = runTest {
|
||||||
|
val charSequence = buildSpannedString {
|
||||||
|
append("Hello ")
|
||||||
|
inSpans(aMentionSpan(rawValue = A_USER_ID_2.value)) {
|
||||||
|
append("@NotAlice")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = rule.getText(aTextContentWithFormattedBody(charSequence))
|
||||||
|
|
||||||
|
assertThat(result.getMentionSpans().firstOrNull()?.text).isEqualTo(A_USER_ID_2.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.getText(
|
||||||
|
content: TimelineItemTextBasedContent,
|
||||||
|
): CharSequence {
|
||||||
|
val completable = CompletableDeferred<CharSequence>()
|
||||||
|
setContent {
|
||||||
|
val roomMemberProfilesCache = RoomMemberProfilesCache().apply {
|
||||||
|
replace(listOf(aRoomMember(userId = A_USER_ID, displayName = A_USER_NAME)))
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalRoomMemberProfilesCache provides roomMemberProfilesCache,
|
||||||
|
) {
|
||||||
|
completable.complete(getTextWithResolvedMentions(content = content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return completable.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aMentionSpan(
|
||||||
|
rawValue: String,
|
||||||
|
text: String = "",
|
||||||
|
type: MentionSpan.Type = MentionSpan.Type.USER
|
||||||
|
) = MentionSpan(
|
||||||
|
text = text,
|
||||||
|
rawValue = rawValue,
|
||||||
|
type = type,
|
||||||
|
backgroundColor = 0,
|
||||||
|
textColor = 0,
|
||||||
|
startPadding = 0,
|
||||||
|
endPadding = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun aTextContentWithFormattedBody(formattedBody: CharSequence?, body: String = "") =
|
||||||
|
TimelineItemTextContent(
|
||||||
|
body = body,
|
||||||
|
htmlDocument = null,
|
||||||
|
formattedBody = formattedBody,
|
||||||
|
isEdited = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -25,9 +25,8 @@ import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
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.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
|
|
@ -37,7 +36,9 @@ import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view that allows a user to edit the default notification setting for rooms. This can be set separately
|
* A view that allows a user to edit the default notification setting for rooms. This can be set separately
|
||||||
|
|
@ -96,12 +97,6 @@ fun EditDefaultNotificationSettingView(
|
||||||
RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute)
|
RoomNotificationMode.MUTE -> stringResource(id = CommonStrings.common_mute)
|
||||||
null -> ""
|
null -> ""
|
||||||
}
|
}
|
||||||
val avatarData = AvatarData(
|
|
||||||
id = summary.identifier(),
|
|
||||||
name = summary.details.name,
|
|
||||||
url = summary.details.avatarUrl,
|
|
||||||
size = AvatarSize.CustomRoomNotificationSetting,
|
|
||||||
)
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
val roomName = summary.details.name
|
val roomName = summary.details.name
|
||||||
|
|
@ -114,7 +109,12 @@ fun EditDefaultNotificationSettingView(
|
||||||
Text(text = subtitle)
|
Text(text = subtitle)
|
||||||
},
|
},
|
||||||
leadingContent = ListItemContent.Custom {
|
leadingContent = ListItemContent.Custom {
|
||||||
Avatar(avatarData = avatarData)
|
CompositeAvatar(
|
||||||
|
avatarData = summary.details.getAvatarData(size = AvatarSize.CustomRoomNotificationSetting),
|
||||||
|
heroes = summary.details.heroes.map { user ->
|
||||||
|
user.getAvatarData(size = AvatarSize.CustomRoomNotificationSetting)
|
||||||
|
}.toPersistentList()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
openRoomNotificationSettings(summary.details.roomId)
|
openRoomNotificationSettings(summary.details.roomId)
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ fun EditUserProfileView(
|
||||||
matrixId = state.userId.value,
|
matrixId = state.userId.value,
|
||||||
displayName = state.displayName,
|
displayName = state.displayName,
|
||||||
avatarUrl = state.userAvatarUrl,
|
avatarUrl = state.userAvatarUrl,
|
||||||
avatarSize = AvatarSize.RoomHeader,
|
avatarSize = AvatarSize.EditProfileDetails,
|
||||||
onAvatarClick = { onAvatarClick() },
|
onAvatarClick = { onAvatarClick() },
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="full_screen_intent_banner_message">"Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Палепшыце якасць званкоў"</string>
|
||||||
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Выберыце спосаб атрымання апавяшчэнняў"</string>
|
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Выберыце спосаб атрымання апавяшчэнняў"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode">"Рэжым распрацоўшчыка"</string>
|
<string name="screen_advanced_settings_developer_mode">"Рэжым распрацоўшчыка"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Падайце распрацоўнікам доступ да функцый і функцыянальным магчымасцям."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Падайце распрацоўнікам доступ да функцый і функцыянальным магчымасцям."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="full_screen_intent_banner_message">"Abyste nikdy nezmeškali důležitý hovor, změňte nastavení tak, abyste povolili oznámení na celé obrazovce, když je telefon uzamčen."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Vylepšete si zážitek z volání"</string>
|
||||||
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Vyberte, jak chcete přijímat oznámení"</string>
|
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Vyberte, jak chcete přijímat oznámení"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode">"Vývojářský režim"</string>
|
<string name="screen_advanced_settings_developer_mode">"Vývojářský režim"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Povolením získáte přístup k funkcím a funkcím pro vývojáře."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Povolením získáte přístup k funkcím a funkcím pro vývojáře."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="full_screen_intent_banner_message">"Afin de ne jamais manquer un appel important, veuillez modifier vos paramètres pour autoriser les notifications en plein écran lorsque votre appareil est verrouillé."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Améliorez votre expérience d’appel"</string>
|
||||||
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Choisissez le mode de réception des notifications"</string>
|
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Choisissez le mode de réception des notifications"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode">"Mode développeur"</string>
|
<string name="screen_advanced_settings_developer_mode">"Mode développeur"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="full_screen_intent_banner_message">"Pentru a vă asigura că nu pierdeți niciodată un apel important, vă rugăm să modificați setările pentru a permite notificări fullscreen atunci când telefonul este blocat."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Îmbunătățiți-vă experiența in timpul unui apel"</string>
|
||||||
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Alegeți modul de primire a notificărilor"</string>
|
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Alegeți modul de primire a notificărilor"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode">"Modul dezvoltator"</string>
|
<string name="screen_advanced_settings_developer_mode">"Modul dezvoltator"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Activați pentru a avea acces la funcționalități pentru dezvoltatori."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Activați pentru a avea acces la funcționalități pentru dezvoltatori."</string>
|
||||||
<string name="screen_advanced_settings_element_call_base_url">"Adresa URL de bază Element Call"</string>
|
<string name="screen_advanced_settings_element_call_base_url">"Adresa URL de bază Element Call"</string>
|
||||||
<string name="screen_advanced_settings_element_call_base_url_description">"Setați o adresă URL de bază personalizată pentru Element Call."</string>
|
<string name="screen_advanced_settings_element_call_base_url_description">"Setați o adresă URL de bază personalizată pentru Element Call."</string>
|
||||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL invalid, vă rugăm să vă asigurați că includeți protocolul (http/https) și adresa corectă."</string>
|
<string name="screen_advanced_settings_element_call_base_url_validation_error">"URL invalid, vă rugăm să vă asigurați că includeți protocolul (http/https) și adresa corectă."</string>
|
||||||
|
<string name="screen_advanced_settings_push_provider_android">"Furnizor de notificări push"</string>
|
||||||
<string name="screen_advanced_settings_rich_text_editor_description">"Dezactivați editorul avansat pentru a tasta manual Markdown."</string>
|
<string name="screen_advanced_settings_rich_text_editor_description">"Dezactivați editorul avansat pentru a tasta manual Markdown."</string>
|
||||||
<string name="screen_advanced_settings_send_read_receipts">"Chitanțe de citire"</string>
|
<string name="screen_advanced_settings_send_read_receipts">"Chitanțe de citire"</string>
|
||||||
<string name="screen_advanced_settings_send_read_receipts_description">"Dacă dezactivată, chitanțele dumneavoastră de citire nu vor fi trimise nimănui. Veți primi în continuare chitanțe de citire de la alți utilizatori."</string>
|
<string name="screen_advanced_settings_send_read_receipts_description">"Dacă dezactivată, chitanțele dumneavoastră de citire nu vor fi trimise nimănui. Veți primi în continuare chitanțe de citire de la alți utilizatori."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="full_screen_intent_banner_message">"Чтобы никогда не пропустить важный звонок, измените настройки, чтобы разрешить полноэкранные уведомления, когда ваш телефон заблокирован."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Улучшите качество звонков"</string>
|
||||||
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Выберите способ получения уведомлений"</string>
|
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Выберите способ получения уведомлений"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode">"Режим разработчика"</string>
|
<string name="screen_advanced_settings_developer_mode">"Режим разработчика"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Предоставьте разработчикам доступ к функциям и функциональным возможностям."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Предоставьте разработчикам доступ к функциям и функциональным возможностям."</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="full_screen_intent_banner_message">"Aby ste už nikdy nezmeškali dôležitý hovor, zmeňte svoje nastavenia a povoľte upozornenia na celú obrazovku, keď je váš telefón uzamknutý."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Vylepšite svoj zážitok z hovoru"</string>
|
||||||
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Vyberte spôsob prijímania oznámení"</string>
|
<string name="screen_advanced_settings_choose_distributor_dialog_title_android">"Vyberte spôsob prijímania oznámení"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode">"Vývojársky režim"</string>
|
<string name="screen_advanced_settings_developer_mode">"Vývojársky režim"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Umožniť prístup k možnostiam a funkciám pre vývojárov."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Umožniť prístup k možnostiam a funkciám pre vývojárov."</string>
|
||||||
|
|
|
||||||
|
|
@ -50,4 +50,6 @@ Om du fortsätter kan vissa av dina inställningar ändras."</string>
|
||||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"systeminställningar"</string>
|
<string name="screen_notification_settings_system_notifications_action_required_content_link">"systeminställningar"</string>
|
||||||
<string name="screen_notification_settings_system_notifications_turned_off">"Systemaviseringar avstängda"</string>
|
<string name="screen_notification_settings_system_notifications_turned_off">"Systemaviseringar avstängda"</string>
|
||||||
<string name="screen_notification_settings_title">"Aviseringar"</string>
|
<string name="screen_notification_settings_title">"Aviseringar"</string>
|
||||||
|
<string name="troubleshoot_notifications_entry_point_section">"Felsök"</string>
|
||||||
|
<string name="troubleshoot_notifications_entry_point_title">"Felsök aviseringar"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -282,11 +282,13 @@ class DefaultBugReporter @Inject constructor(
|
||||||
listener.onUploadFailed(serverError)
|
listener.onUploadFailed(serverError)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// delete the generated files when the bug report process has finished
|
withContext(coroutineDispatchers.io) {
|
||||||
for (file in bugReportFiles) {
|
// delete the generated files when the bug report process has finished
|
||||||
file.safeDelete()
|
for (file in bugReportFiles) {
|
||||||
|
file.safeDelete()
|
||||||
|
}
|
||||||
|
response?.close()
|
||||||
}
|
}
|
||||||
response?.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,12 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
|
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
|
||||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||||
import io.element.android.libraries.matrix.ui.room.canCall
|
import io.element.android.libraries.matrix.ui.room.canCall
|
||||||
|
import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember
|
||||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||||
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
|
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -97,8 +99,9 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
|
val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
|
||||||
val canJoinCall by room.canCall(updateKey = syncUpdateTimestamp)
|
val canJoinCall by room.canCall(updateKey = syncUpdateTimestamp)
|
||||||
val dmMember by room.getDirectRoomMember(membersState)
|
val dmMember by room.getDirectRoomMember(membersState)
|
||||||
|
val currentMember by room.getCurrentRoomMember(membersState)
|
||||||
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
|
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
|
||||||
val roomType by getRoomType(dmMember)
|
val roomType by getRoomType(dmMember, currentMember)
|
||||||
|
|
||||||
val topicState = remember(canEditTopic, roomTopic, roomType) {
|
val topicState = remember(canEditTopic, roomTopic, roomType) {
|
||||||
val topic = roomTopic
|
val topic = roomTopic
|
||||||
|
|
@ -151,6 +154,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
displayRolesAndPermissionsSettings = !room.isDm && isUserAdmin,
|
displayRolesAndPermissionsSettings = !room.isDm && isUserAdmin,
|
||||||
isPublic = isPublic,
|
isPublic = isPublic,
|
||||||
|
heroes = roomInfo?.heroes.orEmpty().toPersistentList(),
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -163,10 +167,16 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getRoomType(dmMember: RoomMember?): State<RoomDetailsType> = remember(dmMember) {
|
private fun getRoomType(
|
||||||
|
dmMember: RoomMember?,
|
||||||
|
currentMember: RoomMember?,
|
||||||
|
): State<RoomDetailsType> = remember(dmMember, currentMember) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (dmMember != null) {
|
if (dmMember != null && currentMember != null) {
|
||||||
RoomDetailsType.Dm(dmMember)
|
RoomDetailsType.Dm(
|
||||||
|
me = currentMember,
|
||||||
|
otherMember = dmMember,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
RoomDetailsType.Room
|
RoomDetailsType.Room
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class RoomDetailsState(
|
data class RoomDetailsState(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
|
|
@ -43,13 +45,17 @@ data class RoomDetailsState(
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
val displayRolesAndPermissionsSettings: Boolean,
|
val displayRolesAndPermissionsSettings: Boolean,
|
||||||
val isPublic: Boolean,
|
val isPublic: Boolean,
|
||||||
|
val heroes: ImmutableList<MatrixUser>,
|
||||||
val eventSink: (RoomDetailsEvent) -> Unit
|
val eventSink: (RoomDetailsEvent) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface RoomDetailsType {
|
sealed interface RoomDetailsType {
|
||||||
data object Room : RoomDetailsType
|
data object Room : RoomDetailsType
|
||||||
data class Dm(val roomMember: RoomMember) : RoomDetailsType
|
data class Dm(
|
||||||
|
val me: RoomMember,
|
||||||
|
val otherMember: RoomMember,
|
||||||
|
) : RoomDetailsType
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.roomdetails.impl
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||||
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
import io.element.android.features.userprofile.shared.UserProfileState
|
import io.element.android.features.userprofile.shared.UserProfileState
|
||||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
|
|
@ -28,6 +29,9 @@ import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
|
open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState> {
|
||||||
override val values: Sequence<RoomDetailsState>
|
override val values: Sequence<RoomDetailsState>
|
||||||
|
|
@ -48,6 +52,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
||||||
),
|
),
|
||||||
aRoomDetailsState(canCall = false, canInvite = false),
|
aRoomDetailsState(canCall = false, canInvite = false),
|
||||||
aRoomDetailsState(isPublic = false),
|
aRoomDetailsState(isPublic = false),
|
||||||
|
aRoomDetailsState(heroes = aMatrixUserList()),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +104,7 @@ fun aRoomDetailsState(
|
||||||
isFavorite: Boolean = false,
|
isFavorite: Boolean = false,
|
||||||
displayAdminSettings: Boolean = false,
|
displayAdminSettings: Boolean = false,
|
||||||
isPublic: Boolean = true,
|
isPublic: Boolean = true,
|
||||||
|
heroes: List<MatrixUser> = emptyList(),
|
||||||
eventSink: (RoomDetailsEvent) -> Unit = {},
|
eventSink: (RoomDetailsEvent) -> Unit = {},
|
||||||
) = RoomDetailsState(
|
) = RoomDetailsState(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
|
@ -119,6 +125,7 @@ fun aRoomDetailsState(
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
displayRolesAndPermissionsSettings = displayAdminSettings,
|
displayRolesAndPermissionsSettings = displayAdminSettings,
|
||||||
isPublic = isPublic,
|
isPublic = isPublic,
|
||||||
|
heroes = heroes.toPersistentList(),
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -135,6 +142,10 @@ fun aDmRoomDetailsState(
|
||||||
roomName: String = "Daniel",
|
roomName: String = "Daniel",
|
||||||
) = aRoomDetailsState(
|
) = aRoomDetailsState(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
roomType = RoomDetailsType.Dm(aDmRoomMember(isIgnored = isDmMemberIgnored)),
|
isPublic = false,
|
||||||
|
roomType = RoomDetailsType.Dm(
|
||||||
|
aRoomMember(),
|
||||||
|
aDmRoomMember(isIgnored = isDmMemberIgnored),
|
||||||
|
),
|
||||||
roomMemberDetailsState = aUserProfileState()
|
roomMemberDetailsState = aUserProfileState()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,13 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
|
@ -49,14 +49,14 @@ import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||||
import io.element.android.features.roomdetails.impl.components.RoomBadge
|
import io.element.android.features.roomdetails.impl.components.RoomBadge
|
||||||
import io.element.android.features.userprofile.shared.UserProfileHeaderSection
|
|
||||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
|
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
|
||||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
|
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
|
||||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||||
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.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.DmAvatars
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.button.MainActionButton
|
import io.element.android.libraries.designsystem.components.button.MainActionButton
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
|
|
@ -79,11 +79,16 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.room.getBestName
|
import io.element.android.libraries.matrix.api.room.getBestName
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomDetailsView(
|
fun RoomDetailsView(
|
||||||
|
|
@ -125,38 +130,35 @@ fun RoomDetailsView(
|
||||||
roomId = state.roomId,
|
roomId = state.roomId,
|
||||||
roomName = state.roomName,
|
roomName = state.roomName,
|
||||||
roomAlias = state.roomAlias,
|
roomAlias = state.roomAlias,
|
||||||
isEncrypted = state.isEncrypted,
|
heroes = state.heroes,
|
||||||
isPublic = state.isPublic,
|
|
||||||
openAvatarPreview = { avatarUrl ->
|
openAvatarPreview = { avatarUrl ->
|
||||||
openAvatarPreview(state.roomName, avatarUrl)
|
openAvatarPreview(state.roomName, avatarUrl)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
MainActionsSection(
|
|
||||||
state = state,
|
|
||||||
onShareRoom = onShareRoom,
|
|
||||||
onInvitePeople = invitePeople,
|
|
||||||
onCall = onJoinCallClick,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is RoomDetailsType.Dm -> {
|
is RoomDetailsType.Dm -> {
|
||||||
val member = state.roomType.roomMember
|
DmHeaderSection(
|
||||||
UserProfileHeaderSection(
|
me = state.roomType.me,
|
||||||
avatarUrl = state.roomAvatarUrl ?: member.avatarUrl,
|
otherMember = state.roomType.otherMember,
|
||||||
userId = member.userId,
|
roomName = state.roomName,
|
||||||
userName = state.roomName,
|
openAvatarPreview = { name, avatarUrl ->
|
||||||
openAvatarPreview = { avatarUrl ->
|
openAvatarPreview(name, avatarUrl)
|
||||||
openAvatarPreview(member.getBestName(), avatarUrl)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
MainActionsSection(
|
|
||||||
state = state,
|
|
||||||
onShareRoom = onShareRoom,
|
|
||||||
onInvitePeople = invitePeople,
|
|
||||||
onCall = onJoinCallClick,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BadgeList(
|
||||||
|
isEncrypted = state.isEncrypted,
|
||||||
|
isPublic = state.isPublic,
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
MainActionsSection(
|
||||||
|
state = state,
|
||||||
|
onShareRoom = onShareRoom,
|
||||||
|
onInvitePeople = invitePeople,
|
||||||
|
onCall = onJoinCallClick,
|
||||||
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
if (state.roomTopic !is RoomTopicState.Hidden) {
|
if (state.roomTopic !is RoomTopicState.Hidden) {
|
||||||
|
|
@ -322,8 +324,7 @@ private fun RoomHeaderSection(
|
||||||
roomId: RoomId,
|
roomId: RoomId,
|
||||||
roomName: String,
|
roomName: String,
|
||||||
roomAlias: RoomAlias?,
|
roomAlias: RoomAlias?,
|
||||||
isEncrypted: Boolean,
|
heroes: ImmutableList<MatrixUser>,
|
||||||
isPublic: Boolean,
|
|
||||||
openAvatarPreview: (url: String) -> Unit,
|
openAvatarPreview: (url: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -332,30 +333,65 @@ private fun RoomHeaderSection(
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Avatar(
|
CompositeAvatar(
|
||||||
avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader),
|
avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader),
|
||||||
|
heroes = heroes.map { user ->
|
||||||
|
user.getAvatarData(size = AvatarSize.RoomHeader)
|
||||||
|
}.toPersistentList(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(70.dp)
|
|
||||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||||
.testTag(TestTags.roomDetailAvatar)
|
.testTag(TestTags.roomDetailAvatar)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
TitleAndSubtitle(title = roomName, subtitle = roomAlias?.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DmHeaderSection(
|
||||||
|
me: RoomMember,
|
||||||
|
otherMember: RoomMember,
|
||||||
|
roomName: String,
|
||||||
|
openAvatarPreview: (name: String, url: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
DmAvatars(
|
||||||
|
userAvatarData = me.getAvatarData(size = AvatarSize.DmCluster),
|
||||||
|
otherUserAvatarData = otherMember.getAvatarData(size = AvatarSize.DmCluster),
|
||||||
|
openAvatarPreview = { url -> openAvatarPreview(me.getBestName(), url) },
|
||||||
|
openOtherAvatarPreview = { url -> openAvatarPreview(roomName, url) },
|
||||||
|
)
|
||||||
|
TitleAndSubtitle(
|
||||||
|
title = roomName,
|
||||||
|
subtitle = otherMember.userId.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColumnScope.TitleAndSubtitle(
|
||||||
|
title: String,
|
||||||
|
subtitle: String?,
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = ElementTheme.typography.fontHeadingLgBold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
if (subtitle != null) {
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Text(
|
Text(
|
||||||
text = roomName,
|
text = subtitle,
|
||||||
style = ElementTheme.typography.fontHeadingLgBold,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
if (roomAlias != null) {
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
Text(
|
|
||||||
text = roomAlias.value,
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
BadgeList(isEncrypted = isEncrypted, isPublic = isPublic)
|
|
||||||
Spacer(Modifier.height(32.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,11 +399,12 @@ private fun RoomHeaderSection(
|
||||||
private fun BadgeList(
|
private fun BadgeList(
|
||||||
isEncrypted: Boolean,
|
isEncrypted: Boolean,
|
||||||
isPublic: Boolean,
|
isPublic: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (isEncrypted || isPublic) {
|
if (isEncrypted || isPublic) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = modifier
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
if (isEncrypted) {
|
if (isEncrypted) {
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -276,11 +276,7 @@ private fun RoomMemberListItem(
|
||||||
}
|
}
|
||||||
MatrixUserRow(
|
MatrixUserRow(
|
||||||
modifier = modifier.clickable(onClick = onClick),
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
matrixUser = MatrixUser(
|
matrixUser = roomMember.toMatrixUser(),
|
||||||
userId = roomMember.userId,
|
|
||||||
displayName = roomMember.displayName,
|
|
||||||
avatarUrl = roomMember.avatarUrl,
|
|
||||||
),
|
|
||||||
avatarSize = AvatarSize.UserListItem,
|
avatarSize = AvatarSize.UserListItem,
|
||||||
trailingContent = roleText?.let {
|
trailingContent = roleText?.let {
|
||||||
@Composable {
|
@Composable {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||||
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
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.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
|
|
@ -59,6 +58,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.getBestName
|
import io.element.android.libraries.matrix.api.room.getBestName
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -217,12 +217,7 @@ private fun RoomMemberActionsBottomSheet(
|
||||||
modifier = Modifier.padding(vertical = 16.dp)
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
avatarData = AvatarData(
|
avatarData = roomMember.getAvatarData(size = AvatarSize.RoomListManageUser),
|
||||||
id = roomMember.userId.value,
|
|
||||||
name = roomMember.displayName,
|
|
||||||
url = roomMember.avatarUrl,
|
|
||||||
size = AvatarSize.RoomListManageUser,
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 28.dp)
|
.padding(bottom = 28.dp)
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
import io.element.android.libraries.matrix.api.room.activeRoomMembers
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -50,21 +50,21 @@ class RolesAndPermissionsPresenter @Inject constructor(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||||
val roomMembers by room.membersStateFlow.collectAsState()
|
val roomMembers by room.membersStateFlow.collectAsState()
|
||||||
// Get the list of joined room members, in order to filter members present in the power
|
// Get the list of active room members (joined or invited), in order to filter members present in the power
|
||||||
// level state Event, but not member of the room anymore.
|
// level state Event.
|
||||||
val joinedRoomMemberIds by remember {
|
val activeRoomMemberIds by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
roomMembers.joinedRoomMembers().map { it.userId }
|
roomMembers.activeRoomMembers().map { it.userId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val moderatorCount by remember {
|
val moderatorCount by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
roomInfo.userCountWithRole(joinedRoomMemberIds, RoomMember.Role.MODERATOR)
|
roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.MODERATOR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val adminCount by remember {
|
val adminCount by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
roomInfo.userCountWithRole(joinedRoomMemberIds, RoomMember.Role.ADMIN)
|
roomInfo.userCountWithRole(activeRoomMemberIds, RoomMember.Role.ADMIN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
val changeOwnRoleAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
|
@ -118,9 +118,9 @@ class RolesAndPermissionsPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MatrixRoomInfo?.userCountWithRole(joinedRoomMemberIds: List<UserId>, role: RoomMember.Role): Int {
|
private fun MatrixRoomInfo?.userCountWithRole(userIds: List<UserId>, role: RoomMember.Role): Int {
|
||||||
return this?.userPowerLevels.orEmpty().count { (userId, level) ->
|
return this?.userPowerLevels.orEmpty().count { (userId, level) ->
|
||||||
RoomMember.Role.forPowerLevel(level) == role && userId in joinedRoomMemberIds
|
RoomMember.Role.forPowerLevel(level) == role && userId in userIds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ import io.element.android.libraries.matrix.api.room.getBestName
|
||||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
import io.element.android.libraries.matrix.ui.components.SelectedUsersRowList
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -327,7 +328,7 @@ private fun ListMemberItem(
|
||||||
}
|
}
|
||||||
MemberRow(
|
MemberRow(
|
||||||
modifier = Modifier.clickable(enabled = canToggle, onClick = { onToggleSelection(roomMember) }),
|
modifier = Modifier.clickable(enabled = canToggle, onClick = { onToggleSelection(roomMember) }),
|
||||||
avatarData = AvatarData(roomMember.userId.value, roomMember.displayName, roomMember.avatarUrl, AvatarSize.UserListItem),
|
avatarData = roomMember.getAvatarData(size = AvatarSize.UserListItem),
|
||||||
name = roomMember.getBestName(),
|
name = roomMember.getBestName(),
|
||||||
userId = roomMember.userId.value.takeIf { roomMember.displayName?.isNotBlank() == true },
|
userId = roomMember.userId.value.takeIf { roomMember.displayName?.isNotBlank() == true },
|
||||||
isPending = roomMember.membership == RoomMembershipState.INVITE,
|
isPending = roomMember.membership == RoomMembershipState.INVITE,
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,13 @@
|
||||||
<string name="screen_room_change_role_confirm_demote_self_action">"Degradera"</string>
|
<string name="screen_room_change_role_confirm_demote_self_action">"Degradera"</string>
|
||||||
<string name="screen_room_change_role_confirm_demote_self_description">"Du kommer inte att kunna ångra denna ändring eftersom du degraderar dig själv, om du är den sista privilegierade användaren i rummet kommer det att vara omöjligt att återfå privilegier."</string>
|
<string name="screen_room_change_role_confirm_demote_self_description">"Du kommer inte att kunna ångra denna ändring eftersom du degraderar dig själv, om du är den sista privilegierade användaren i rummet kommer det att vara omöjligt att återfå privilegier."</string>
|
||||||
<string name="screen_room_change_role_confirm_demote_self_title">"Degradera dig själv?"</string>
|
<string name="screen_room_change_role_confirm_demote_self_title">"Degradera dig själv?"</string>
|
||||||
|
<string name="screen_room_change_role_invited_member_name">"%1$s (Väntar)"</string>
|
||||||
<string name="screen_room_change_role_moderators_title">"Redigera moderatorer"</string>
|
<string name="screen_room_change_role_moderators_title">"Redigera moderatorer"</string>
|
||||||
<string name="screen_room_change_role_section_administrators">"Administratörer"</string>
|
<string name="screen_room_change_role_section_administrators">"Administratörer"</string>
|
||||||
<string name="screen_room_change_role_section_moderators">"Moderatorer"</string>
|
<string name="screen_room_change_role_section_moderators">"Moderatorer"</string>
|
||||||
<string name="screen_room_change_role_section_users">"Medlemmar"</string>
|
<string name="screen_room_change_role_section_users">"Medlemmar"</string>
|
||||||
|
<string name="screen_room_change_role_unsaved_changes_description">"Du har osparade ändringar."</string>
|
||||||
|
<string name="screen_room_change_role_unsaved_changes_title">"Spara ändringar?"</string>
|
||||||
<string name="screen_room_details_add_topic_title">"Lägg till ämne"</string>
|
<string name="screen_room_details_add_topic_title">"Lägg till ämne"</string>
|
||||||
<string name="screen_room_details_already_a_member">"Redan medlem"</string>
|
<string name="screen_room_details_already_a_member">"Redan medlem"</string>
|
||||||
<string name="screen_room_details_already_invited">"Redan inbjuden"</string>
|
<string name="screen_room_details_already_invited">"Redan inbjuden"</string>
|
||||||
|
|
@ -53,11 +56,13 @@
|
||||||
<string name="screen_room_member_list_ban_member_confirmation_action">"Banna"</string>
|
<string name="screen_room_member_list_ban_member_confirmation_action">"Banna"</string>
|
||||||
<string name="screen_room_member_list_ban_member_confirmation_description">"Denne kommer inte att kunna gå med i det här rummet igen om denne bjuds in."</string>
|
<string name="screen_room_member_list_ban_member_confirmation_description">"Denne kommer inte att kunna gå med i det här rummet igen om denne bjuds in."</string>
|
||||||
<string name="screen_room_member_list_ban_member_confirmation_title">"Är du säker på att du vill banna den här medlemmen?"</string>
|
<string name="screen_room_member_list_ban_member_confirmation_title">"Är du säker på att du vill banna den här medlemmen?"</string>
|
||||||
|
<string name="screen_room_member_list_banned_empty">"Det finns inga bannade användare i det här rummet."</string>
|
||||||
<string name="screen_room_member_list_banning_user">"Bannar %1$s"</string>
|
<string name="screen_room_member_list_banning_user">"Bannar %1$s"</string>
|
||||||
<plurals name="screen_room_member_list_header_title">
|
<plurals name="screen_room_member_list_header_title">
|
||||||
<item quantity="one">"%1$d person"</item>
|
<item quantity="one">"%1$d person"</item>
|
||||||
<item quantity="other">"%1$d personer"</item>
|
<item quantity="other">"%1$d personer"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="screen_room_member_list_manage_member_ban">"Ta bort och banna medlem"</string>
|
||||||
<string name="screen_room_member_list_manage_member_remove">"Ta bort från rummet"</string>
|
<string name="screen_room_member_list_manage_member_remove">"Ta bort från rummet"</string>
|
||||||
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Ta bort och banna medlem"</string>
|
<string name="screen_room_member_list_manage_member_remove_confirmation_ban">"Ta bort och banna medlem"</string>
|
||||||
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Ta bara bort medlem"</string>
|
<string name="screen_room_member_list_manage_member_remove_confirmation_kick">"Ta bara bort medlem"</string>
|
||||||
|
|
@ -89,11 +94,16 @@
|
||||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Endast omnämnanden och nyckelord"</string>
|
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Endast omnämnanden och nyckelord"</string>
|
||||||
<string name="screen_room_notification_settings_room_custom_settings_title">"I det här rummet, meddela mig för"</string>
|
<string name="screen_room_notification_settings_room_custom_settings_title">"I det här rummet, meddela mig för"</string>
|
||||||
<string name="screen_room_roles_and_permissions_admins">"Administratörer"</string>
|
<string name="screen_room_roles_and_permissions_admins">"Administratörer"</string>
|
||||||
|
<string name="screen_room_roles_and_permissions_change_my_role">"Ändra min roll"</string>
|
||||||
|
<string name="screen_room_roles_and_permissions_change_role_demote_to_member">"Degradera till medlem"</string>
|
||||||
|
<string name="screen_room_roles_and_permissions_change_role_demote_to_moderator">"Degradera till moderator"</string>
|
||||||
<string name="screen_room_roles_and_permissions_member_moderation">"Medlemsmoderering"</string>
|
<string name="screen_room_roles_and_permissions_member_moderation">"Medlemsmoderering"</string>
|
||||||
<string name="screen_room_roles_and_permissions_messages_and_content">"Meddelanden och innehåll"</string>
|
<string name="screen_room_roles_and_permissions_messages_and_content">"Meddelanden och innehåll"</string>
|
||||||
<string name="screen_room_roles_and_permissions_moderators">"Moderatorer"</string>
|
<string name="screen_room_roles_and_permissions_moderators">"Moderatorer"</string>
|
||||||
<string name="screen_room_roles_and_permissions_permissions_header">"Behörigheter"</string>
|
<string name="screen_room_roles_and_permissions_permissions_header">"Behörigheter"</string>
|
||||||
<string name="screen_room_roles_and_permissions_reset">"Återställ behörigheter"</string>
|
<string name="screen_room_roles_and_permissions_reset">"Återställ behörigheter"</string>
|
||||||
|
<string name="screen_room_roles_and_permissions_reset_confirm_description">"När du har återställt behörigheterna kommer du att förlora de aktuella inställningarna."</string>
|
||||||
|
<string name="screen_room_roles_and_permissions_reset_confirm_title">"Återställ behörigheter?"</string>
|
||||||
<string name="screen_room_roles_and_permissions_roles_header">"Roller"</string>
|
<string name="screen_room_roles_and_permissions_roles_header">"Roller"</string>
|
||||||
<string name="screen_room_roles_and_permissions_room_details">"Rumsdetaljer"</string>
|
<string name="screen_room_roles_and_permissions_room_details">"Rumsdetaljer"</string>
|
||||||
<string name="screen_room_roles_and_permissions_title">"Roller och behörigheter"</string>
|
<string name="screen_room_roles_and_permissions_title">"Roller och behörigheter"</string>
|
||||||
|
|
|
||||||
|
|
@ -169,8 +169,12 @@ class RoomDetailsPresenterTest {
|
||||||
val presenter = createRoomDetailsPresenter(room)
|
val presenter = createRoomDetailsPresenter(room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
|
assertThat(initialState.roomType).isEqualTo(
|
||||||
|
RoomDetailsType.Dm(
|
||||||
|
me = myRoomMember,
|
||||||
|
otherMember = otherRoomMember,
|
||||||
|
)
|
||||||
|
)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithTag
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -126,6 +127,7 @@ class RoomDetailsViewTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Config(qualifiers = "h1024dp")
|
||||||
@Test
|
@Test
|
||||||
fun `click on add topic emit expected event`() {
|
fun `click on add topic emit expected event`() {
|
||||||
ensureCalledOnceWithParam<RoomDetailsAction>(RoomDetailsAction.AddTopic) { callback ->
|
ensureCalledOnceWithParam<RoomDetailsAction>(RoomDetailsAction.AddTopic) { callback ->
|
||||||
|
|
@ -176,7 +178,11 @@ class RoomDetailsViewTest {
|
||||||
fun `click on avatar test on DM`() {
|
fun `click on avatar test on DM`() {
|
||||||
val eventsRecorder = EventsRecorder<RoomDetailsEvent>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<RoomDetailsEvent>(expectEvents = false)
|
||||||
val state = aRoomDetailsState(
|
val state = aRoomDetailsState(
|
||||||
roomType = RoomDetailsType.Dm(aDmRoomMember(avatarUrl = "an_avatar_url")),
|
roomType = RoomDetailsType.Dm(
|
||||||
|
aRoomMember(),
|
||||||
|
aDmRoomMember(avatarUrl = "an_avatar_url"),
|
||||||
|
),
|
||||||
|
roomName = "Daniel",
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
val callback = EnsureCalledOnceWithTwoParams("Daniel", "an_avatar_url")
|
val callback = EnsureCalledOnceWithTwoParams("Daniel", "an_avatar_url")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_room_directory_search_loading_error">"Misslyckades att ladda"</string>
|
||||||
|
<string name="screen_room_directory_search_title">"Rumskatalog"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -53,7 +53,7 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvid
|
||||||
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||||
import io.element.android.libraries.core.extensions.orEmpty
|
import io.element.android.libraries.core.extensions.orEmpty
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
|
@ -158,7 +158,10 @@ private fun RoomSummaryScaffoldRow(
|
||||||
.padding(horizontal = 16.dp, vertical = 11.dp)
|
.padding(horizontal = 16.dp, vertical = 11.dp)
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
) {
|
) {
|
||||||
Avatar(room.avatarData)
|
CompositeAvatar(
|
||||||
|
avatarData = room.avatarData,
|
||||||
|
heroes = room.heroes,
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||||
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomListRoomSummaryFactory @Inject constructor(
|
class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
|
|
@ -54,43 +58,45 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
inviteSender = null,
|
inviteSender = null,
|
||||||
isDm = false,
|
isDm = false,
|
||||||
canonicalAlias = null,
|
canonicalAlias = null,
|
||||||
|
heroes = persistentListOf(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(roomSummary: RoomSummary.Filled): RoomListRoomSummary {
|
fun create(roomSummary: RoomSummary.Filled): RoomListRoomSummary {
|
||||||
val roomIdentifier = roomSummary.identifier()
|
return create(roomSummary.details)
|
||||||
val avatarData = AvatarData(
|
}
|
||||||
id = roomIdentifier,
|
|
||||||
name = roomSummary.details.name,
|
private fun create(details: RoomSummaryDetails): RoomListRoomSummary {
|
||||||
url = roomSummary.details.avatarUrl,
|
val avatarData = details.getAvatarData(size = AvatarSize.RoomListItem)
|
||||||
size = AvatarSize.RoomListItem,
|
|
||||||
)
|
|
||||||
return RoomListRoomSummary(
|
return RoomListRoomSummary(
|
||||||
id = roomIdentifier,
|
id = details.roomId.value,
|
||||||
roomId = RoomId(roomIdentifier),
|
roomId = details.roomId,
|
||||||
name = roomSummary.details.name,
|
name = details.name,
|
||||||
numberOfUnreadMessages = roomSummary.details.numUnreadMessages,
|
numberOfUnreadMessages = details.numUnreadMessages,
|
||||||
numberOfUnreadMentions = roomSummary.details.numUnreadMentions,
|
numberOfUnreadMentions = details.numUnreadMentions,
|
||||||
numberOfUnreadNotifications = roomSummary.details.numUnreadNotifications,
|
numberOfUnreadNotifications = details.numUnreadNotifications,
|
||||||
isMarkedUnread = roomSummary.details.isMarkedUnread,
|
isMarkedUnread = details.isMarkedUnread,
|
||||||
timestamp = lastMessageTimestampFormatter.format(roomSummary.details.lastMessageTimestamp),
|
timestamp = lastMessageTimestampFormatter.format(details.lastMessageTimestamp),
|
||||||
lastMessage = roomSummary.details.lastMessage?.let { message ->
|
lastMessage = details.lastMessage?.let { message ->
|
||||||
roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)
|
roomLastMessageFormatter.format(message.event, details.isDirect)
|
||||||
}.orEmpty(),
|
}.orEmpty(),
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
userDefinedNotificationMode = roomSummary.details.userDefinedNotificationMode,
|
userDefinedNotificationMode = details.userDefinedNotificationMode,
|
||||||
hasRoomCall = roomSummary.details.hasRoomCall,
|
hasRoomCall = details.hasRoomCall,
|
||||||
isDirect = roomSummary.details.isDirect,
|
isDirect = details.isDirect,
|
||||||
isFavorite = roomSummary.details.isFavorite,
|
isFavorite = details.isFavorite,
|
||||||
inviteSender = roomSummary.details.inviter?.toInviteSender(),
|
inviteSender = details.inviter?.toInviteSender(),
|
||||||
isDm = roomSummary.details.isDm,
|
isDm = details.isDm,
|
||||||
canonicalAlias = roomSummary.details.canonicalAlias,
|
canonicalAlias = details.canonicalAlias,
|
||||||
displayType = if (roomSummary.details.currentUserMembership == CurrentUserMembership.INVITED) {
|
displayType = if (details.currentUserMembership == CurrentUserMembership.INVITED) {
|
||||||
RoomSummaryDisplayType.INVITE
|
RoomSummaryDisplayType.INVITE
|
||||||
} else {
|
} else {
|
||||||
RoomSummaryDisplayType.ROOM
|
RoomSummaryDisplayType.ROOM
|
||||||
}
|
},
|
||||||
|
heroes = details.heroes.map { user ->
|
||||||
|
user.getAvatarData(size = AvatarSize.RoomListItem)
|
||||||
|
}.toImmutableList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.animateScrollBy
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
|
@ -34,8 +35,12 @@ import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.FilterChipDefaults
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -66,7 +71,31 @@ fun RoomListFiltersView(
|
||||||
state.eventSink(RoomListFiltersEvents.ToggleFilter(filter))
|
state.eventSink(RoomListFiltersEvents.ToggleFilter(filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scrollToStart by remember { mutableIntStateOf(0) }
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
LaunchedEffect(scrollToStart) {
|
||||||
|
// Scroll until the first item start to be displayed
|
||||||
|
// Since all items have different size, there is no way to compute the amount of
|
||||||
|
// pixel to scroll to go directly to the start of the row.
|
||||||
|
// But IRL it should only happen for one item.
|
||||||
|
while (lazyListState.firstVisibleItemIndex > 0) {
|
||||||
|
lazyListState.animateScrollBy(
|
||||||
|
value = -(lazyListState.firstVisibleItemScrollOffset + 1f),
|
||||||
|
animationSpec = spring(
|
||||||
|
stiffness = Spring.StiffnessMediumLow,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Then scroll to the start of the list, a bit faster, to fully reveal the first
|
||||||
|
// item, which can be the close button to reset filter, or the first item
|
||||||
|
// if the user has scroll a bit before clicking on the close button.
|
||||||
|
lazyListState.animateScrollBy(
|
||||||
|
value = -lazyListState.firstVisibleItemScrollOffset.toFloat(),
|
||||||
|
animationSpec = spring(
|
||||||
|
stiffness = Spring.StiffnessMedium,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
val previousFilters = remember { mutableStateOf(listOf<RoomListFilter>()) }
|
val previousFilters = remember { mutableStateOf(listOf<RoomListFilter>()) }
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(start = 8.dp, end = 16.dp),
|
contentPadding = PaddingValues(start = 8.dp, end = 16.dp),
|
||||||
|
|
@ -84,6 +113,9 @@ fun RoomListFiltersView(
|
||||||
onClick = {
|
onClick = {
|
||||||
previousFilters.value = state.selectedFilters()
|
previousFilters.value = state.selectedFilters()
|
||||||
onClearFiltersClick()
|
onClearFiltersClick()
|
||||||
|
// When clearing filter, we want to ensure that the list
|
||||||
|
// of filters is scrolled to the start.
|
||||||
|
scrollToStart++
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -100,6 +132,10 @@ fun RoomListFiltersView(
|
||||||
onClick = {
|
onClick = {
|
||||||
previousFilters.value = state.selectedFilters()
|
previousFilters.value = state.selectedFilters()
|
||||||
onToggleFilter(it)
|
onToggleFilter(it)
|
||||||
|
// When selecting a filter, we want to scroll to the start of the list
|
||||||
|
if (filterWithSelection.isSelected.not()) {
|
||||||
|
scrollToStart++
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class RoomListRoomSummary(
|
data class RoomListRoomSummary(
|
||||||
|
|
@ -43,6 +44,7 @@ data class RoomListRoomSummary(
|
||||||
val isDm: Boolean,
|
val isDm: Boolean,
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
val inviteSender: InviteSender?,
|
val inviteSender: InviteSender?,
|
||||||
|
val heroes: ImmutableList<AvatarData>,
|
||||||
) {
|
) {
|
||||||
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
||||||
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
|
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
|
||||||
override val values: Sequence<RoomListRoomSummary>
|
override val values: Sequence<RoomListRoomSummary>
|
||||||
|
|
@ -142,6 +143,7 @@ internal fun aRoomListRoomSummary(
|
||||||
inviteSender: InviteSender? = null,
|
inviteSender: InviteSender? = null,
|
||||||
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||||
canonicalAlias: RoomAlias? = null,
|
canonicalAlias: RoomAlias? = null,
|
||||||
|
heroes: List<AvatarData> = emptyList(),
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = id,
|
id = id,
|
||||||
roomId = RoomId(id),
|
roomId = RoomId(id),
|
||||||
|
|
@ -161,4 +163,5 @@ internal fun aRoomListRoomSummary(
|
||||||
inviteSender = inviteSender,
|
inviteSender = inviteSender,
|
||||||
displayType = displayType,
|
displayType = displayType,
|
||||||
canonicalAlias = canonicalAlias,
|
canonicalAlias = canonicalAlias,
|
||||||
|
heroes = heroes.toImmutableList(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="confirm_recovery_key_banner_message">"Ваша рэзервовая копія чата зараз не сінхранізавана. Вам трэба пацвердзіць ключ аднаўлення, каб захаваць доступ да рэзервовай копіі чата."</string>
|
<string name="confirm_recovery_key_banner_message">"Ваша рэзервовая копія чата зараз не сінхранізавана. Вам трэба пацвердзіць ключ аднаўлення, каб захаваць доступ да рэзервовай копіі чата."</string>
|
||||||
<string name="confirm_recovery_key_banner_title">"Увядзіце ключ аднаўлення"</string>
|
<string name="confirm_recovery_key_banner_title">"Увядзіце ключ аднаўлення"</string>
|
||||||
|
<string name="full_screen_intent_banner_message">"Каб не прапусціць важны званок, зменіце налады, каб дазволіць поўнаэкранныя апавяшчэнні, калі тэлефон заблакіраваны."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Палепшыце якасць званкоў"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Вы ўпэўненыя, што хочаце адхіліць запрашэнне ў %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Адхіліць запрашэнне"</string>
|
<string name="screen_invites_decline_chat_title">"Адхіліць запрашэнне"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Вы ўпэўненыя, што хочаце адмовіцца ад прыватных зносін з %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="confirm_recovery_key_banner_message">"Vaše záloha chatu není aktuálně synchronizována. Abyste si zachovali přístup k záloze chatu, musíte potvrdit klíč pro obnovení."</string>
|
<string name="confirm_recovery_key_banner_message">"Vaše záloha chatu není aktuálně synchronizována. Abyste si zachovali přístup k záloze chatu, musíte potvrdit klíč pro obnovení."</string>
|
||||||
<string name="confirm_recovery_key_banner_title">"Potvrďte klíč pro obnovení"</string>
|
<string name="confirm_recovery_key_banner_title">"Potvrďte klíč pro obnovení"</string>
|
||||||
|
<string name="full_screen_intent_banner_message">"Abyste nikdy nezmeškali důležitý hovor, změňte nastavení tak, abyste povolili oznámení na celé obrazovce, když je telefon uzamčen."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Vylepšete si zážitek z volání"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Opravdu chcete odmítnout pozvánku do %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Opravdu chcete odmítnout pozvánku do %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Odmítnout pozvání"</string>
|
<string name="screen_invites_decline_chat_title">"Odmítnout pozvání"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="confirm_recovery_key_banner_message">"La sauvegarde des conversations est désynchronisée. Vous devez confirmer la clé de récupération pour accéder à votre historique."</string>
|
<string name="confirm_recovery_key_banner_message">"La sauvegarde des conversations est désynchronisée. Vous devez confirmer la clé de récupération pour accéder à votre historique."</string>
|
||||||
<string name="confirm_recovery_key_banner_title">"Confirmer votre clé de récupération"</string>
|
<string name="confirm_recovery_key_banner_title">"Confirmer votre clé de récupération"</string>
|
||||||
|
<string name="full_screen_intent_banner_message">"Afin de ne jamais manquer un appel important, veuillez modifier vos paramètres pour autoriser les notifications en plein écran lorsque votre appareil est verrouillé."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Améliorez votre expérience d’appel"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?"</string>
|
<string name="screen_invites_decline_chat_message">"Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Refuser l’invitation"</string>
|
<string name="screen_invites_decline_chat_title">"Refuser l’invitation"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="confirm_recovery_key_banner_message">"Backup-ul pentru chat nu este sincronizat în prezent. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup."</string>
|
<string name="confirm_recovery_key_banner_message">"Backup-ul pentru chat nu este sincronizat în prezent. Trebuie să confirmați cheia de recuperare pentru a menține accesul la backup."</string>
|
||||||
<string name="confirm_recovery_key_banner_title">"Confirmați cheia de recuperare"</string>
|
<string name="confirm_recovery_key_banner_title">"Confirmați cheia de recuperare"</string>
|
||||||
|
<string name="full_screen_intent_banner_message">"Pentru a vă asigura că nu pierdeți niciodată un apel important, vă rugăm să modificați setările pentru a permite notificări fullscreen atunci când telefonul este blocat."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Îmbunătățiți-vă experiența in timpul unui apel"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Sigur doriți să refuzați alăturarea la %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Sigur doriți să refuzați alăturarea la %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Refuzați invitația"</string>
|
<string name="screen_invites_decline_chat_title">"Refuzați invitația"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Sigur doriți să refuzați conversațiile cu %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Sigur doriți să refuzați conversațiile cu %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
"Введите "
|
"Введите "
|
||||||
<b>"ключ восстановления"</b>
|
<b>"ключ восстановления"</b>
|
||||||
</string>
|
</string>
|
||||||
|
<string name="full_screen_intent_banner_message">"Чтобы никогда не пропустить важный звонок, измените настройки, чтобы разрешить полноэкранные уведомления, когда ваш телефон заблокирован."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Улучшите качество звонков"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Вы уверены, что хотите отклонить приглашение в %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Вы уверены, что хотите отклонить приглашение в %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Отклонить приглашение"</string>
|
<string name="screen_invites_decline_chat_title">"Отклонить приглашение"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Вы уверены, что хотите отказаться от личного общения с %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Вы уверены, что хотите отказаться от личного общения с %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="confirm_recovery_key_banner_message">"Vaša záloha konverzácie nie je momentálne synchronizovaná. Na zachovanie prístupu k zálohe konverzácie musíte potvrdiť svoj kľúč na obnovu."</string>
|
<string name="confirm_recovery_key_banner_message">"Vaša záloha konverzácie nie je momentálne synchronizovaná. Na zachovanie prístupu k zálohe konverzácie musíte potvrdiť svoj kľúč na obnovu."</string>
|
||||||
<string name="confirm_recovery_key_banner_title">"Potvrďte svoj kľúč na obnovenie"</string>
|
<string name="confirm_recovery_key_banner_title">"Potvrďte svoj kľúč na obnovenie"</string>
|
||||||
|
<string name="full_screen_intent_banner_message">"Aby ste už nikdy nezmeškali dôležitý hovor, zmeňte svoje nastavenia a povoľte upozornenia na celú obrazovku, keď je váš telefón uzamknutý."</string>
|
||||||
|
<string name="full_screen_intent_banner_title">"Vylepšite svoj zážitok z hovoru"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Odmietnuť pozvanie"</string>
|
<string name="screen_invites_decline_chat_title">"Odmietnuť pozvanie"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,11 @@
|
||||||
<string name="screen_roomlist_empty_message">"Kom igång genom att skicka meddelanden till någon."</string>
|
<string name="screen_roomlist_empty_message">"Kom igång genom att skicka meddelanden till någon."</string>
|
||||||
<string name="screen_roomlist_empty_title">"Inga chattar än."</string>
|
<string name="screen_roomlist_empty_title">"Inga chattar än."</string>
|
||||||
<string name="screen_roomlist_filter_favourites">"Favoriter"</string>
|
<string name="screen_roomlist_filter_favourites">"Favoriter"</string>
|
||||||
|
<string name="screen_roomlist_filter_favourites_empty_state_subtitle">"Du kan lägga till en chatt till dina favoriter i chattinställningarna.
|
||||||
|
För tillfället kan du avmarkera filter för att se dina andra chattar"</string>
|
||||||
<string name="screen_roomlist_filter_favourites_empty_state_title">"Du har inga favoritchattar än"</string>
|
<string name="screen_roomlist_filter_favourites_empty_state_title">"Du har inga favoritchattar än"</string>
|
||||||
<string name="screen_roomlist_filter_low_priority">"Låg prioritet"</string>
|
<string name="screen_roomlist_filter_low_priority">"Låg prioritet"</string>
|
||||||
|
<string name="screen_roomlist_filter_mixed_empty_state_subtitle">"Du kan avmarkera filter för att se dina andra chattar"</string>
|
||||||
<string name="screen_roomlist_filter_mixed_empty_state_title">"Du har inga chattar för det här valet"</string>
|
<string name="screen_roomlist_filter_mixed_empty_state_title">"Du har inga chattar för det här valet"</string>
|
||||||
<string name="screen_roomlist_filter_people">"Personer"</string>
|
<string name="screen_roomlist_filter_people">"Personer"</string>
|
||||||
<string name="screen_roomlist_filter_people_empty_state_title">"Du har inga DM:er än"</string>
|
<string name="screen_roomlist_filter_people_empty_state_title">"Du har inga DM:er än"</string>
|
||||||
|
|
@ -27,6 +30,7 @@ Du har inga olästa meddelanden!"</string>
|
||||||
<string name="screen_roomlist_main_space_title">"Alla chattar"</string>
|
<string name="screen_roomlist_main_space_title">"Alla chattar"</string>
|
||||||
<string name="screen_roomlist_mark_as_read">"Markera som läst"</string>
|
<string name="screen_roomlist_mark_as_read">"Markera som läst"</string>
|
||||||
<string name="screen_roomlist_mark_as_unread">"Markera som oläst"</string>
|
<string name="screen_roomlist_mark_as_unread">"Markera som oläst"</string>
|
||||||
|
<string name="screen_roomlist_room_directory_button_title">"Bläddra bland alla rum"</string>
|
||||||
<string name="session_verification_banner_message">"Det verkar som om du använder en ny enhet. Verifiera med en annan enhet för att komma åt dina krypterade meddelanden."</string>
|
<string name="session_verification_banner_message">"Det verkar som om du använder en ny enhet. Verifiera med en annan enhet för att komma åt dina krypterade meddelanden."</string>
|
||||||
<string name="session_verification_banner_title">"Verifiera att det är du"</string>
|
<string name="session_verification_banner_title">"Verifiera att det är du"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class RoomListRoomSummaryTest {
|
class RoomListRoomSummaryTest {
|
||||||
|
|
@ -91,6 +92,7 @@ internal fun createRoomListRoomSummary(
|
||||||
userDefinedNotificationMode: RoomNotificationMode? = null,
|
userDefinedNotificationMode: RoomNotificationMode? = null,
|
||||||
isFavorite: Boolean = false,
|
isFavorite: Boolean = false,
|
||||||
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||||
|
heroes: List<AvatarData> = emptyList(),
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = A_ROOM_ID.value,
|
id = A_ROOM_ID.value,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -110,4 +112,5 @@ internal fun createRoomListRoomSummary(
|
||||||
canonicalAlias = null,
|
canonicalAlias = null,
|
||||||
inviteSender = null,
|
inviteSender = null,
|
||||||
isDm = false,
|
isDm = false,
|
||||||
|
heroes = heroes.toPersistentList(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,11 @@
|
||||||
package io.element.android.features.userprofile.shared
|
package io.element.android.features.userprofile.shared
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -36,6 +33,8 @@ import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
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.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
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
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
|
|
@ -55,15 +54,12 @@ fun UserProfileHeaderSection(
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.size(70.dp)) {
|
Avatar(
|
||||||
Avatar(
|
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
|
||||||
avatarData = AvatarData(userId.value, userName, avatarUrl, AvatarSize.UserHeader),
|
modifier = Modifier
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||||
.fillMaxSize()
|
|
||||||
.testTag(TestTags.memberDetailAvatar)
|
.testTag(TestTags.memberDetailAvatar)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
if (userName != null) {
|
if (userName != null) {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -86,3 +82,14 @@ fun UserProfileHeaderSection(
|
||||||
Spacer(Modifier.height(40.dp))
|
Spacer(Modifier.height(40.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun UserProfileHeaderSectionPreview() = ElementPreview {
|
||||||
|
UserProfileHeaderSection(
|
||||||
|
avatarUrl = null,
|
||||||
|
userId = UserId("@alice:example.com"),
|
||||||
|
userName = "Alice",
|
||||||
|
openAvatarPreview = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_identity_confirmation_subtitle">"Verifiera den här enheten för att konfigurera säkra meddelanden."</string>
|
||||||
|
<string name="screen_identity_confirmation_title">"Bekräfta att det är du"</string>
|
||||||
|
<string name="screen_identity_confirmed_subtitle">"Nu kan du läsa eller skicka meddelanden säkert, och alla du chattar med kan också lita på den här enheten."</string>
|
||||||
|
<string name="screen_identity_confirmed_title">"Enhet verifierad"</string>
|
||||||
|
<string name="screen_identity_use_another_device">"Använd en annan enhet"</string>
|
||||||
|
<string name="screen_identity_waiting_on_other_device">"Väntar på annan enhet …"</string>
|
||||||
<string name="screen_session_verification_cancelled_subtitle">"Något verkar inte stämma. Antingen gick tidsgränsen för begäran ut eller så avvisades begäran."</string>
|
<string name="screen_session_verification_cancelled_subtitle">"Något verkar inte stämma. Antingen gick tidsgränsen för begäran ut eller så avvisades begäran."</string>
|
||||||
<string name="screen_session_verification_compare_emojis_subtitle">"Bekräfta att emojierna nedan matchar de som visas på din andra session."</string>
|
<string name="screen_session_verification_compare_emojis_subtitle">"Bekräfta att emojierna nedan matchar de som visas på din andra session."</string>
|
||||||
<string name="screen_session_verification_compare_emojis_title">"Jämför emojis"</string>
|
<string name="screen_session_verification_compare_emojis_title">"Jämför emojis"</string>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ coroutines = "1.8.1"
|
||||||
accompanist = "0.34.0"
|
accompanist = "0.34.0"
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
test_core = "1.5.0"
|
test_core = "1.6.0"
|
||||||
|
|
||||||
#other
|
#other
|
||||||
coil = "2.6.0"
|
coil = "2.6.0"
|
||||||
|
|
@ -44,7 +44,7 @@ serialization_json = "1.6.3"
|
||||||
showkase = "1.0.3"
|
showkase = "1.0.3"
|
||||||
appyx = "1.4.0"
|
appyx = "1.4.0"
|
||||||
sqldelight = "2.0.2"
|
sqldelight = "2.0.2"
|
||||||
wysiwyg = "2.37.3"
|
wysiwyg = "2.37.4"
|
||||||
telephoto = "0.11.2"
|
telephoto = "0.11.2"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
|
|
@ -55,8 +55,7 @@ anvil = "2.4.9"
|
||||||
autoservice = "1.1.1"
|
autoservice = "1.1.1"
|
||||||
|
|
||||||
# quality
|
# quality
|
||||||
androidx-test-ext-junit = "1.1.5"
|
androidx-test-ext-junit = "1.2.0"
|
||||||
espresso-core = "3.5.1"
|
|
||||||
kover = "0.8.0"
|
kover = "0.8.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|
@ -68,7 +67,7 @@ kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v
|
||||||
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
|
kover_gradle_plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" }
|
||||||
gms_google_services = "com.google.gms:google-services:4.4.2"
|
gms_google_services = "com.google.gms:google-services:4.4.2"
|
||||||
# https://firebase.google.com/docs/android/setup#available-libraries
|
# https://firebase.google.com/docs/android/setup#available-libraries
|
||||||
google_firebase_bom = "com.google.firebase:firebase-bom:33.1.0"
|
google_firebase_bom = "com.google.firebase:firebase-bom:33.1.1"
|
||||||
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
|
||||||
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
|
||||||
|
|
||||||
|
|
@ -138,7 +137,7 @@ test_core = { module = "androidx.test:core", version.ref = "test_core" }
|
||||||
test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" }
|
test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" }
|
||||||
test_arch_core = "androidx.arch.core:core-testing:2.2.0"
|
test_arch_core = "androidx.arch.core:core-testing:2.2.0"
|
||||||
test_junit = "junit:junit:4.13.2"
|
test_junit = "junit:junit:4.13.2"
|
||||||
test_runner = "androidx.test:runner:1.5.2"
|
test_runner = "androidx.test:runner:1.6.0"
|
||||||
test_mockk = "io.mockk:mockk:1.13.11"
|
test_mockk = "io.mockk:mockk:1.13.11"
|
||||||
test_konsist = "com.lemonappdev:konsist:0.15.1"
|
test_konsist = "com.lemonappdev:konsist:0.15.1"
|
||||||
test_turbine = "app.cash.turbine:turbine:1.1.0"
|
test_turbine = "app.cash.turbine:turbine:1.1.0"
|
||||||
|
|
@ -175,7 +174,7 @@ otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
|
||||||
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
|
||||||
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" }
|
||||||
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||||
statemachine = "com.freeletics.flowredux:compose:1.2.1"
|
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
||||||
maplibre = "org.maplibre.gl:android-sdk:11.0.1"
|
maplibre = "org.maplibre.gl:android-sdk:11.0.1"
|
||||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.0"
|
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.0"
|
||||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.0"
|
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.0"
|
||||||
|
|
@ -184,7 +183,7 @@ kotlinpoet = "com.squareup:kotlinpoet:1.17.0"
|
||||||
zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
|
zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog:posthog-android:3.3.2"
|
posthog = "com.posthog:posthog-android:3.4.0"
|
||||||
sentry = "io.sentry:sentry-android:7.10.0"
|
sentry = "io.sentry:sentry-android:7.10.0"
|
||||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.23.1"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.23.1"
|
||||||
|
|
@ -209,8 +208,6 @@ google_autoservice_annotations = { module = "com.google.auto.service:auto-servic
|
||||||
# See https://github.com/renovatebot/renovate/issues/18354
|
# See https://github.com/renovatebot/renovate/issues/18354
|
||||||
android_composeCompiler = { module = "androidx.compose.compiler:compiler", version.ref = "composecompiler" }
|
android_composeCompiler = { module = "androidx.compose.compiler:compiler", version.ref = "composecompiler" }
|
||||||
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
|
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
|
||||||
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
|
|
||||||
appcompat = "androidx.appcompat:appcompat:1.7.0"
|
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,19 +26,18 @@ import kotlinx.datetime.toJavaLocalDate
|
||||||
import kotlinx.datetime.toJavaLocalDateTime
|
import kotlinx.datetime.toJavaLocalDateTime
|
||||||
import java.time.Period
|
import java.time.Period
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.FormatStyle
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
// TODO rework this date formatting
|
|
||||||
class DateFormatters @Inject constructor(
|
class DateFormatters @Inject constructor(
|
||||||
private val locale: Locale,
|
private val locale: Locale,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
private val timeZone: TimeZone,
|
private val timeZone: TimeZone,
|
||||||
) {
|
) {
|
||||||
private val onlyTimeFormatter: DateTimeFormatter by lazy {
|
private val onlyTimeFormatter: DateTimeFormatter by lazy {
|
||||||
val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") ?: "HH:mm"
|
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
|
||||||
DateTimeFormatter.ofPattern(pattern, locale)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val dateWithMonthFormatter: DateTimeFormatter by lazy {
|
private val dateWithMonthFormatter: DateTimeFormatter by lazy {
|
||||||
|
|
@ -51,6 +50,10 @@ class DateFormatters @Inject constructor(
|
||||||
DateTimeFormatter.ofPattern(pattern, locale)
|
DateTimeFormatter.ofPattern(pattern, locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
|
||||||
|
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun formatTime(localDateTime: LocalDateTime): String {
|
internal fun formatTime(localDateTime: LocalDateTime): String {
|
||||||
return onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
|
return onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
}
|
}
|
||||||
|
|
@ -63,6 +66,10 @@ class DateFormatters @Inject constructor(
|
||||||
return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun formatDateWithFullFormat(localDateTime: LocalDateTime): String {
|
||||||
|
return dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
|
||||||
|
}
|
||||||
|
|
||||||
internal fun formatDate(
|
internal fun formatDate(
|
||||||
dateToFormat: LocalDateTime,
|
dateToFormat: LocalDateTime,
|
||||||
currentDate: LocalDateTime,
|
currentDate: LocalDateTime,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ class DefaultDaySeparatorFormatter @Inject constructor(
|
||||||
) : DaySeparatorFormatter {
|
) : DaySeparatorFormatter {
|
||||||
override fun format(timestamp: Long): String {
|
override fun format(timestamp: Long): String {
|
||||||
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
|
||||||
return dateFormatters.formatDateWithYear(dateToFormat)
|
// TODO use relative formatting once iOS uses it too
|
||||||
|
return dateFormatters.formatDateWithFullFormat(dateToFormat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
|
||||||
import io.element.android.libraries.dateformatter.test.FakeClock
|
import io.element.android.libraries.dateformatter.test.FakeClock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ class DefaultLastMessageTimestampFormatterTest {
|
||||||
val now = "1980-04-06T18:35:24.00Z"
|
val now = "1980-04-06T18:35:24.00Z"
|
||||||
val dat = "1980-04-06T18:35:24.00Z"
|
val dat = "1980-04-06T18:35:24.00Z"
|
||||||
val formatter = createFormatter(now)
|
val formatter = createFormatter(now)
|
||||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:35")
|
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:35 PM")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -52,7 +53,7 @@ class DefaultLastMessageTimestampFormatterTest {
|
||||||
val now = "1980-04-06T18:35:24.00Z"
|
val now = "1980-04-06T18:35:24.00Z"
|
||||||
val dat = "1980-04-06T18:35:23.00Z"
|
val dat = "1980-04-06T18:35:23.00Z"
|
||||||
val formatter = createFormatter(now)
|
val formatter = createFormatter(now)
|
||||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:35")
|
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:35 PM")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -60,7 +61,7 @@ class DefaultLastMessageTimestampFormatterTest {
|
||||||
val now = "1980-04-06T18:35:24.00Z"
|
val now = "1980-04-06T18:35:24.00Z"
|
||||||
val dat = "1980-04-06T18:34:24.00Z"
|
val dat = "1980-04-06T18:34:24.00Z"
|
||||||
val formatter = createFormatter(now)
|
val formatter = createFormatter(now)
|
||||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:34")
|
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:34 PM")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -68,7 +69,7 @@ class DefaultLastMessageTimestampFormatterTest {
|
||||||
val now = "1980-04-06T18:35:24.00Z"
|
val now = "1980-04-06T18:35:24.00Z"
|
||||||
val dat = "1980-04-06T17:35:24.00Z"
|
val dat = "1980-04-06T17:35:24.00Z"
|
||||||
val formatter = createFormatter(now)
|
val formatter = createFormatter(now)
|
||||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("17:35")
|
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("5:35 PM")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -96,6 +97,15 @@ class DefaultLastMessageTimestampFormatterTest {
|
||||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("06.04.1979")
|
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("06.04.1979")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test full format`() {
|
||||||
|
val now = "1980-04-06T18:35:24.00Z"
|
||||||
|
val dat = "1979-04-06T18:35:24.00Z"
|
||||||
|
val clock = FakeClock().apply { givenInstant(Instant.parse(now)) }
|
||||||
|
val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC)
|
||||||
|
assertThat(dateFormatters.formatDateWithFullFormat(Instant.parse(dat).toLocalDateTime(TimeZone.UTC))).isEqualTo("Friday, April 6, 1979")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create DefaultLastMessageFormatter and set current time to the provided date.
|
* Create DefaultLastMessageFormatter and set current time to the provided date.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
|
@ -52,18 +53,22 @@ fun Avatar(
|
||||||
avatarData: AvatarData,
|
avatarData: AvatarData,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentDescription: String? = null,
|
contentDescription: String? = null,
|
||||||
|
// If not null, will be used instead of the size from avatarData
|
||||||
|
forcedAvatarSize: Dp? = null,
|
||||||
) {
|
) {
|
||||||
val commonModifier = modifier
|
val commonModifier = modifier
|
||||||
.size(avatarData.size.dp)
|
.size(forcedAvatarSize ?: avatarData.size.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
if (avatarData.url.isNullOrBlank()) {
|
if (avatarData.url.isNullOrBlank()) {
|
||||||
InitialsAvatar(
|
InitialsAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
modifier = commonModifier,
|
modifier = commonModifier,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ImageAvatar(
|
ImageAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
modifier = commonModifier,
|
modifier = commonModifier,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
)
|
)
|
||||||
|
|
@ -73,6 +78,7 @@ fun Avatar(
|
||||||
@Composable
|
@Composable
|
||||||
private fun ImageAvatar(
|
private fun ImageAvatar(
|
||||||
avatarData: AvatarData,
|
avatarData: AvatarData,
|
||||||
|
forcedAvatarSize: Dp?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentDescription: String? = null,
|
contentDescription: String? = null,
|
||||||
) {
|
) {
|
||||||
|
|
@ -98,9 +104,15 @@ private fun ImageAvatar(
|
||||||
SideEffect {
|
SideEffect {
|
||||||
Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
|
Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
|
||||||
}
|
}
|
||||||
InitialsAvatar(avatarData = avatarData)
|
InitialsAvatar(
|
||||||
|
avatarData = avatarData,
|
||||||
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else -> InitialsAvatar(avatarData = avatarData)
|
else -> InitialsAvatar(
|
||||||
|
avatarData = avatarData,
|
||||||
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,13 +121,14 @@ private fun ImageAvatar(
|
||||||
@Composable
|
@Composable
|
||||||
private fun InitialsAvatar(
|
private fun InitialsAvatar(
|
||||||
avatarData: AvatarData,
|
avatarData: AvatarData,
|
||||||
|
forcedAvatarSize: Dp?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme)
|
val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme)
|
||||||
Box(
|
Box(
|
||||||
modifier.background(color = avatarColors.background)
|
modifier.background(color = avatarColors.background)
|
||||||
) {
|
) {
|
||||||
val fontSize = avatarData.size.dp.toSp() / 2
|
val fontSize = (forcedAvatarSize ?: avatarData.size.dp).toSp() / 2
|
||||||
val originalFont = ElementTheme.typography.fontHeadingMdBold
|
val originalFont = ElementTheme.typography.fontHeadingMdBold
|
||||||
val ratio = fontSize.value / originalFont.fontSize.value
|
val ratio = fontSize.value / originalFont.fontSize.value
|
||||||
val lineHeight = originalFont.lineHeight * ratio
|
val lineHeight = originalFont.lineHeight * ratio
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ enum class AvatarSize(val dp: Dp) {
|
||||||
SelectedUser(56.dp),
|
SelectedUser(56.dp),
|
||||||
SelectedRoom(56.dp),
|
SelectedRoom(56.dp),
|
||||||
|
|
||||||
|
DmCluster(75.dp),
|
||||||
|
|
||||||
TimelineRoom(32.dp),
|
TimelineRoom(32.dp),
|
||||||
TimelineSender(32.dp),
|
TimelineSender(32.dp),
|
||||||
TimelineReadReceipt(16.dp),
|
TimelineReadReceipt(16.dp),
|
||||||
|
|
@ -55,4 +57,8 @@ enum class AvatarSize(val dp: Dp) {
|
||||||
CustomRoomNotificationSetting(36.dp),
|
CustomRoomNotificationSetting(36.dp),
|
||||||
|
|
||||||
RoomDirectoryItem(36.dp),
|
RoomDirectoryItem(36.dp),
|
||||||
|
|
||||||
|
EditProfileDetails(96.dp),
|
||||||
|
|
||||||
|
Suggestion(32.dp),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.avatar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import java.util.Collections
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CompositeAvatar(
|
||||||
|
avatarData: AvatarData,
|
||||||
|
heroes: ImmutableList<AvatarData>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentDescription: String? = null,
|
||||||
|
) {
|
||||||
|
if (avatarData.url != null || heroes.isEmpty()) {
|
||||||
|
Avatar(avatarData, modifier, contentDescription)
|
||||||
|
} else {
|
||||||
|
val limitedHeroes = heroes.take(4)
|
||||||
|
val numberOfHeroes = limitedHeroes.size
|
||||||
|
if (numberOfHeroes == 4) {
|
||||||
|
// Swap 2 and 3 so that the 4th hero is at the bottom right
|
||||||
|
Collections.swap(limitedHeroes, 2, 3)
|
||||||
|
}
|
||||||
|
when (numberOfHeroes) {
|
||||||
|
0 -> {
|
||||||
|
error("Unsupported number of heroes: 0")
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
Avatar(heroes[0], modifier, contentDescription)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val angle = 2 * Math.PI / numberOfHeroes
|
||||||
|
val offsetRadius = when (numberOfHeroes) {
|
||||||
|
2 -> avatarData.size.dp.value / 4.2
|
||||||
|
3 -> avatarData.size.dp.value / 4.0
|
||||||
|
4 -> avatarData.size.dp.value / 3.1
|
||||||
|
else -> error("Unsupported number of heroes: $numberOfHeroes")
|
||||||
|
}
|
||||||
|
val heroAvatarSize = when (numberOfHeroes) {
|
||||||
|
2 -> avatarData.size.dp / 2.2f
|
||||||
|
3 -> avatarData.size.dp / 2.4f
|
||||||
|
4 -> avatarData.size.dp / 2.2f
|
||||||
|
else -> error("Unsupported number of heroes: $numberOfHeroes")
|
||||||
|
}
|
||||||
|
val angleOffset = when (numberOfHeroes) {
|
||||||
|
2 -> PI
|
||||||
|
3 -> 7 * PI / 6
|
||||||
|
4 -> 13 * PI / 4
|
||||||
|
else -> error("Unsupported number of heroes: $numberOfHeroes")
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(avatarData.size.dp)
|
||||||
|
.semantics {
|
||||||
|
this.contentDescription = contentDescription.orEmpty()
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
limitedHeroes.forEachIndexed { index, heroAvatar ->
|
||||||
|
val xOffset = (offsetRadius * cos(angle * index.toDouble() + angleOffset)).dp
|
||||||
|
val yOffset = (offsetRadius * sin(angle * index.toDouble() + angleOffset)).dp
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(heroAvatarSize)
|
||||||
|
.offset(
|
||||||
|
x = xOffset,
|
||||||
|
y = yOffset,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Avatar(
|
||||||
|
heroAvatar,
|
||||||
|
forcedAvatarSize = heroAvatarSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Avatars)
|
||||||
|
@Composable
|
||||||
|
internal fun CompositeAvatarPreview() = ElementThemedPreview {
|
||||||
|
val mainAvatar = anAvatarData(
|
||||||
|
id = "Zac",
|
||||||
|
name = "Zac",
|
||||||
|
size = AvatarSize.RoomListItem,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
repeat(6) { nbOfHeroes ->
|
||||||
|
CompositeAvatar(
|
||||||
|
avatarData = mainAvatar,
|
||||||
|
heroes = List(nbOfHeroes) { aHeroAvatarData(it) }.toPersistentList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aHeroAvatarData(i: Int) = anAvatarData(
|
||||||
|
id = ('A' + i).toString(),
|
||||||
|
name = ('A' + i).toString()
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.avatar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import io.element.android.libraries.designsystem.text.toPx
|
||||||
|
import io.element.android.libraries.testtags.TestTags
|
||||||
|
import io.element.android.libraries.testtags.testTag
|
||||||
|
|
||||||
|
/** Ratio between the box size (120 on Figma) and the avatar size (75 on Figma). */
|
||||||
|
private const val SIZE_RATIO = 1.6f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.figma.com/design/A2pAEvTEpJZBiOPUlcMnKi/Settings-%2B-Room-Details-(new)?node-id=1787-56333
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DmAvatars(
|
||||||
|
userAvatarData: AvatarData,
|
||||||
|
otherUserAvatarData: AvatarData,
|
||||||
|
openAvatarPreview: (url: String) -> Unit,
|
||||||
|
openOtherAvatarPreview: (url: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val boxSize = userAvatarData.size.dp * SIZE_RATIO
|
||||||
|
val boxSizePx = boxSize.toPx()
|
||||||
|
val otherAvatarRadius = otherUserAvatarData.size.dp.toPx() / 2
|
||||||
|
Box(
|
||||||
|
modifier = modifier.size(boxSize),
|
||||||
|
) {
|
||||||
|
// Draw user avatar and cut top right corner
|
||||||
|
Avatar(
|
||||||
|
avatarData = userAvatarData,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.graphicsLayer {
|
||||||
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
|
}
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawCircle(
|
||||||
|
color = Color.Black,
|
||||||
|
center = Offset(
|
||||||
|
x = boxSizePx - otherAvatarRadius,
|
||||||
|
y = size.height - (boxSizePx - otherAvatarRadius),
|
||||||
|
),
|
||||||
|
radius = otherAvatarRadius / 0.9f,
|
||||||
|
blendMode = BlendMode.Clear,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(enabled = userAvatarData.url != null) {
|
||||||
|
userAvatarData.url?.let { openAvatarPreview(it) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Draw other user avatar
|
||||||
|
Avatar(
|
||||||
|
avatarData = otherUserAvatarData,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(enabled = otherUserAvatarData.url != null) {
|
||||||
|
otherUserAvatarData.url?.let { openOtherAvatarPreview(it) }
|
||||||
|
}
|
||||||
|
.testTag(TestTags.memberDetailAvatar)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Avatars)
|
||||||
|
@Composable
|
||||||
|
internal fun DmAvatarsPreview() = ElementThemedPreview {
|
||||||
|
val size = AvatarSize.DmCluster
|
||||||
|
DmAvatars(
|
||||||
|
userAvatarData = anAvatarData(
|
||||||
|
id = "Alice",
|
||||||
|
name = "Alice",
|
||||||
|
size = size,
|
||||||
|
),
|
||||||
|
otherUserAvatarData = anAvatarData(
|
||||||
|
id = "Bob",
|
||||||
|
name = "Bob",
|
||||||
|
size = size,
|
||||||
|
),
|
||||||
|
openAvatarPreview = {},
|
||||||
|
openOtherAvatarPreview = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,16 @@
|
||||||
<string name="state_event_avatar_changed_too">"(avatar ändrades också)"</string>
|
<string name="state_event_avatar_changed_too">"(avatar ändrades också)"</string>
|
||||||
<string name="state_event_avatar_url_changed">"%1$s bytte sin avatar"</string>
|
<string name="state_event_avatar_url_changed">"%1$s bytte sin avatar"</string>
|
||||||
<string name="state_event_avatar_url_changed_by_you">"Du bytte din avatar"</string>
|
<string name="state_event_avatar_url_changed_by_you">"Du bytte din avatar"</string>
|
||||||
|
<string name="state_event_demoted_to_member">"%1$s degraderades till medlem"</string>
|
||||||
|
<string name="state_event_demoted_to_moderator">"%1$s degraderades till moderator"</string>
|
||||||
<string name="state_event_display_name_changed_from">"%1$s bytte sitt visningsnamn från %2$s till %3$s"</string>
|
<string name="state_event_display_name_changed_from">"%1$s bytte sitt visningsnamn från %2$s till %3$s"</string>
|
||||||
<string name="state_event_display_name_changed_from_by_you">"Du bytte ditt visningsnamn från %1$s till %2$s"</string>
|
<string name="state_event_display_name_changed_from_by_you">"Du bytte ditt visningsnamn från %1$s till %2$s"</string>
|
||||||
<string name="state_event_display_name_removed">"%1$s tog bort sitt visningsnamn (det var %2$s)"</string>
|
<string name="state_event_display_name_removed">"%1$s tog bort sitt visningsnamn (det var %2$s)"</string>
|
||||||
<string name="state_event_display_name_removed_by_you">"Du tog bort ditt visningsnamn (det var %1$s)"</string>
|
<string name="state_event_display_name_removed_by_you">"Du tog bort ditt visningsnamn (det var %1$s)"</string>
|
||||||
<string name="state_event_display_name_set">"%1$s satte sitt visningsnamn till %2$s"</string>
|
<string name="state_event_display_name_set">"%1$s satte sitt visningsnamn till %2$s"</string>
|
||||||
<string name="state_event_display_name_set_by_you">"Du satte ditt visningsnamn till %1$s"</string>
|
<string name="state_event_display_name_set_by_you">"Du satte ditt visningsnamn till %1$s"</string>
|
||||||
|
<string name="state_event_promoted_to_administrator">"%1$s befordrades till admin"</string>
|
||||||
|
<string name="state_event_promoted_to_moderator">"%1$s befordrades till moderator"</string>
|
||||||
<string name="state_event_room_avatar_changed">"%1$s bytte rummets avatar"</string>
|
<string name="state_event_room_avatar_changed">"%1$s bytte rummets avatar"</string>
|
||||||
<string name="state_event_room_avatar_changed_by_you">"Du bytte rummets avatar"</string>
|
<string name="state_event_room_avatar_changed_by_you">"Du bytte rummets avatar"</string>
|
||||||
<string name="state_event_room_avatar_removed">"%1$s tog bort rummets avatar"</string>
|
<string name="state_event_room_avatar_removed">"%1$s tog bort rummets avatar"</string>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
|
|
||||||
|
|
@ -49,5 +50,6 @@ data class MatrixRoomInfo(
|
||||||
val notificationCount: Long,
|
val notificationCount: Long,
|
||||||
val userDefinedNotificationMode: RoomNotificationMode?,
|
val userDefinedNotificationMode: RoomNotificationMode?,
|
||||||
val hasRoomCall: Boolean,
|
val hasRoomCall: Boolean,
|
||||||
val activeRoomCallParticipants: ImmutableList<String>
|
val activeRoomCallParticipants: ImmutableList<String>,
|
||||||
|
val heroes: ImmutableList<MatrixUser>,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,7 @@ fun MatrixRoomMembersState.roomMembers(): List<RoomMember>? {
|
||||||
fun MatrixRoomMembersState.joinedRoomMembers(): List<RoomMember> {
|
fun MatrixRoomMembersState.joinedRoomMembers(): List<RoomMember> {
|
||||||
return roomMembers().orEmpty().filter { it.membership == RoomMembershipState.JOIN }
|
return roomMembers().orEmpty().filter { it.membership == RoomMembershipState.JOIN }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MatrixRoomMembersState.activeRoomMembers(): List<RoomMember> {
|
||||||
|
return roomMembers().orEmpty().filter { it.membership.isActive() }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.libraries.matrix.api.room.powerlevels
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
import io.element.android.libraries.matrix.api.room.activeRoomMembers
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -27,14 +27,13 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a flow of the list of room members who are still in the room (with membership == RoomMembershipState.JOIN)
|
* Return a flow of the list of active room members who have the given role.
|
||||||
* and who have the given role.
|
|
||||||
*/
|
*/
|
||||||
fun MatrixRoom.usersWithRole(role: RoomMember.Role): Flow<ImmutableList<RoomMember>> {
|
fun MatrixRoom.usersWithRole(role: RoomMember.Role): Flow<ImmutableList<RoomMember>> {
|
||||||
return roomInfoFlow
|
return roomInfoFlow
|
||||||
.map { it.userPowerLevels.filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } }
|
.map { it.userPowerLevels.filter { (_, powerLevel) -> RoomMember.Role.forPowerLevel(powerLevel) == role } }
|
||||||
.combine(membersStateFlow) { powerLevels, membersState ->
|
.combine(membersStateFlow) { powerLevels, membersState ->
|
||||||
membersState.joinedRoomMembers()
|
membersState.activeRoomMembers()
|
||||||
.filter { powerLevels.containsKey(it.userId) }
|
.filter { powerLevels.containsKey(it.userId) }
|
||||||
.toPersistentList()
|
.toPersistentList()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
sealed interface RoomSummary {
|
sealed interface RoomSummary {
|
||||||
data class Empty(val identifier: String) : RoomSummary
|
data class Empty(val identifier: String) : RoomSummary
|
||||||
|
|
@ -52,6 +53,7 @@ data class RoomSummaryDetails(
|
||||||
val isDm: Boolean,
|
val isDm: Boolean,
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
val currentUserMembership: CurrentUserMembership,
|
val currentUserMembership: CurrentUserMembership,
|
||||||
|
val heroes: List<MatrixUser>,
|
||||||
) {
|
) {
|
||||||
val lastMessageTimestamp = lastMessage?.originServerTs
|
val lastMessageTimestamp = lastMessage?.originServerTs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,10 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface LocalEventSendState {
|
sealed interface LocalEventSendState {
|
||||||
data object NotSentYet : LocalEventSendState
|
data object NotSentYet : LocalEventSendState
|
||||||
|
sealed class SendingFailed(open val error: String) : LocalEventSendState {
|
||||||
data class SendingFailed(
|
data class Recoverable(override val error: String) : SendingFailed(error)
|
||||||
val error: String
|
data class Unrecoverable(override val error: String) : SendingFailed(error)
|
||||||
) : LocalEventSendState
|
}
|
||||||
|
|
||||||
data class Sent(
|
data class Sent(
|
||||||
val eventId: EventId
|
val eventId: EventId
|
||||||
) : LocalEventSendState
|
) : LocalEventSendState
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,12 @@ import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentMap
|
import kotlinx.collections.immutable.toPersistentMap
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomHero
|
||||||
import org.matrix.rustcomponents.sdk.Membership as RustMembership
|
import org.matrix.rustcomponents.sdk.Membership as RustMembership
|
||||||
import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo
|
import org.matrix.rustcomponents.sdk.RoomInfo as RustRoomInfo
|
||||||
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
|
import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode
|
||||||
|
|
@ -55,7 +57,8 @@ class MatrixRoomInfoMapper {
|
||||||
notificationCount = it.notificationCount.toLong(),
|
notificationCount = it.notificationCount.toLong(),
|
||||||
userDefinedNotificationMode = it.userDefinedNotificationMode?.map(),
|
userDefinedNotificationMode = it.userDefinedNotificationMode?.map(),
|
||||||
hasRoomCall = it.hasRoomCall,
|
hasRoomCall = it.hasRoomCall,
|
||||||
activeRoomCallParticipants = it.activeRoomCallParticipants.toImmutableList()
|
activeRoomCallParticipants = it.activeRoomCallParticipants.toImmutableList(),
|
||||||
|
heroes = it.elementHeroes().toImmutableList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +75,15 @@ fun RustRoomNotificationMode.map(): RoomNotificationMode = when (this) {
|
||||||
RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE
|
RustRoomNotificationMode.MUTE -> RoomNotificationMode.MUTE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a RoomHero to a MatrixUser. There is not need to create a RoomHero type on the application side.
|
||||||
|
*/
|
||||||
|
fun RoomHero.map(): MatrixUser = MatrixUser(
|
||||||
|
userId = UserId(userId),
|
||||||
|
displayName = displayName,
|
||||||
|
avatarUrl = avatarUrl
|
||||||
|
)
|
||||||
|
|
||||||
fun mapPowerLevels(powerLevels: Map<String, Long>): ImmutableMap<UserId, Long> {
|
fun mapPowerLevels(powerLevels: Map<String, Long>): ImmutableMap<UserId, Long> {
|
||||||
return powerLevels.mapKeys { (key, _) -> UserId(key) }.toPersistentMap()
|
return powerLevels.mapKeys { (key, _) -> UserId(key) }.toPersistentMap()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.room
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the heroes from the room info.
|
||||||
|
* For now we only use heroes for direct rooms with 2 members.
|
||||||
|
* Also we keep the heroes only if there is one single hero.
|
||||||
|
*/
|
||||||
|
fun RoomInfo.elementHeroes(): List<MatrixUser> {
|
||||||
|
return heroes
|
||||||
|
.takeIf { isDirect && activeMembersCount.toLong() == 2L }
|
||||||
|
?.takeIf { it.size == 1 }
|
||||||
|
?.map { it.map() }
|
||||||
|
.orEmpty()
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper
|
import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper
|
||||||
|
import io.element.android.libraries.matrix.impl.room.elementHeroes
|
||||||
import io.element.android.libraries.matrix.impl.room.map
|
import io.element.android.libraries.matrix.impl.room.map
|
||||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
||||||
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
||||||
|
|
@ -49,6 +50,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
|
||||||
isDm = roomInfo.isDirect && roomInfo.activeMembersCount.toLong() == 2L,
|
isDm = roomInfo.isDirect && roomInfo.activeMembersCount.toLong() == 2L,
|
||||||
isFavorite = roomInfo.isFavourite,
|
isFavorite = roomInfo.isFavourite,
|
||||||
currentUserMembership = roomInfo.membership.map(),
|
currentUserMembership = roomInfo.membership.map(),
|
||||||
|
heroes = roomInfo.elementHeroes(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,13 @@ fun RustEventSendState?.map(): LocalEventSendState? {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
null -> null
|
null -> null
|
||||||
RustEventSendState.NotSentYet -> LocalEventSendState.NotSentYet
|
RustEventSendState.NotSentYet -> LocalEventSendState.NotSentYet
|
||||||
is RustEventSendState.SendingFailed -> LocalEventSendState.SendingFailed(error)
|
is RustEventSendState.SendingFailed -> {
|
||||||
|
if (this.isRecoverable) {
|
||||||
|
LocalEventSendState.SendingFailed.Recoverable(this.error)
|
||||||
|
} else {
|
||||||
|
LocalEventSendState.SendingFailed.Unrecoverable(this.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
is RustEventSendState.Sent -> LocalEventSendState.Sent(EventId(eventId))
|
is RustEventSendState.Sent -> LocalEventSendState.Sent(EventId(eventId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerL
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
|
|
@ -764,7 +765,8 @@ fun aRoomInfo(
|
||||||
userDefinedNotificationMode: RoomNotificationMode? = null,
|
userDefinedNotificationMode: RoomNotificationMode? = null,
|
||||||
hasRoomCall: Boolean = false,
|
hasRoomCall: Boolean = false,
|
||||||
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
|
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
|
||||||
activeRoomCallParticipants: List<String> = emptyList()
|
activeRoomCallParticipants: List<String> = emptyList(),
|
||||||
|
heroes: List<MatrixUser> = emptyList(),
|
||||||
) = MatrixRoomInfo(
|
) = MatrixRoomInfo(
|
||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
|
|
@ -789,6 +791,7 @@ fun aRoomInfo(
|
||||||
hasRoomCall = hasRoomCall,
|
hasRoomCall = hasRoomCall,
|
||||||
userPowerLevels = userPowerLevels,
|
userPowerLevels = userPowerLevels,
|
||||||
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
|
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
|
||||||
|
heroes = heroes.toImmutableList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun defaultRoomPowerLevels() = MatrixRoomPowerLevels(
|
fun defaultRoomPowerLevels() = MatrixRoomPowerLevels(
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
|
|
@ -78,6 +79,7 @@ fun aRoomSummaryDetails(
|
||||||
isDm: Boolean = false,
|
isDm: Boolean = false,
|
||||||
isFavorite: Boolean = false,
|
isFavorite: Boolean = false,
|
||||||
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
|
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
|
||||||
|
heroes: List<MatrixUser> = emptyList(),
|
||||||
) = RoomSummaryDetails(
|
) = RoomSummaryDetails(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
name = name,
|
name = name,
|
||||||
|
|
@ -95,6 +97,7 @@ fun aRoomSummaryDetails(
|
||||||
isDm = isDm,
|
isDm = isDm,
|
||||||
isFavorite = isFavorite,
|
isFavorite = isFavorite,
|
||||||
currentUserMembership = currentUserMembership,
|
currentUserMembership = currentUserMembership,
|
||||||
|
heroes = heroes,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomMessage(
|
fun aRoomMessage(
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ dependencies {
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
|
|
@ -62,4 +63,5 @@ dependencies {
|
||||||
testImplementation(libs.test.mockk)
|
testImplementation(libs.test.mockk)
|
||||||
testImplementation(libs.test.robolectric)
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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