Merge pull request #678 from vector-im/feature/bma/designRoomList
Iterate on design
This commit is contained in:
@@ -222,6 +222,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
override fun onRoomSettingsClicked(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomFlowNode.NavTarget.RoomDetails))
|
||||
}
|
||||
|
||||
override fun onReportBugClicked() {
|
||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||
}
|
||||
}
|
||||
roomListEntryPoint
|
||||
.nodeBuilder(this, buildContext)
|
||||
|
||||
@@ -45,6 +45,7 @@ dependencies {
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.deeplink)
|
||||
implementation(projects.libraries.mediapickers.api)
|
||||
implementation(projects.libraries.mediaupload.api)
|
||||
implementation(projects.libraries.usersearch.impl)
|
||||
|
||||
@@ -20,9 +20,9 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
@@ -40,7 +40,7 @@ fun SearchMultipleUsersResultItem(
|
||||
CheckableUnresolvedUserRow(
|
||||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = searchResult.matrixUser.userId.value,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
@@ -49,7 +49,7 @@ fun SearchMultipleUsersResultItem(
|
||||
checked = isUserSelected,
|
||||
modifier = modifier,
|
||||
matrixUser = searchResult.matrixUser,
|
||||
avatarSize = AvatarSize.Custom(36.dp),
|
||||
avatarSize = AvatarSize.UserListItem,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
@@ -63,8 +63,11 @@ internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { Con
|
||||
private fun ContentToPreview() {
|
||||
Column {
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
|
||||
Divider()
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
|
||||
Divider()
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
|
||||
Divider()
|
||||
SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
@@ -39,26 +39,27 @@ fun SearchSingleUserResultItem(
|
||||
if (searchResult.isUnresolved) {
|
||||
UnresolvedUserRow(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
|
||||
avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = searchResult.matrixUser.userId.value,
|
||||
)
|
||||
} else {
|
||||
MatrixUserRow(
|
||||
modifier = modifier.clickable(onClick = onClick),
|
||||
matrixUser = searchResult.matrixUser,
|
||||
avatarSize = AvatarSize.Custom(36.dp),
|
||||
avatarSize = AvatarSize.UserListItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview{ ContentToPreview() }
|
||||
internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column {
|
||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
|
||||
Divider()
|
||||
SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
import android.content.Context
|
||||
import android.app.Activity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -29,24 +29,18 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class CreateRoomRootNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: CreateRoomRootPresenter,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
interface Callback : Plugin {
|
||||
@@ -73,31 +67,18 @@ class CreateRoomRootNode @AssistedInject constructor(
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val context = LocalContext.current
|
||||
val activity = LocalContext.current as Activity
|
||||
CreateRoomRootView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onClosePressed = this::navigateUp,
|
||||
onNewRoomClicked = callback::onCreateNewRoom,
|
||||
onOpenDM = callback::onStartChatSuccess,
|
||||
onInviteFriendsClicked = { invitePeople(context) },
|
||||
onInviteFriendsClicked = { invitePeople(activity) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun invitePeople(context: Context) {
|
||||
val permalinkResult = PermalinkBuilder.permalinkForUser(matrixClient.sessionId)
|
||||
permalinkResult.onSuccess { permalink ->
|
||||
val appName = buildMeta.applicationName
|
||||
startSharePlainTextIntent(
|
||||
context = context,
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = context.getString(CommonStrings.action_invite_friends),
|
||||
text = context.getString(CommonStrings.invite_friends_text, appName, permalink),
|
||||
extraTitle = context.getString(CommonStrings.invite_friends_rich_title, appName),
|
||||
noActivityFoundMessage = context.getString(io.element.android.libraries.androidutils.R.string.error_no_compatible_app_found)
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
private fun invitePeople(activity: Activity) {
|
||||
inviteFriendsUseCase.execute(activity)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummary
|
||||
@@ -158,12 +159,14 @@ class InviteListPresenter @Inject constructor(
|
||||
id = i.userId.value,
|
||||
name = i.displayName,
|
||||
url = i.avatarUrl,
|
||||
size = AvatarSize.RoomInviteItem,
|
||||
)
|
||||
else
|
||||
AvatarData(
|
||||
id = roomId.value,
|
||||
name = name,
|
||||
url = avatarURLString
|
||||
url = avatarURLString,
|
||||
size = AvatarSize.RoomInviteItem,
|
||||
)
|
||||
|
||||
val alias = if (isDirect)
|
||||
@@ -186,6 +189,7 @@ class InviteListPresenter @Inject constructor(
|
||||
id = userId.value,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.InviteSender,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package io.element.android.features.invitelist.impl.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -28,14 +27,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -52,6 +48,7 @@ import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummaryProvider
|
||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
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.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
@@ -74,8 +71,8 @@ internal fun InviteSummaryRow(
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeight)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeight)
|
||||
) {
|
||||
DefaultInviteSummaryRow(
|
||||
invite = invite,
|
||||
@@ -93,20 +90,20 @@ internal fun DefaultInviteSummaryRow(
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Avatar(
|
||||
invite.roomAvatarData.copy(size = AvatarSize.Custom(52.dp)),
|
||||
invite.roomAvatarData,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.alignByBaseline()
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.alignByBaseline()
|
||||
.weight(1f)
|
||||
) {
|
||||
val bonusPadding = if (invite.isNew) 12.dp else 0.dp
|
||||
|
||||
@@ -145,7 +142,9 @@ internal fun DefaultInviteSummaryRow(
|
||||
OutlinedButton(
|
||||
content = { Text(stringResource(CommonStrings.action_decline), style = ElementTextStyles.Button) },
|
||||
onClick = onDeclineClicked,
|
||||
modifier = Modifier.weight(1f).heightIn(max = 36.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.heightIn(max = 36.dp),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
|
||||
)
|
||||
|
||||
@@ -154,20 +153,16 @@ internal fun DefaultInviteSummaryRow(
|
||||
Button(
|
||||
content = { Text(stringResource(CommonStrings.action_accept), style = ElementTextStyles.Button) },
|
||||
onClick = onAcceptClicked,
|
||||
modifier = Modifier.weight(1f).heightIn(max = 36.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.heightIn(max = 36.dp),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val unreadIndicatorColor = if (invite.isNew) MaterialTheme.roomListUnreadIndicator() else Color.Transparent
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(unreadIndicatorColor)
|
||||
)
|
||||
UnreadIndicatorAtom(color = unreadIndicatorColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +173,7 @@ private fun SenderRow(sender: InviteSender) {
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
|
||||
avatarData = sender.avatarData,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.screen_invites_invited_you, sender.displayName, sender.userId.value).let { text ->
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.element.android.features.invitelist.impl.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
@@ -26,14 +27,14 @@ data class InviteListInviteSummary(
|
||||
val roomId: RoomId,
|
||||
val roomName: String = "",
|
||||
val roomAlias: String? = null,
|
||||
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName),
|
||||
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName, size = AvatarSize.RoomInviteItem),
|
||||
val sender: InviteSender? = null,
|
||||
val isDirect: Boolean = false,
|
||||
val isNew: Boolean = false,
|
||||
)
|
||||
|
||||
data class InviteSender(
|
||||
data class InviteSender constructor(
|
||||
val userId: UserId,
|
||||
val displayName: String,
|
||||
val avatarData: AvatarData = AvatarData(userId.value, displayName),
|
||||
val avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.features.invitelist.test.FakeSeenInvitesStore
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
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.RoomMembershipState
|
||||
@@ -36,8 +37,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@@ -91,6 +92,7 @@ class InviteListPresenterTests {
|
||||
id = A_USER_ID.value,
|
||||
name = A_USER_NAME,
|
||||
url = AN_AVATAR_URL,
|
||||
size = AvatarSize.RoomInviteItem,
|
||||
)
|
||||
)
|
||||
Truth.assertThat(withInviteState.inviteList[0].sender).isNull()
|
||||
@@ -119,6 +121,7 @@ class InviteListPresenterTests {
|
||||
id = A_USER_ID.value,
|
||||
name = A_USER_NAME,
|
||||
url = AN_AVATAR_URL,
|
||||
size = AvatarSize.InviteSender,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
id = room.roomId.value,
|
||||
name = room.name,
|
||||
url = room.avatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
size = AvatarSize.TimelineRoom
|
||||
)
|
||||
roomName.value = room.name
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.components.retrysendme
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
|
||||
@@ -42,7 +43,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||
fun aMessagesState() = MessagesState(
|
||||
roomId = RoomId("!id:domain"),
|
||||
roomName = "Room name",
|
||||
roomAvatar = AvatarData("!id:domain", "Room name"),
|
||||
roomAvatar = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom),
|
||||
userHasPermissionToSendMessage = true,
|
||||
composerState = aMessageComposerState().copy(
|
||||
text = StableCharSequence("Hello"),
|
||||
|
||||
@@ -19,6 +19,10 @@ package io.element.android.features.messages.impl.actionlist
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
@@ -29,15 +33,27 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
anActionListState().copy(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(),
|
||||
actions = persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Redact,
|
||||
)
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemImageContent()),
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemVideoContent()),
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
anActionListState().copy(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(content = aTimelineItemFileContent()),
|
||||
actions = aTimelineItemActionList(),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,3 +61,15 @@ fun anActionListState() = ActionListState(
|
||||
target = ActionListState.Target.None,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
|
||||
return persistentListOf(
|
||||
TimelineItemAction.Reply,
|
||||
TimelineItemAction.Forward,
|
||||
TimelineItemAction.Copy,
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.Redact,
|
||||
TimelineItemAction.ReportContent,
|
||||
TimelineItemAction.Developer,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,27 +42,22 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
@@ -74,6 +69,7 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.text.toSp
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
@@ -169,9 +165,11 @@ private fun SheetContent(
|
||||
) {
|
||||
item {
|
||||
Column {
|
||||
MessageSummary(event = target.event, modifier = Modifier
|
||||
MessageSummary(
|
||||
event = target.event, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp))
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
Divider()
|
||||
}
|
||||
@@ -214,10 +212,10 @@ private fun SheetContent(
|
||||
@Composable
|
||||
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
|
||||
val content: @Composable () -> Unit
|
||||
var icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.SMALL)) }
|
||||
var icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) }
|
||||
val contentStyle = ElementTextStyles.Regular.bodyMD.copy(color = MaterialTheme.colorScheme.secondary)
|
||||
val imageModifier = Modifier
|
||||
.size(36.dp)
|
||||
.size(AvatarSize.MessageActionSender.dp)
|
||||
.clip(RoundedCornerShape(9.dp))
|
||||
|
||||
@Composable
|
||||
@@ -232,7 +230,6 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||
when (event.content) {
|
||||
is TimelineItemTextBasedContent,
|
||||
is TimelineItemStateContent,
|
||||
is TimelineItemProfileChangeContent,
|
||||
is TimelineItemEncryptedContent,
|
||||
is TimelineItemRedactedContent,
|
||||
is TimelineItemUnknownContent -> content = { ContentForBody(textContent) }
|
||||
@@ -282,7 +279,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||
Row(modifier = modifier) {
|
||||
icon()
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row {
|
||||
if (event.senderDisplayName != null) {
|
||||
Text(
|
||||
@@ -291,16 +288,16 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
event.sentTime,
|
||||
style = ElementTextStyles.Regular.caption2,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.End,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
content()
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
event.sentTime,
|
||||
style = ElementTextStyles.Regular.caption2,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +346,7 @@ private fun EmojiButton(
|
||||
) {
|
||||
Text(
|
||||
emoji,
|
||||
fontSize = 28.dpToSp(),
|
||||
fontSize = 28.dp.toSp(),
|
||||
modifier = modifier.clickable(
|
||||
enabled = true,
|
||||
onClick = { onClicked(emoji) },
|
||||
@@ -359,11 +356,6 @@ private fun EmojiButton(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Int.dpToSp(): TextUnit = with(LocalDensity.current) {
|
||||
return dp.toSp()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) =
|
||||
|
||||
@@ -21,13 +21,13 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
@@ -48,6 +48,7 @@ import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
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.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogDefaults
|
||||
@@ -227,18 +228,22 @@ internal fun RoomSummaryView(
|
||||
modifier = modifier
|
||||
.clickable { onSelection(summary) }
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.heightIn(56.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val roomAlias = summary.canonicalAlias ?: summary.roomId.value
|
||||
Avatar(
|
||||
avatarData = AvatarData(id = roomAlias, name = summary.name, url = summary.avatarURLString),
|
||||
avatarData = AvatarData(
|
||||
id = roomAlias,
|
||||
name = summary.name,
|
||||
url = summary.avatarURLString,
|
||||
size = AvatarSize.ForwardRoomListItem,
|
||||
),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 4.dp, top = 8.dp, bottom = 8.dp)
|
||||
.alignByBaseline()
|
||||
.padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
// Name
|
||||
|
||||
@@ -35,9 +35,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.text.roundToPx
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import me.saket.telephoto.zoomable.zoomable
|
||||
@@ -51,7 +51,7 @@ fun PdfViewer(
|
||||
modifier = modifier.zoomable(pdfViewerState.zoomableState),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val maxWidthInPx = maxWidth.dpToPx()
|
||||
val maxWidthInPx = maxWidth.roundToPx()
|
||||
DisposableEffect(pdfViewerState) {
|
||||
pdfViewerState.openForWidth(maxWidthInPx)
|
||||
onDispose {
|
||||
@@ -107,15 +107,9 @@ private fun PdfPageView(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(state.height.pxToDp())
|
||||
.height(state.height.toDp())
|
||||
.background(color = Color.White)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() }
|
||||
|
||||
@Composable
|
||||
private fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.roundToPx() }
|
||||
|
||||
@@ -23,7 +23,10 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
@@ -32,6 +35,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventSendStat
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlin.random.Random
|
||||
|
||||
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
|
||||
@@ -83,15 +88,23 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList
|
||||
content = content,
|
||||
groupPosition = TimelineItemGroupPosition.First
|
||||
),
|
||||
// A state event on top of it
|
||||
aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
groupPosition = TimelineItemGroupPosition.None
|
||||
),
|
||||
// A grouped event on top of it
|
||||
aGroupedEvents(),
|
||||
// A day separator
|
||||
aTimelineItemDaySeparator(),
|
||||
// Loading
|
||||
aTimelineItemLoading(),
|
||||
)
|
||||
}
|
||||
|
||||
fun aTimelineItemLoading(): TimelineItem.Virtual {
|
||||
return TimelineItem.Virtual("virtual_loading", TimelineItemLoadingModel)
|
||||
}
|
||||
|
||||
fun aTimelineItemDaySeparator(): TimelineItem.Virtual {
|
||||
return TimelineItem.Virtual("virtual_day", aTimelineItemDaySeparatorModel("Today"))
|
||||
}
|
||||
|
||||
internal fun aTimelineItemEvent(
|
||||
eventId: EventId = EventId("\$" + Random.nextInt().toString()),
|
||||
transactionId: String? = null,
|
||||
@@ -101,22 +114,19 @@ internal fun aTimelineItemEvent(
|
||||
sendState: EventSendState = EventSendState.Sent(eventId),
|
||||
inReplyTo: InReplyTo? = null,
|
||||
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
||||
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
|
||||
): TimelineItem.Event {
|
||||
return TimelineItem.Event(
|
||||
id = eventId.value,
|
||||
eventId = eventId,
|
||||
transactionId = transactionId,
|
||||
senderId = UserId("@senderId:domain"),
|
||||
senderAvatar = AvatarData("@senderId:domain", "sender"),
|
||||
senderAvatar = AvatarData("@senderId:domain", "sender", size = AvatarSize.TimelineSender),
|
||||
content = content,
|
||||
reactionsState = TimelineItemReactions(
|
||||
persistentListOf(
|
||||
AggregatedReaction("👍", "1")
|
||||
)
|
||||
),
|
||||
reactionsState = timelineItemReactions,
|
||||
sentTime = "12:34",
|
||||
isMine = isMine,
|
||||
senderDisplayName = "sender",
|
||||
senderDisplayName = "Sender",
|
||||
groupPosition = groupPosition,
|
||||
sendState = sendState,
|
||||
inReplyTo = inReplyTo,
|
||||
@@ -124,6 +134,19 @@ internal fun aTimelineItemEvent(
|
||||
)
|
||||
}
|
||||
|
||||
fun aTimelineItemReactions(
|
||||
count: Int = 1,
|
||||
isHighlighted: Boolean = false,
|
||||
): TimelineItemReactions {
|
||||
return TimelineItemReactions(
|
||||
reactions = buildList {
|
||||
repeat(count) {
|
||||
add(AggregatedReaction(key = "👍", count = 1 + it, isHighlighted = isHighlighted))
|
||||
}
|
||||
}.toPersistentList()
|
||||
)
|
||||
}
|
||||
|
||||
internal fun aTimelineItemDebugInfo(
|
||||
model: String = "Rust(Model())",
|
||||
originalJson: String? = null,
|
||||
@@ -131,3 +154,17 @@ internal fun aTimelineItemDebugInfo(
|
||||
) = TimelineItemDebugInfo(
|
||||
model, originalJson, latestEditedJson
|
||||
)
|
||||
|
||||
fun aGroupedEvents(): TimelineItem.GroupedEvents {
|
||||
val event = aTimelineItemEvent(
|
||||
isMine = true,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
groupPosition = TimelineItemGroupPosition.None
|
||||
)
|
||||
return TimelineItem.GroupedEvents(
|
||||
events = listOf(
|
||||
event,
|
||||
event,
|
||||
).toImmutableList()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ fun TimelineView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
reverseLayout = true,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = state.timelineItems,
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -48,6 +49,9 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||
private val BUBBLE_RADIUS = 12.dp
|
||||
private val BUBBLE_INCOMING_OFFSET = 16.dp
|
||||
|
||||
// Design says: The maximum width of a bubble is still 3/4 of the screen width
|
||||
private const val BUBBLE_WIDTH_RATIO = 0.75f
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageEventBubble(
|
||||
@@ -103,21 +107,30 @@ fun MessageEventBubble(
|
||||
}
|
||||
}
|
||||
val bubbleShape = bubbleShape()
|
||||
Surface(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.widthIn(min = 80.dp)
|
||||
.offsetForItem()
|
||||
.clip(bubbleShape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(),
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
color = backgroundBubbleColor,
|
||||
shape = bubbleShape,
|
||||
content = content
|
||||
)
|
||||
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
|
||||
.padding(horizontal = 16.dp)
|
||||
.offsetForItem(),
|
||||
// Need to set the contentAlignment again (it's already set in TimelineItemEventRow), for the case
|
||||
// when content width is low.
|
||||
contentAlignment = if (state.isMine) Alignment.CenterEnd else Alignment.CenterStart
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.widthIn(min = 80.dp)
|
||||
.clip(bubbleShape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(),
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
color = backgroundBubbleColor,
|
||||
shape = bubbleShape,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package io.element.android.features.messages.impl.timeline.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -27,6 +29,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -37,23 +40,40 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
@Composable
|
||||
fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Modifier) {
|
||||
// First Surface is to render a border with the same background color as the background
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
// TODO Should use compound.bgSubtlePrimary
|
||||
color = ElementTheme.colors.gray300,
|
||||
border = BorderStroke(2.dp, MaterialTheme.colorScheme.background),
|
||||
shape = RoundedCornerShape(corner = CornerSize(12.dp)),
|
||||
shape = RoundedCornerShape(corner = CornerSize(14.dp)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 5.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// TODO `reaction.isHighlighted` is not used.
|
||||
Text(text = reaction.key, fontSize = 12.sp)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
|
||||
Box(modifier = Modifier.padding(2.dp)) {
|
||||
val reactionModifier = if (reaction.isHighlighted) {
|
||||
Modifier
|
||||
// TODO Check the color, should use compound.borderInteractivePrimary
|
||||
.border(BorderStroke(1.dp, Color(0xFF808994)), RoundedCornerShape(corner = CornerSize(12.dp)))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Row(
|
||||
modifier = reactionModifier.padding(vertical = 4.dp, horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = reaction.key, fontSize = 15.sp)
|
||||
if (reaction.count > 1) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = reaction.count.toString(),
|
||||
color = if (reaction.isHighlighted) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -27,11 +28,9 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -40,22 +39,33 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.LastBaseline
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.event.toExtraPadding
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
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.libraries.designsystem.ElementTextStyles
|
||||
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.AvatarData
|
||||
import io.element.android.libraries.theme.LocalColors
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
@@ -66,6 +76,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEventRow(
|
||||
@@ -89,66 +100,72 @@ fun TimelineItemEventRow(
|
||||
inReplyToClick(inReplyToEventId)
|
||||
}
|
||||
|
||||
val (parentAlignment, contentAlignment) = if (event.isMine) {
|
||||
Pair(Alignment.CenterEnd, Alignment.End)
|
||||
} else {
|
||||
Pair(Alignment.CenterStart, Alignment.Start)
|
||||
}
|
||||
|
||||
// To avoid using negative offset, we display in this Box a column with:
|
||||
// - Spacer to give room to the Sender information if they must be displayed;
|
||||
// - The message bubble;
|
||||
// - Spacer for the reactions if there are some.
|
||||
// Then the Sender information and the reactions are displayed on top of it.
|
||||
// This fixes some clickable issue and some unexpected margin on top and bottom of each message row
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
contentAlignment = parentAlignment
|
||||
contentAlignment = if (event.isMine) Alignment.CenterEnd else Alignment.CenterStart
|
||||
) {
|
||||
Row {
|
||||
Column(horizontalAlignment = contentAlignment) {
|
||||
if (event.showSenderInformation) {
|
||||
MessageSenderInformation(
|
||||
event.safeSenderName,
|
||||
event.senderAvatar,
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.offset(y = 12.dp)
|
||||
.clickable(onClick = ::onUserDataClicked)
|
||||
)
|
||||
}
|
||||
val bubbleState = BubbleState(
|
||||
groupPosition = event.groupPosition,
|
||||
isMine = event.isMine,
|
||||
isHighlighted = isHighlighted,
|
||||
)
|
||||
MessageEventBubble(
|
||||
state = bubbleState,
|
||||
Column {
|
||||
if (event.showSenderInformation) {
|
||||
Spacer(modifier = Modifier.height(event.senderAvatar.size.dp - 8.dp))
|
||||
}
|
||||
val bubbleState = BubbleState(
|
||||
groupPosition = event.groupPosition,
|
||||
isMine = event.isMine,
|
||||
isHighlighted = isHighlighted,
|
||||
)
|
||||
MessageEventBubble(
|
||||
state = bubbleState,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
) {
|
||||
MessageEventBubbleContent(
|
||||
event = event,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = Modifier
|
||||
.zIndex(-1f)
|
||||
.widthIn(max = 320.dp)
|
||||
) {
|
||||
MessageEventBubbleContent(
|
||||
event = event,
|
||||
interactionSource = interactionSource,
|
||||
onMessageClick = onClick,
|
||||
onMessageLongClick = onLongClick,
|
||||
inReplyToClick = ::inReplyToClicked,
|
||||
onTimestampClicked = {
|
||||
onTimestampClicked(event)
|
||||
}
|
||||
)
|
||||
}
|
||||
TimelineItemReactionsView(
|
||||
reactionsState = event.reactionsState,
|
||||
modifier = Modifier
|
||||
.zIndex(1f)
|
||||
.offset(x = if (event.isMine) 0.dp else 20.dp, y = -(4.dp))
|
||||
onMessageClick = onClick,
|
||||
onMessageLongClick = onLongClick,
|
||||
inReplyToClick = ::inReplyToClicked,
|
||||
onTimestampClicked = {
|
||||
onTimestampClicked(event)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
}
|
||||
}
|
||||
// Align to the top of the box
|
||||
if (event.showSenderInformation) {
|
||||
MessageSenderInformation(
|
||||
event.safeSenderName,
|
||||
event.senderAvatar,
|
||||
Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.align(Alignment.TopStart)
|
||||
.clickable(onClick = ::onUserDataClicked)
|
||||
)
|
||||
}
|
||||
// Align to the bottom of the box
|
||||
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||
TimelineItemReactionsView(
|
||||
reactionsState = event.reactionsState,
|
||||
modifier = Modifier
|
||||
.align(if (event.isMine) Alignment.BottomEnd else Alignment.BottomStart)
|
||||
.padding(start = if (event.isMine) 16.dp else 36.dp, end = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
// This is assuming that we are in a ColumnScope, but this is OK, for both Preview and real usage.
|
||||
if (event.groupPosition.isNew()) {
|
||||
Spacer(modifier = modifier.height(8.dp))
|
||||
Spacer(modifier = modifier.height(16.dp))
|
||||
} else {
|
||||
Spacer(modifier = modifier.height(2.dp))
|
||||
}
|
||||
@@ -157,20 +174,38 @@ fun TimelineItemEventRow(
|
||||
@Composable
|
||||
private fun MessageSenderInformation(
|
||||
sender: String,
|
||||
senderAvatar: AvatarData?,
|
||||
senderAvatar: AvatarData,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(modifier = modifier) {
|
||||
if (senderAvatar != null) {
|
||||
val avatarStrokeSize = 3.dp
|
||||
val avatarStrokeColor = MaterialTheme.colorScheme.background
|
||||
val avatarSize = senderAvatar.size.dp
|
||||
Box(
|
||||
modifier = modifier
|
||||
) {
|
||||
// Background of Avatar, to erase the corner of the message content
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.size(size = avatarSize + avatarStrokeSize)
|
||||
.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))
|
||||
Text(
|
||||
text = sender,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = sender,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier
|
||||
.alignBy(LastBaseline)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +231,7 @@ private fun MessageEventBubbleContent(
|
||||
interactionSource = interactionSource,
|
||||
onClick = onMessageClick,
|
||||
onLongClick = onMessageLongClick,
|
||||
extraPadding = event.toExtraPadding(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -221,14 +257,14 @@ private fun MessageEventBubbleContent(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column(modifier) {
|
||||
ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp))
|
||||
Box(modifier) {
|
||||
ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp))
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
modifier = timestampModifier
|
||||
.align(Alignment.End)
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -358,3 +394,91 @@ private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready) =
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
Column {
|
||||
sequenceOf(false, true).forEach {
|
||||
TimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = it,
|
||||
content = aTimelineItemTextContent().copy(
|
||||
body = "A long text which will be displayed on several lines and" +
|
||||
" hopefully can be manually adjusted to test different behaviors."
|
||||
)
|
||||
),
|
||||
isHighlighted = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onUserDataClick = {},
|
||||
inReplyToClick = {},
|
||||
onTimestampClicked = {},
|
||||
)
|
||||
TimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = it,
|
||||
content = aTimelineItemImageContent().copy(
|
||||
aspectRatio = 5f
|
||||
)
|
||||
),
|
||||
isHighlighted = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onUserDataClick = {},
|
||||
inReplyToClick = {},
|
||||
onTimestampClicked = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowTimestampLightPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) =
|
||||
ElementPreviewLight { ContentTimestampToPreview(event) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowTimestampDarkPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) =
|
||||
ElementPreviewDark { ContentTimestampToPreview(event) }
|
||||
|
||||
@Composable
|
||||
private fun ContentTimestampToPreview(event: TimelineItem.Event) {
|
||||
Column {
|
||||
val oldContent = event.content as TimelineItemTextContent
|
||||
listOf(
|
||||
"Text",
|
||||
"Text longer, displayed on 1 line",
|
||||
"Text which should be rendered on several lines",
|
||||
).forEach { str ->
|
||||
listOf(false, true).forEach { useDocument ->
|
||||
TimelineItemEventRow(
|
||||
event = event.copy(
|
||||
content = oldContent.copy(
|
||||
body = str,
|
||||
htmlDocument = if (useDocument) Jsoup.parse(str) else null,
|
||||
),
|
||||
reactionsState = aTimelineItemReactions(count = 0),
|
||||
senderDisplayName = if (useDocument) "Document case" else "Text case",
|
||||
),
|
||||
isHighlighted = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onUserDataClick = {},
|
||||
inReplyToClick = {},
|
||||
onTimestampClicked = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ fun TimelineItemReactionsView(
|
||||
reactionsState: TimelineItemReactions,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (reactionsState.reactions.isEmpty()) return
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
mainAxisSpacing = 2.dp,
|
||||
|
||||
@@ -25,11 +25,18 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
||||
@Composable
|
||||
fun TimelineItemStateEventRow(
|
||||
@@ -60,8 +67,33 @@ fun TimelineItemStateEventRow(
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
extraPadding = noExtraPadding,
|
||||
modifier = Modifier.defaultTimelineContentPadding()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemStateEventRowLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemStateEventRowDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TimelineItemStateEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
isMine = false,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
groupPosition = TimelineItemGroupPosition.None
|
||||
),
|
||||
isHighlighted = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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 androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.libraries.core.bool.orFalse
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
// Allow to not overlap the timestamp with the text, in the message bubble.
|
||||
// Compute the size of the worst case.
|
||||
data class ExtraPadding(val str: String)
|
||||
|
||||
val noExtraPadding = ExtraPadding("")
|
||||
|
||||
/**
|
||||
* See [io.element.android.features.messages.impl.timeline.components.TimelineEventTimestampView] for the related View.
|
||||
* And https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1819%253A99506 for the design.
|
||||
*/
|
||||
@Composable
|
||||
fun TimelineItem.Event.toExtraPadding(): ExtraPadding {
|
||||
val formattedTime = sentTime
|
||||
val hasMessageSendingFailed = sendState is EventSendState.SendingFailed
|
||||
val isMessageEdited = (content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
|
||||
|
||||
var strLen = 2
|
||||
if (isMessageEdited) {
|
||||
strLen += stringResource(id = CommonStrings.common_edited_suffix).length + 2
|
||||
}
|
||||
strLen += formattedTime.length
|
||||
if (hasMessageSendingFailed) {
|
||||
strLen += 5
|
||||
}
|
||||
// A space and a few unbreakable spaces
|
||||
return ExtraPadding(" " + "\u00A0".repeat(strLen))
|
||||
}
|
||||
@@ -17,10 +17,8 @@
|
||||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
@@ -35,6 +33,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
fun TimelineItemEventContentView(
|
||||
content: TimelineItemEventContent,
|
||||
interactionSource: MutableInteractionSource,
|
||||
extraPadding: ExtraPadding,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -42,14 +41,17 @@ fun TimelineItemEventContentView(
|
||||
when (content) {
|
||||
is TimelineItemEncryptedContent -> TimelineItemEncryptedView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemRedactedContent -> TimelineItemRedactedView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemTextBasedContent -> TimelineItemTextView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
onTextClicked = onClick,
|
||||
@@ -57,6 +59,7 @@ fun TimelineItemEventContentView(
|
||||
)
|
||||
is TimelineItemUnknownContent -> TimelineItemUnknownView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemImageContent -> TimelineItemImageView(
|
||||
@@ -69,6 +72,7 @@ fun TimelineItemEventContentView(
|
||||
)
|
||||
is TimelineItemFileContent -> TimelineItemFileView(
|
||||
content = content,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemStateContent -> TimelineItemStateView(
|
||||
|
||||
@@ -31,12 +31,14 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@Composable
|
||||
fun TimelineItemEncryptedView(
|
||||
content: TimelineItemEncryptedContent,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TimelineItemInformativeView(
|
||||
text = stringResource(id = CommonStrings.common_decryption_error),
|
||||
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
|
||||
icon = Icons.Default.Warning,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
@@ -56,6 +58,7 @@ private fun ContentToPreview() {
|
||||
TimelineItemEncryptedView(
|
||||
content = TimelineItemEncryptedContent(
|
||||
data = UnableToDecryptContent.Data.Unknown
|
||||
)
|
||||
),
|
||||
extraPadding = noExtraPadding
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||
@Composable
|
||||
fun TimelineItemFileView(
|
||||
content: TimelineItemFileContent,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
@@ -76,7 +77,7 @@ fun TimelineItemFileView(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = content.fileExtensionAndSize,
|
||||
text = content.fileExtensionAndSize + extraPadding.str,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
@@ -98,5 +99,8 @@ internal fun TimelineItemFileViewDarkPreview(@PreviewParameter(TimelineItemFileC
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(content: TimelineItemFileContent) {
|
||||
TimelineItemFileView(content)
|
||||
TimelineItemFileView(
|
||||
content,
|
||||
extraPadding = noExtraPadding,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ fun TimelineItemInformativeView(
|
||||
text: String,
|
||||
iconDescription: String,
|
||||
icon: ImageVector,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
@@ -58,7 +59,7 @@ fun TimelineItemInformativeView(
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
fontSize = 14.sp,
|
||||
text = text
|
||||
text = text + extraPadding.str
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -76,6 +77,7 @@ private fun ContentToPreview() {
|
||||
TimelineItemInformativeView(
|
||||
text = "Info",
|
||||
iconDescription = "",
|
||||
icon = Icons.Default.Delete
|
||||
icon = Icons.Default.Delete,
|
||||
extraPadding = noExtraPadding,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,12 +30,14 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@Composable
|
||||
fun TimelineItemRedactedView(
|
||||
content: TimelineItemRedactedContent,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TimelineItemInformativeView(
|
||||
text = stringResource(id = CommonStrings.common_message_removed),
|
||||
iconDescription = stringResource(id = CommonStrings.common_message_removed),
|
||||
icon = Icons.Default.Delete,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
@@ -52,5 +54,8 @@ internal fun TimelineItemRedactedViewDarkPreview() =
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TimelineItemRedactedView(TimelineItemRedactedContent)
|
||||
TimelineItemRedactedView(
|
||||
TimelineItemRedactedContent,
|
||||
extraPadding = noExtraPadding
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import android.text.util.Linkify.PHONE_NUMBERS
|
||||
import android.text.util.Linkify.WEB_URLS
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -29,6 +32,7 @@ import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
import io.element.android.features.messages.impl.timeline.components.html.HtmlDocument
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
@@ -37,30 +41,37 @@ import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.theme.LinkColor
|
||||
import io.element.android.libraries.designsystem.text.toAnnotatedString
|
||||
|
||||
@Composable
|
||||
fun TimelineItemTextView(
|
||||
content: TimelineItemTextBasedContent,
|
||||
interactionSource: MutableInteractionSource,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier,
|
||||
onTextClicked: () -> Unit = {},
|
||||
onTextLongClicked: () -> Unit = {},
|
||||
) {
|
||||
val htmlDocument = content.htmlDocument
|
||||
if (htmlDocument != null) {
|
||||
HtmlDocument(
|
||||
document = htmlDocument,
|
||||
modifier = modifier,
|
||||
onTextClicked = onTextClicked,
|
||||
onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
// For now we ignore the extra padding for html content, so add some spacing
|
||||
// below the content (as previous behavior)
|
||||
Column(modifier = modifier) {
|
||||
HtmlDocument(
|
||||
document = htmlDocument,
|
||||
modifier = Modifier,
|
||||
onTextClicked = onTextClicked,
|
||||
onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
} else {
|
||||
Box(modifier) {
|
||||
val linkStyle = SpanStyle(
|
||||
color = LinkColor,
|
||||
)
|
||||
val styledText = remember(content.body) { content.body.linkify(linkStyle) }
|
||||
val styledText = remember(content.body) { content.body.linkify(linkStyle) + extraPadding.str.toAnnotatedString() }
|
||||
ClickableLinkText(
|
||||
text = styledText,
|
||||
linkAnnotationTag = "URL",
|
||||
@@ -109,6 +120,10 @@ internal fun TimelineItemTextViewDarkPreview(@PreviewParameter(TimelineItemTextB
|
||||
|
||||
@Composable
|
||||
fun ContentToPreview(content: TimelineItemTextBasedContent) {
|
||||
TimelineItemTextView(content, MutableInteractionSource())
|
||||
TimelineItemTextView(
|
||||
content = content,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
extraPadding = ExtraPadding(" (padding)"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,12 +30,14 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@Composable
|
||||
fun TimelineItemUnknownView(
|
||||
content: TimelineItemUnknownContent,
|
||||
extraPadding: ExtraPadding,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TimelineItemInformativeView(
|
||||
text = stringResource(id = CommonStrings.common_unsupported_event),
|
||||
iconDescription = stringResource(id = CommonStrings.dialog_title_warning),
|
||||
icon = Icons.Default.Info,
|
||||
extraPadding = extraPadding,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
@@ -52,5 +54,8 @@ internal fun TimelineItemUnknownViewDarkPreview() =
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
TimelineItemUnknownView(TimelineItemUnknownContent)
|
||||
TimelineItemUnknownView(
|
||||
content = TimelineItemUnknownContent,
|
||||
extraPadding = noExtraPadding
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -39,30 +40,31 @@ internal fun TimelineItemDaySeparatorView(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(8.dp),
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = model.formattedDate,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemDaySeparatorViewLightPreview(@PreviewParameter(
|
||||
TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
||||
internal fun TimelineItemDaySeparatorViewLightPreview(
|
||||
@PreviewParameter(TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
||||
) =
|
||||
ElementPreviewLight { ContentToPreview(model) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemDaySeparatorViewDarkPreview(@PreviewParameter(
|
||||
TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
||||
internal fun TimelineItemDaySeparatorViewDarkPreview(
|
||||
@PreviewParameter(TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel
|
||||
) =
|
||||
ElementPreviewDark { ContentToPreview(model) }
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
|
||||
import io.element.android.libraries.core.bool.orTrue
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
|
||||
@@ -34,6 +35,7 @@ import javax.inject.Inject
|
||||
|
||||
class TimelineItemEventFactory @Inject constructor(
|
||||
private val contentFactory: TimelineItemContentFactory,
|
||||
private val matrixClient: MatrixClient,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
@@ -67,7 +69,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||
id = currentSender.value,
|
||||
name = senderDisplayName ?: currentSender.value,
|
||||
url = senderAvatarUrl,
|
||||
size = AvatarSize.SMALL
|
||||
size = AvatarSize.TimelineSender
|
||||
)
|
||||
return TimelineItem.Event(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
@@ -89,8 +91,13 @@ class TimelineItemEventFactory @Inject constructor(
|
||||
|
||||
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
|
||||
val aggregatedReactions = event.reactions.map {
|
||||
AggregatedReaction(key = it.key, count = it.count.toString(), isHighlighted = false)
|
||||
AggregatedReaction(
|
||||
key = it.key,
|
||||
count = it.count.toInt(),
|
||||
isHighlighted = it.senderIds.contains(matrixClient.sessionId),
|
||||
)
|
||||
}
|
||||
aggregatedReactions.sortedByDescending { it.count }
|
||||
return TimelineItemReactions(aggregatedReactions.toImmutableList())
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,13 @@
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.model
|
||||
|
||||
/**
|
||||
* @property key the reaction key (e.g. "👍")
|
||||
* @property count the number of users who reacted with this key
|
||||
* @property isHighlighted true if the reaction has (also) been sent by the current user.
|
||||
*/
|
||||
data class AggregatedReaction(
|
||||
val key: String,
|
||||
val count: String,
|
||||
val count: Int,
|
||||
val isHighlighted: Boolean = false
|
||||
)
|
||||
|
||||
@@ -20,16 +20,20 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
open class AggregatedReactionProvider : PreviewParameterProvider<AggregatedReaction> {
|
||||
override val values: Sequence<AggregatedReaction>
|
||||
get() = sequenceOf(
|
||||
anAggregatedReaction(),
|
||||
anAggregatedReaction().copy(count = "88"),
|
||||
anAggregatedReaction().copy(isHighlighted = true),
|
||||
anAggregatedReaction().copy(count = "88", isHighlighted = true),
|
||||
)
|
||||
get() = sequenceOf(false, true).flatMap {
|
||||
sequenceOf(
|
||||
anAggregatedReaction(isHighlighted = it),
|
||||
anAggregatedReaction(isHighlighted = it, count = 88),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun anAggregatedReaction() = AggregatedReaction(
|
||||
key = "👍",
|
||||
count = "1", // TODO Why is it a String?
|
||||
isHighlighted = false,
|
||||
fun anAggregatedReaction(
|
||||
key: String = "👍",
|
||||
count: Int = 1,
|
||||
isHighlighted: Boolean = false,
|
||||
) = AggregatedReaction(
|
||||
key = key,
|
||||
count = count,
|
||||
isHighlighted = isHighlighted,
|
||||
)
|
||||
|
||||
@@ -39,11 +39,11 @@ class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEv
|
||||
class TimelineItemTextBasedContentProvider : PreviewParameterProvider<TimelineItemTextBasedContent> {
|
||||
override val values = sequenceOf(
|
||||
aTimelineItemEmoteContent(),
|
||||
aTimelineItemEmoteContent().copy(htmlDocument = Jsoup.parse("Emote")),
|
||||
aTimelineItemEmoteContent().copy(htmlDocument = Jsoup.parse("Emote Document")),
|
||||
aTimelineItemNoticeContent(),
|
||||
aTimelineItemNoticeContent().copy(htmlDocument = Jsoup.parse("Notice")),
|
||||
aTimelineItemNoticeContent().copy(htmlDocument = Jsoup.parse("Notice Document")),
|
||||
aTimelineItemTextContent(),
|
||||
aTimelineItemTextContent().copy(htmlDocument = Jsoup.parse("Text")),
|
||||
aTimelineItemTextContent().copy(htmlDocument = Jsoup.parse("Text Document")),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ open class TimelineItemFileContentProvider : PreviewParameterProvider<TimelineIt
|
||||
)
|
||||
}
|
||||
|
||||
fun aTimelineItemFileContent(fileName: String) = TimelineItemFileContent(
|
||||
fun aTimelineItemFileContent(fileName: String = "A file.pdf") = TimelineItemFileContent(
|
||||
body = fileName,
|
||||
thumbnailSource = MediaSource(url = ""),
|
||||
fileSource = MediaSource(url = ""),
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
|
||||
package io.element.android.features.messages.fixtures
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||
@@ -30,7 +31,6 @@ import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.room.aTimelineItemDebugInfo
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal fun aMessageEvent(
|
||||
eventId: EventId? = AN_EVENT_ID,
|
||||
@@ -43,11 +43,11 @@ internal fun aMessageEvent(
|
||||
eventId = eventId,
|
||||
senderId = A_USER_ID,
|
||||
senderDisplayName = A_USER_NAME,
|
||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
|
||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME, size = AvatarSize.TimelineSender),
|
||||
content = content,
|
||||
sentTime = "",
|
||||
isMine = isMine,
|
||||
reactionsState = TimelineItemReactions(persistentListOf()),
|
||||
reactionsState = aTimelineItemReactions(count = 0),
|
||||
sendState = EventSendState.Sent(AN_EVENT_ID),
|
||||
inReplyTo = inReplyTo,
|
||||
debugInfo = debugInfo,
|
||||
|
||||
@@ -36,6 +36,7 @@ import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
||||
@@ -44,7 +45,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||
return TimelineItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
eventItemFactory = TimelineItemEventFactory(
|
||||
TimelineItemContentFactory(
|
||||
contentFactory = TimelineItemContentFactory(
|
||||
messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()),
|
||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||
stickerFactory = TimelineItemContentStickerFactory(),
|
||||
@@ -54,7 +55,8 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
|
||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||
)
|
||||
),
|
||||
matrixClient = FakeMatrixClient(),
|
||||
),
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
|
||||
@@ -18,10 +18,9 @@ package io.element.android.features.messages.timeline.groups
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.fixtures.aMessageEvent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
|
||||
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||
@@ -42,7 +41,7 @@ class TimelineItemGrouperTest {
|
||||
senderAvatar = anAvatarData(),
|
||||
senderDisplayName = "",
|
||||
content = TimelineItemStateEventContent(body = "a state event"),
|
||||
reactionsState = TimelineItemReactions(emptyList<AggregatedReaction>().toImmutableList()),
|
||||
reactionsState = aTimelineItemReactions(count = 0),
|
||||
sendState = EventSendState.Sent(AN_EVENT_ID),
|
||||
inReplyTo = null,
|
||||
debugInfo = aTimelineItemDebugInfo(),
|
||||
|
||||
@@ -229,7 +229,7 @@ internal fun RoomHeaderSection(
|
||||
Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(modifier = Modifier.size(70.dp)) {
|
||||
Avatar(
|
||||
avatarData = AvatarData(roomId, roomName, avatarUrl, AvatarSize.HUGE),
|
||||
avatarData = AvatarData(roomId, roomName, avatarUrl, AvatarSize.RoomHeader),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ private fun EditableAvatarView(
|
||||
when (state.roomAvatarUrl?.scheme) {
|
||||
null, "mxc" -> {
|
||||
Avatar(
|
||||
avatarData = AvatarData(state.roomId, state.roomName, state.roomAvatarUrl?.toString(), size = AvatarSize.HUGE),
|
||||
avatarData = AvatarData(state.roomId, state.roomName, state.roomAvatarUrl?.toString(), size = AvatarSize.RoomHeader),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ private fun RoomInviteMembersSearchBar(
|
||||
if (invitableUser.isUnresolved && !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined) {
|
||||
CheckableUnresolvedUserRow(
|
||||
checked = invitableUser.isSelected,
|
||||
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM),
|
||||
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
id = invitableUser.matrixUser.userId.value,
|
||||
onCheckedChange = { onUserToggled(invitableUser.matrixUser) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@@ -195,7 +195,7 @@ private fun RoomInviteMembersSearchBar(
|
||||
CheckableUserRow(
|
||||
checked = invitableUser.isSelected,
|
||||
enabled = !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined,
|
||||
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM),
|
||||
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
|
||||
name = invitableUser.matrixUser.getBestName(),
|
||||
subtext = when {
|
||||
// If they're already invited or joined we show that information
|
||||
|
||||
@@ -191,7 +191,7 @@ private fun RoomMemberListItem(
|
||||
displayName = roomMember.displayName,
|
||||
avatarUrl = roomMember.avatarUrl
|
||||
),
|
||||
avatarSize = AvatarSize.Custom(36.dp),
|
||||
avatarSize = AvatarSize.UserListItem,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ internal fun RoomMemberHeaderSection(
|
||||
Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(modifier = Modifier.size(70.dp)) {
|
||||
Avatar(
|
||||
avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.HUGE),
|
||||
avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.UserHeader),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
||||
fun onSessionVerificationClicked()
|
||||
fun onInvitesClicked()
|
||||
fun onRoomSettingsClicked(roomId: RoomId)
|
||||
fun onReportBugClicked()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ dependencies {
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.dateformatter.api)
|
||||
implementation(projects.libraries.eventformatter.api)
|
||||
implementation(projects.libraries.deeplink)
|
||||
implementation(projects.features.invitelist.api)
|
||||
implementation(projects.features.networkmonitor.api)
|
||||
implementation(projects.features.leaveroom.api)
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
@@ -26,6 +28,8 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@@ -33,7 +37,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||
class RoomListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: RoomListPresenter,
|
||||
private val presenter: RoomListPresenter,
|
||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
private fun onRoomClicked(roomId: RoomId) {
|
||||
@@ -60,9 +65,21 @@ class RoomListNode @AssistedInject constructor(
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomSettingsClicked(roomId) }
|
||||
}
|
||||
|
||||
private fun onMenuActionClicked(activity: Activity, roomListMenuAction: RoomListMenuAction) {
|
||||
when (roomListMenuAction) {
|
||||
RoomListMenuAction.InviteFriends -> {
|
||||
inviteFriendsUseCase.execute(activity)
|
||||
}
|
||||
RoomListMenuAction.ReportBug -> {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onReportBugClicked() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val activity = LocalContext.current as Activity
|
||||
RoomListView(
|
||||
state = state,
|
||||
onRoomClicked = this::onRoomClicked,
|
||||
@@ -71,6 +88,7 @@ class RoomListNode @AssistedInject constructor(
|
||||
onVerifyClicked = this::onSessionVerificationClicked,
|
||||
onInvitesClicked = this::onInvitesClicked,
|
||||
onRoomSettingsClicked = this::onRoomSettingsClicked,
|
||||
onMenuActionClicked = { onMenuActionClicked(activity, it) },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import io.element.android.libraries.core.coroutine.parallelMap
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
@@ -190,7 +191,8 @@ class RoomListPresenter @Inject constructor(
|
||||
val avatarData = AvatarData(
|
||||
id = roomSummary.identifier(),
|
||||
name = roomSummary.details.name,
|
||||
url = roomSummary.details.avatarURLString
|
||||
url = roomSummary.details.avatarURLString,
|
||||
size = AvatarSize.RoomListItem,
|
||||
)
|
||||
val roomIdentifier = roomSummary.identifier()
|
||||
RoomListRoomSummary(
|
||||
|
||||
@@ -21,6 +21,7 @@ import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryPlaceholders
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
@@ -68,7 +69,7 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||
hasUnread = true,
|
||||
timestamp = "14:18",
|
||||
lastMessage = "A very very very very long message which suites on two lines",
|
||||
avatarData = AvatarData("!id", "R"),
|
||||
avatarData = AvatarData("!id", "R", size = AvatarSize.RoomListItem),
|
||||
id = "!roomId:domain",
|
||||
roomId = RoomId("!roomId:domain")
|
||||
),
|
||||
@@ -77,10 +78,11 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||
hasUnread = false,
|
||||
timestamp = "14:16",
|
||||
lastMessage = "A short message",
|
||||
avatarData = AvatarData("!id", "Z"),
|
||||
avatarData = AvatarData("!id", "Z", size = AvatarSize.RoomListItem),
|
||||
id = "!roomId2:domain",
|
||||
roomId = RoomId("!roomId2:domain")
|
||||
),
|
||||
RoomListRoomSummaryPlaceholders.create("!roomId2:domain")
|
||||
RoomListRoomSummaryPlaceholders.create("!roomId2:domain"),
|
||||
RoomListRoomSummaryPlaceholders.create("!roomId3:domain"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,23 +21,17 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Snackbar
|
||||
@@ -50,12 +44,10 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
@@ -63,23 +55,20 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomView
|
||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||
import io.element.android.features.roomlist.impl.components.RequestVerificationHeader
|
||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||
import io.element.android.features.roomlist.impl.components.RoomListTopBar
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchResultContent
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchResultView
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
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.noFontPadding
|
||||
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -95,6 +84,7 @@ fun RoomListView(
|
||||
onCreateRoomClicked: () -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
@@ -118,12 +108,13 @@ fun RoomListView(
|
||||
|
||||
RoomListContent(
|
||||
state = state,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
onOpenSettings = onSettingsClicked,
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
)
|
||||
// This overlaid view will only be visible when state.displaySearchResults is true
|
||||
RoomListSearchResultView(
|
||||
@@ -142,13 +133,14 @@ fun RoomListView(
|
||||
@Composable
|
||||
fun RoomListContent(
|
||||
state: RoomListState,
|
||||
onVerifyClicked: () -> Unit,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
onCreateRoomClicked: () -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onVerifyClicked: () -> Unit = {},
|
||||
onRoomClicked: (RoomId) -> Unit = {},
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit = {},
|
||||
onOpenSettings: () -> Unit = {},
|
||||
onCreateRoomClicked: () -> Unit = {},
|
||||
onInvitesClicked: () -> Unit = {},
|
||||
) {
|
||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||
onRoomClicked(room.roomId)
|
||||
@@ -190,77 +182,45 @@ fun RoomListContent(
|
||||
areSearchResultsDisplayed = state.displaySearchResults,
|
||||
onFilterChanged = { state.eventSink(RoomListEvents.UpdateFilter(it)) },
|
||||
onToggleSearch = { state.eventSink(RoomListEvents.ToggleSearchResults) },
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
onOpenSettings = onOpenSettings,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
Column(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = lazyListState,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = lazyListState,
|
||||
) {
|
||||
if (state.displayVerificationPrompt) {
|
||||
item {
|
||||
RequestVerificationHeader(
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.invitesState != InvitesState.NoInvites) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
||||
.padding(horizontal = 16.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.heightIn(min = 48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.action_invites_list),
|
||||
fontSize = 14.sp,
|
||||
style = noFontPadding,
|
||||
)
|
||||
|
||||
if (state.invitesState == InvitesState.NewInvites) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.roomListUnreadIndicator())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = state.roomList,
|
||||
contentType = { _, room -> room.contentType() },
|
||||
) { index, room ->
|
||||
RoomSummaryRow(
|
||||
room = room,
|
||||
onClick = ::onRoomClicked,
|
||||
onLongClick = onRoomLongClicked,
|
||||
if (state.displayVerificationPrompt) {
|
||||
item {
|
||||
RequestVerificationHeader(
|
||||
onVerifyClicked = onVerifyClicked,
|
||||
onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) }
|
||||
)
|
||||
if (index != state.roomList.lastIndex) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.invitesState != InvitesState.NoInvites) {
|
||||
item {
|
||||
InvitesEntryPointView(onInvitesClicked, state)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = state.roomList,
|
||||
contentType = { _, room -> room.contentType() },
|
||||
) { index, room ->
|
||||
RoomSummaryRow(
|
||||
room = room,
|
||||
onClick = ::onRoomClicked,
|
||||
onLongClick = onRoomLongClicked,
|
||||
)
|
||||
if (index != state.roomList.lastIndex) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,7 +231,12 @@ fun RoomListContent(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
onClick = onCreateRoomClicked
|
||||
) {
|
||||
Icon(resourceId = DrawableR.drawable.ic_edit_square, contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message))
|
||||
Icon(
|
||||
// Correct icon alignment for better rendering.
|
||||
modifier = Modifier.padding(start = 1.dp, bottom = 1.dp),
|
||||
resourceId = DrawableR.drawable.ic_edit_square,
|
||||
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message)
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
@@ -287,67 +252,37 @@ fun RoomListContent(
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun RequestVerificationHeader(
|
||||
onVerifyClicked: () -> Unit,
|
||||
onDismissClicked: () -> Unit,
|
||||
private fun InvitesEntryPointView(
|
||||
onInvitesClicked: () -> Unit,
|
||||
state: RoomListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
Surface(
|
||||
modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = MaterialTheme.colorScheme.surfaceVariant
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
||||
.padding(horizontal = 16.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.heightIn(min = 48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
stringResource(R.string.session_verification_banner_title),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTextStyles.Bold.body,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.clickable(onClick = onDismissClicked),
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(CommonStrings.action_close)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(stringResource(R.string.session_verification_banner_message), style = ElementTextStyles.Regular.bodyMD)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 7.dp),
|
||||
onClick = onVerifyClicked,
|
||||
) {
|
||||
Text(stringResource(CommonStrings.action_continue), style = ElementTextStyles.Button)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(CommonStrings.action_invites_list),
|
||||
fontSize = 14.sp,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
if (state.invitesState == InvitesState.NewInvites) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
UnreadIndicatorAtom()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun PreviewRequestVerificationHeaderLight() {
|
||||
ElementPreviewLight {
|
||||
RequestVerificationHeader(onVerifyClicked = {}, onDismissClicked = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun PreviewRequestVerificationHeaderDark() {
|
||||
ElementPreviewDark {
|
||||
RequestVerificationHeader(onVerifyClicked = {}, onDismissClicked = {})
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RoomListRoomSummary.contentType() = isPlaceholder
|
||||
|
||||
@Preview
|
||||
@@ -369,18 +304,7 @@ private fun ContentToPreview(state: RoomListState) {
|
||||
onVerifyClicked = {},
|
||||
onCreateRoomClicked = {},
|
||||
onInvitesClicked = {},
|
||||
onRoomSettingsClicked = {}
|
||||
onRoomSettingsClicked = {},
|
||||
onMenuActionClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContentPreview() {
|
||||
ElementPreviewLight {
|
||||
RoomListSearchResultContent(
|
||||
state = aRoomListState(),
|
||||
onRoomClicked = {},
|
||||
onRoomLongClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.roomlist.impl.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
internal fun RequestVerificationHeader(
|
||||
onVerifyClicked: () -> Unit,
|
||||
onDismissClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
Surface(
|
||||
modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = MaterialTheme.colorScheme.surfaceVariant
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
stringResource(R.string.session_verification_banner_title),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTextStyles.Bold.body,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.clickable(onClick = onDismissClicked),
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(CommonStrings.action_close)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
stringResource(R.string.session_verification_banner_message),
|
||||
style = ElementTextStyles.Regular.bodyMD
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 20.dp, vertical = 7.dp),
|
||||
onClick = onVerifyClicked,
|
||||
) {
|
||||
Text(
|
||||
stringResource(CommonStrings.action_continue),
|
||||
style = ElementTextStyles.Button
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun PreviewRequestVerificationHeaderLight() {
|
||||
ElementPreviewLight {
|
||||
RequestVerificationHeader(onVerifyClicked = {}, onDismissClicked = {})
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun PreviewRequestVerificationHeaderDark() {
|
||||
ElementPreviewDark {
|
||||
RequestVerificationHeader(onVerifyClicked = {}, onDismissClicked = {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.roomlist.impl.components
|
||||
|
||||
enum class RoomListMenuAction {
|
||||
InviteFriends,
|
||||
ReportBug
|
||||
}
|
||||
@@ -19,23 +19,33 @@ package io.element.android.features.roomlist.impl.components
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
@@ -57,6 +67,7 @@ fun RoomListTopBar(
|
||||
areSearchResultsDisplayed: Boolean,
|
||||
onFilterChanged: (String) -> Unit,
|
||||
onToggleSearch: () -> Unit,
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -79,6 +90,7 @@ fun RoomListTopBar(
|
||||
matrixUser = matrixUser,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onSearchClicked = onToggleSearch,
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
scrollBehavior = scrollBehavior,
|
||||
modifier = modifier,
|
||||
)
|
||||
@@ -89,16 +101,20 @@ fun RoomListTopBar(
|
||||
private fun DefaultRoomListTopBar(
|
||||
matrixUser: MatrixUser?,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
onOpenSettings: () -> Unit,
|
||||
onSearchClicked: () -> Unit,
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onOpenSettings: () -> Unit = {},
|
||||
onSearchClicked: () -> Unit = {},
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
MediumTopAppBar(
|
||||
modifier = modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
title = {
|
||||
val fontSize = if (scrollBehavior.state.collapsedFraction > 0.5) 20.sp else 22.sp
|
||||
Text(
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.headlineMedium.copy(fontSize = fontSize),
|
||||
text = stringResource(id = R.string.screen_roomlist_main_space_title)
|
||||
)
|
||||
},
|
||||
@@ -108,7 +124,11 @@ private fun DefaultRoomListTopBar(
|
||||
modifier = Modifier.testTag(TestTags.homeScreenSettings),
|
||||
onClick = onOpenSettings
|
||||
) {
|
||||
val avatarData by remember { derivedStateOf { matrixUser.getAvatarData() } }
|
||||
val avatarData by remember {
|
||||
derivedStateOf {
|
||||
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
|
||||
}
|
||||
}
|
||||
Avatar(avatarData, contentDescription = stringResource(CommonStrings.common_settings))
|
||||
}
|
||||
}
|
||||
@@ -119,6 +139,32 @@ private fun DefaultRoomListTopBar(
|
||||
) {
|
||||
Icon(Icons.Default.Search, contentDescription = stringResource(CommonStrings.action_search))
|
||||
}
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu }
|
||||
) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.InviteFriends)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.action_invite)) },
|
||||
leadingIcon = { Icon(Icons.Default.Share, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMenuActionClicked(RoomListMenuAction.ReportBug)
|
||||
},
|
||||
text = { Text(stringResource(id = CommonStrings.common_report_a_bug)) },
|
||||
leadingIcon = { Icon(Icons.Default.BugReport, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
@@ -139,5 +185,8 @@ private fun DefaultRoomListTopBarPreview() {
|
||||
DefaultRoomListTopBar(
|
||||
matrixUser = MatrixUser(UserId("@id:domain"), "Alice"),
|
||||
scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()),
|
||||
onOpenSettings = {},
|
||||
onSearchClicked = {},
|
||||
onMenuActionClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.roomlist.impl.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
/**
|
||||
* https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=6547%3A147623
|
||||
*/
|
||||
@Composable
|
||||
internal fun RoomSummaryPlaceholderRow(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(minHeight)
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(AvatarSize.RoomListItem.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.background(color = ElementTheme.compoundColors.textPlaceholder, shape = CircleShape)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 20.dp, top = 19.dp, end = 4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(22.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PlaceholderAtom(width = 40.dp, height = 7.dp)
|
||||
Spacer(modifier = Modifier.width(7.dp))
|
||||
PlaceholderAtom(width = 45.dp, height = 7.dp)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
PlaceholderAtom(width = 22.dp, height = 4.dp)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(25.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PlaceholderAtom(width = 70.dp, height = 6.dp)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
PlaceholderAtom(width = 70.dp, height = 6.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomSummaryPlaceholderRowLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomSummaryPlaceholderRowDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
RoomSummaryPlaceholderRow()
|
||||
}
|
||||
@@ -17,28 +17,22 @@
|
||||
package io.element.android.features.roomlist.impl.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -54,24 +48,21 @@ import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.roomListPlaceHolder
|
||||
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
|
||||
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
|
||||
import io.element.android.libraries.designsystem.theme.roomListRoomName
|
||||
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
||||
|
||||
private val minHeight = 72.dp
|
||||
internal val minHeight = 84.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun RoomSummaryRow(
|
||||
room: RoomListRoomSummary,
|
||||
@@ -79,108 +70,111 @@ internal fun RoomSummaryRow(
|
||||
onLongClick: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val clickModifier = if (room.isPlaceholder) {
|
||||
modifier
|
||||
if (room.isPlaceholder) {
|
||||
RoomSummaryPlaceholderRow(
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
modifier.combinedClickable(
|
||||
onClick = { onClick(room) },
|
||||
onLongClick = { onLongClick(room) },
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
RoomSummaryRealRow(
|
||||
room = room,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
Box(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun RoomSummaryRealRow(
|
||||
room: RoomListRoomSummary,
|
||||
onClick: (RoomListRoomSummary) -> Unit,
|
||||
onLongClick: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val clickModifier = Modifier.combinedClickable(
|
||||
onClick = { onClick(room) },
|
||||
onLongClick = { onLongClick(room) },
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeight)
|
||||
.then(clickModifier)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 11.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
) {
|
||||
DefaultRoomSummaryRow(room = room)
|
||||
Avatar(
|
||||
room
|
||||
.avatarData,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp)
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
NameAndTimestampRow(room = room)
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
LastMessageAndIndicatorRow(room = room)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DefaultRoomSummaryRow(
|
||||
room: RoomListRoomSummary,
|
||||
) {
|
||||
Row(
|
||||
private fun RowScope.NameAndTimestampRow(room: RoomListRoomSummary) {
|
||||
// Name
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = CenterVertically
|
||||
) {
|
||||
Avatar(
|
||||
room.avatarData,
|
||||
modifier = Modifier.placeholder(
|
||||
visible = room.isPlaceholder,
|
||||
shape = CircleShape,
|
||||
color = ElementTheme.colors.roomListPlaceHolder(),
|
||||
)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 4.dp, top = 12.dp, bottom = 12.dp)
|
||||
.alignByBaseline()
|
||||
.weight(1f)
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
modifier = Modifier.placeholder(
|
||||
visible = room.isPlaceholder,
|
||||
shape = TextPlaceholderShape,
|
||||
color = ElementTheme.colors.roomListPlaceHolder(),
|
||||
),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
text = room.name,
|
||||
color = MaterialTheme.roomListRoomName(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Last Message
|
||||
val attributedLastMessage = (room.lastMessage as? AnnotatedString)
|
||||
?: AnnotatedString(room.lastMessage.orEmpty().toString())
|
||||
Text(
|
||||
modifier = Modifier.placeholder(
|
||||
visible = room.isPlaceholder,
|
||||
shape = TextPlaceholderShape,
|
||||
color = ElementTheme.colors.roomListPlaceHolder(),
|
||||
),
|
||||
text = attributedLastMessage,
|
||||
color = MaterialTheme.roomListRoomMessage(),
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
// Timestamp and Unread
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.alignByBaseline(),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.placeholder(
|
||||
visible = room.isPlaceholder,
|
||||
shape = TextPlaceholderShape,
|
||||
color = ElementTheme.colors.roomListPlaceHolder(),
|
||||
),
|
||||
fontSize = 12.sp,
|
||||
text = room.timestamp ?: "",
|
||||
color = MaterialTheme.roomListRoomMessageDate(),
|
||||
)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
val unreadIndicatorColor =
|
||||
if (room.hasUnread) MaterialTheme.roomListUnreadIndicator() else Color.Transparent
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(unreadIndicatorColor)
|
||||
.align(Alignment.End),
|
||||
)
|
||||
}
|
||||
}
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = room.name,
|
||||
color = MaterialTheme.roomListRoomName(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Timestamp
|
||||
Text(
|
||||
fontSize = 12.sp,
|
||||
text = room.timestamp ?: "",
|
||||
color = MaterialTheme.roomListRoomMessageDate(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.LastMessageAndIndicatorRow(room: RoomListRoomSummary) {
|
||||
// Last Message
|
||||
val attributedLastMessage = (room.lastMessage as? AnnotatedString)
|
||||
?: AnnotatedString(room.lastMessage.orEmpty().toString())
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 28.dp),
|
||||
text = attributedLastMessage,
|
||||
color = MaterialTheme.roomListRoomMessage(),
|
||||
fontSize = 14.sp,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Unread
|
||||
val unreadIndicatorColor =
|
||||
if (room.hasUnread) MaterialTheme.roomListUnreadIndicator() else Color.Transparent
|
||||
UnreadIndicatorAtom(
|
||||
modifier = Modifier.padding(top = 3.dp),
|
||||
color = unreadIndicatorColor,
|
||||
)
|
||||
}
|
||||
|
||||
val TextPlaceholderShape = PercentRectangleSizeShape(0.5f)
|
||||
|
||||
@@ -17,18 +17,18 @@
|
||||
package io.element.android.features.roomlist.impl.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@Immutable
|
||||
data class RoomListRoomSummary(
|
||||
data class RoomListRoomSummary constructor(
|
||||
val id: String,
|
||||
val roomId: RoomId,
|
||||
val name: String = "",
|
||||
val hasUnread: Boolean = false,
|
||||
val timestamp: String? = null,
|
||||
val lastMessage: CharSequence? = null,
|
||||
val avatarData: AvatarData = AvatarData(id, name),
|
||||
val avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
|
||||
val isPlaceholder: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.features.roomlist.impl.model
|
||||
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
object RoomListRoomSummaryPlaceholders {
|
||||
@@ -29,7 +30,7 @@ object RoomListRoomSummaryPlaceholders {
|
||||
name = "Short name",
|
||||
timestamp = "hh:mm",
|
||||
lastMessage = "Last message for placeholder",
|
||||
avatarData = AvatarData(id, "S")
|
||||
avatarData = AvatarData(id, "S", size = AvatarSize.RoomListItem)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.element.android.features.roomlist.impl.model
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
|
||||
@@ -28,7 +29,15 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||
aRoomListRoomSummary().copy(hasUnread = true),
|
||||
aRoomListRoomSummary().copy(timestamp = "88:88"),
|
||||
aRoomListRoomSummary().copy(timestamp = "88:88", hasUnread = true),
|
||||
aRoomListRoomSummary().copy(isPlaceholder = true),
|
||||
aRoomListRoomSummary().copy(isPlaceholder = true, timestamp = "88:88"),
|
||||
aRoomListRoomSummary().copy(
|
||||
name = "A very long room name that should be truncated",
|
||||
lastMessage = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" +
|
||||
" ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" +
|
||||
"modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
||||
timestamp = "yesterday",
|
||||
hasUnread = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,6 +48,6 @@ fun aRoomListRoomSummary() = RoomListRoomSummary(
|
||||
hasUnread = false,
|
||||
timestamp = null,
|
||||
lastMessage = "Last message",
|
||||
avatarData = AvatarData("!roomId", "Room name"),
|
||||
avatarData = AvatarData("!roomId", "Room name", size = AvatarSize.RoomListItem),
|
||||
isPlaceholder = false,
|
||||
)
|
||||
|
||||
@@ -48,15 +48,19 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.roomlist.impl.RoomListEvents
|
||||
import io.element.android.features.roomlist.impl.RoomListState
|
||||
import io.element.android.features.roomlist.impl.aRoomListState
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.impl.contentType
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.modifiers.applyIf
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
@@ -109,6 +113,7 @@ internal fun RoomListSearchResultContent(
|
||||
fun onBackButtonPressed() {
|
||||
state.eventSink(RoomListEvents.ToggleSearchResults)
|
||||
}
|
||||
|
||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||
onRoomClicked(room.roomId)
|
||||
}
|
||||
@@ -212,3 +217,23 @@ internal fun RoomListSearchResultContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContentLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun RoomListSearchResultContentDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun ContentToPreview() {
|
||||
RoomListSearchResultContent(
|
||||
state = aRoomListState(),
|
||||
onRoomClicked = {},
|
||||
onRoomLongClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
|
||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||
@@ -400,6 +401,6 @@ private val aRoomListRoomSummary = RoomListRoomSummary(
|
||||
hasUnread = true,
|
||||
timestamp = A_FORMATTED_DATE,
|
||||
lastMessage = "",
|
||||
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME),
|
||||
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
|
||||
isPlaceholder = false,
|
||||
)
|
||||
|
||||
@@ -30,8 +30,12 @@ dependencies {
|
||||
implementation(projects.libraries.di)
|
||||
implementation(libs.dagger)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.deeplink.usecase
|
||||
|
||||
import android.app.Activity
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import io.element.android.libraries.androidutils.R as AndroidUtilsR
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
class InviteFriendsUseCase @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val matrixClient: MatrixClient,
|
||||
private val buildMeta: BuildMeta,
|
||||
) {
|
||||
fun execute(activity: Activity) {
|
||||
val permalinkResult = PermalinkBuilder.permalinkForUser(matrixClient.sessionId)
|
||||
permalinkResult.fold(
|
||||
onSuccess = { permalink ->
|
||||
val appName = buildMeta.applicationName
|
||||
startSharePlainTextIntent(
|
||||
context = activity,
|
||||
activityResultLauncher = null,
|
||||
chooserTitle = stringProvider.getString(CommonStrings.action_invite_friends),
|
||||
text = stringProvider.getString(CommonStrings.invite_friends_text, appName, permalink),
|
||||
extraTitle = stringProvider.getString(CommonStrings.invite_friends_rich_title, appName),
|
||||
noActivityFoundMessage = stringProvider.getString(AndroidUtilsR.string.error_no_compatible_app_found)
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.atomic.atoms
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
@Composable
|
||||
fun PlaceholderAtom(
|
||||
width: Dp,
|
||||
height: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = ElementTheme.compoundColors.textPlaceholder,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(width)
|
||||
.height(height)
|
||||
.background(
|
||||
color = color,
|
||||
shape = RoundedCornerShape(size = height / 2)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun PlaceholderAtomLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun PlaceholderAtomDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
// Use a Red background to see the shape
|
||||
Box(modifier = Modifier.background(color = Color.Red)) {
|
||||
PlaceholderAtom(
|
||||
width = 80.dp,
|
||||
height = 12.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.atomic.atoms
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
|
||||
|
||||
@Composable
|
||||
fun UnreadIndicatorAtom(
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = 12.dp,
|
||||
color: Color = MaterialTheme.roomListUnreadIndicator(),
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun UnreadIndicatorAtomLightPreview() =
|
||||
ElementPreviewLight { ContentToPreview() }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun UnreadIndicatorAtomDarkPreview() =
|
||||
ElementPreviewDark { ContentToPreview() }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
UnreadIndicatorAtom()
|
||||
}
|
||||
@@ -17,7 +17,9 @@
|
||||
package io.element.android.libraries.designsystem.components.avatar
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -30,11 +32,12 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.designsystem.preview.debugPlaceholderAvatar
|
||||
import io.element.android.libraries.designsystem.text.toSp
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.AvatarGradientEnd
|
||||
import io.element.android.libraries.theme.AvatarGradientStart
|
||||
@@ -100,7 +103,7 @@ private fun InitialsAvatar(
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = avatarData.initial,
|
||||
fontSize = (avatarData.size.dp / 2).value.sp,
|
||||
fontSize = avatarData.size.dp.toSp() / 2,
|
||||
color = Color.White,
|
||||
)
|
||||
}
|
||||
@@ -109,4 +112,12 @@ private fun InitialsAvatar(
|
||||
@Preview(group = PreviewGroup.Avatars)
|
||||
@Composable
|
||||
fun AvatarPreview(@PreviewParameter(AvatarDataProvider::class) avatarData: AvatarData) =
|
||||
ElementThemedPreview { Avatar(avatarData) }
|
||||
ElementThemedPreview {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Avatar(avatarData)
|
||||
Text(text = avatarData.size.name + " " + avatarData.size.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ data class AvatarData(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val url: String? = null,
|
||||
val size: AvatarSize = AvatarSize.MEDIUM
|
||||
val size: AvatarSize,
|
||||
) {
|
||||
|
||||
val initial by lazy {
|
||||
|
||||
@@ -20,15 +20,28 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
open class AvatarDataProvider : PreviewParameterProvider<AvatarData> {
|
||||
override val values: Sequence<AvatarData>
|
||||
get() = sequenceOf(
|
||||
anAvatarData(),
|
||||
anAvatarData().copy(name = null),
|
||||
anAvatarData().copy(url = "aUrl"),
|
||||
)
|
||||
get() {
|
||||
AvatarSize.values()
|
||||
.also { it.sortBy { item -> item.name } }
|
||||
.asSequence()
|
||||
return AvatarSize.values().asSequence().map {
|
||||
sequenceOf(
|
||||
anAvatarData(size = it),
|
||||
anAvatarData(size = it).copy(name = null),
|
||||
anAvatarData(size = it).copy(url = "aUrl"),
|
||||
)
|
||||
}
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
fun anAvatarData(id: String = "@id_of_alice:server.org", name: String = "Alice") = AvatarData(
|
||||
fun anAvatarData(
|
||||
// Let's the id not start with a 'a'.
|
||||
id: String = "@id_of_alice:server.org",
|
||||
name: String = "Alice",
|
||||
size: AvatarSize = AvatarSize.RoomListItem,
|
||||
) = AvatarData(
|
||||
id = id,
|
||||
name = name,
|
||||
size = size,
|
||||
)
|
||||
|
||||
@@ -19,13 +19,25 @@ package io.element.android.libraries.designsystem.components.avatar
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
sealed class AvatarSize(open val dp: Dp) {
|
||||
enum class AvatarSize(val dp: Dp) {
|
||||
CurrentUserTopBar(28.dp),
|
||||
|
||||
object SMALL : AvatarSize(32.dp)
|
||||
object MEDIUM : AvatarSize(40.dp)
|
||||
object BIG : AvatarSize(48.dp)
|
||||
object HUGE : AvatarSize(96.dp)
|
||||
RoomHeader(96.dp),
|
||||
RoomListItem(52.dp),
|
||||
|
||||
// FIXME maybe remove this field and switch back to an enum (or remove this class) when design system will be integrated
|
||||
data class Custom(override val dp: Dp) : AvatarSize(dp)
|
||||
ForwardRoomListItem(36.dp),
|
||||
|
||||
UserHeader(96.dp),
|
||||
UserListItem(36.dp),
|
||||
|
||||
SelectedUser(56.dp),
|
||||
SelectedRoom(56.dp),
|
||||
|
||||
TimelineRoom(32.dp),
|
||||
TimelineSender(32.dp),
|
||||
|
||||
MessageActionSender(32.dp),
|
||||
|
||||
RoomInviteItem(52.dp),
|
||||
InviteSender(16.dp),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.text
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
|
||||
/**
|
||||
* Convert Dp to Sp, regarding current density.
|
||||
* Can be used for instance to use Dp unit for text.
|
||||
*/
|
||||
@Composable
|
||||
fun Dp.toSp(): TextUnit = with(LocalDensity.current) { toSp() }
|
||||
|
||||
/**
|
||||
* Convert Px value to Dp, regarding current density.
|
||||
*/
|
||||
@Composable
|
||||
fun Int.toDp(): Dp = with(LocalDensity.current) { toDp() }
|
||||
|
||||
/**
|
||||
* Convert Dp value to pixels, regarding current density.
|
||||
*/
|
||||
@Composable
|
||||
fun Dp.toPx(): Float = with(LocalDensity.current) { toPx() }
|
||||
|
||||
/**
|
||||
* Convert Dp value to pixels, regarding current density.
|
||||
*/
|
||||
@Composable
|
||||
fun Dp.roundToPx(): Int = with(LocalDensity.current) { roundToPx() }
|
||||
@@ -22,10 +22,6 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.theme.ElementColors
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.theme.SystemGrey4Dark
|
||||
import io.element.android.libraries.theme.SystemGrey6Light
|
||||
import io.element.android.libraries.theme.previews.ColorListPreview
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
|
||||
@@ -44,9 +40,6 @@ fun MaterialTheme.roomListRoomMessageDate() = colorScheme.secondary
|
||||
@Composable
|
||||
fun MaterialTheme.roomListUnreadIndicator() = colorScheme.primary
|
||||
|
||||
@Composable
|
||||
fun ElementColors.roomListPlaceHolder() = if (isLight) SystemGrey6Light else SystemGrey4Dark
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun ColorAliasesLightPreview() = ElementPreviewLight { ContentToPreview() }
|
||||
@@ -65,7 +58,6 @@ private fun ContentToPreview() {
|
||||
"roomListRoomMessage" to MaterialTheme.roomListRoomMessage(),
|
||||
"roomListRoomMessageDate" to MaterialTheme.roomListRoomMessageDate(),
|
||||
"roomListUnreadIndicator" to MaterialTheme.roomListUnreadIndicator(),
|
||||
"roomListPlaceHolder" to ElementTheme.colors.roomListPlaceHolder(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.material3.ProgressIndicatorDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -49,11 +50,21 @@ fun CircularProgressIndicator(
|
||||
color: Color = ProgressIndicatorDefaults.circularColor,
|
||||
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
|
||||
) {
|
||||
androidx.compose.material3.CircularProgressIndicator(
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
if (LocalInspectionMode.current) {
|
||||
// Use a determinate progress indicator to improve the preview rendering
|
||||
androidx.compose.material3.CircularProgressIndicator(
|
||||
modifier = modifier,
|
||||
progress = 0.75F,
|
||||
color = color,
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
} else {
|
||||
androidx.compose.material3.CircularProgressIndicator(
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Progress)
|
||||
@@ -68,7 +79,7 @@ private fun ContentToPreview() {
|
||||
)
|
||||
// Fixed progress
|
||||
CircularProgressIndicator(
|
||||
progress = 0.75F
|
||||
progress = 0.90F
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -50,7 +51,7 @@ fun ModalBottomSheet(
|
||||
shape: Shape = BottomSheetDefaults.ExpandedShape,
|
||||
containerColor: Color = BottomSheetDefaults.ContainerColor,
|
||||
contentColor: Color = contentColorFor(containerColor),
|
||||
tonalElevation: Dp = BottomSheetDefaults.Elevation,
|
||||
tonalElevation: Dp = if (ElementTheme.colors.isLight) 0.dp else BottomSheetDefaults.Elevation,
|
||||
scrimColor: Color = BottomSheetDefaults.ScrimColor,
|
||||
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
||||
windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
package io.element.android.libraries.matrix.api.timeline.item.event
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
data class EventReaction(
|
||||
val key: String,
|
||||
val count: Long
|
||||
val count: Long,
|
||||
val senderIds: List<UserId>
|
||||
)
|
||||
|
||||
@@ -78,7 +78,8 @@ private fun List<Reaction>?.map(): List<EventReaction> {
|
||||
return this?.map {
|
||||
EventReaction(
|
||||
key = it.key,
|
||||
count = it.count.toLong()
|
||||
count = it.count.toLong(),
|
||||
senderIds = it.senders.map { sender -> UserId(sender) }
|
||||
)
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ fun CheckableMatrixUserRow(
|
||||
checked: Boolean,
|
||||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
avatarSize: AvatarSize = AvatarSize.MEDIUM,
|
||||
avatarSize: AvatarSize = AvatarSize.UserListItem,
|
||||
onCheckedChange: (Boolean) -> Unit = {},
|
||||
enabled: Boolean = true,
|
||||
) = CheckableUserRow(
|
||||
|
||||
@@ -57,7 +57,7 @@ fun MatrixUserHeader(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Avatar(
|
||||
matrixUser.getAvatarData(size = AvatarSize.HUGE),
|
||||
matrixUser.getAvatarData(size = AvatarSize.UserHeader),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
// Name
|
||||
|
||||
@@ -16,13 +16,10 @@
|
||||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -49,7 +46,7 @@ import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
fun MatrixUserRow(
|
||||
matrixUser: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
avatarSize: AvatarSize = AvatarSize.Custom(36.dp),
|
||||
avatarSize: AvatarSize = AvatarSize.UserListItem,
|
||||
) = UserRow(
|
||||
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||
name = matrixUser.getBestName(),
|
||||
@@ -67,16 +64,14 @@ fun UserRow(
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
.heightIn(min = 56.dp)
|
||||
.padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(avatarData)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
.padding(start = 12.dp),
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
|
||||
@@ -47,7 +47,6 @@ import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
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.room.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
@@ -56,13 +55,14 @@ fun SelectedRoom(
|
||||
modifier: Modifier = Modifier,
|
||||
onRoomRemoved: (RoomSummaryDetails) -> Unit = {},
|
||||
) {
|
||||
Box(modifier = modifier
|
||||
.width(56.dp)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(56.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Avatar(AvatarData(roomSummary.roomId.value, roomSummary.name, roomSummary.avatarURLString, AvatarSize.Custom(56.dp)))
|
||||
Avatar(AvatarData(roomSummary.roomId.value, roomSummary.name, roomSummary.avatarURLString, AvatarSize.SelectedRoom))
|
||||
Text(
|
||||
text = roomSummary.name,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -102,8 +102,8 @@ internal fun SelectedRoomDarkPreview() = ElementPreviewDark { ContentToPreview()
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview() {
|
||||
SelectedRoom(roomSummary =
|
||||
RoomSummaryDetails(
|
||||
SelectedRoom(
|
||||
roomSummary = RoomSummaryDetails(
|
||||
roomId = RoomId("!room:domain"),
|
||||
name = "roomName",
|
||||
canonicalAlias = null,
|
||||
|
||||
@@ -55,13 +55,14 @@ fun SelectedUser(
|
||||
modifier: Modifier = Modifier,
|
||||
onUserRemoved: (MatrixUser) -> Unit = {},
|
||||
) {
|
||||
Box(modifier = modifier
|
||||
.width(56.dp)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(AvatarSize.SelectedUser.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Avatar(matrixUser.getAvatarData(size = AvatarSize.Custom(56.dp)))
|
||||
Avatar(matrixUser.getAvatarData(size = AvatarSize.SelectedUser))
|
||||
Text(
|
||||
text = matrixUser.getBestName(),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
@@ -36,11 +36,11 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.text.toPx
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -76,8 +76,8 @@ fun SelectedUsersList(
|
||||
// users, the last visible user will be precisely half visible. This gives an obvious affordance that there are more entries and the list can be scrolled.
|
||||
// For efficiency, we assume that all the children are the same width. If they needed to be different sizes we'd have to do this calculation each time
|
||||
// they needed to be measured.
|
||||
val minimumSpacing = with(LocalDensity.current) { 24.dp.toPx() }
|
||||
val userWidth = with(LocalDensity.current) { 56.dp.toPx() }
|
||||
val minimumSpacing = 24.dp.toPx()
|
||||
val userWidth = 56.dp.toPx()
|
||||
val userSpacing by remember {
|
||||
derivedStateOf {
|
||||
if (rowWidth == 0) {
|
||||
|
||||
@@ -19,11 +19,9 @@ package io.element.android.libraries.matrix.ui.components
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -44,6 +42,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.theme.components.Checkbox
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
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.noFontPadding
|
||||
@@ -59,15 +58,14 @@ fun UnresolvedUserRow(
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
.heightIn(min = 56.dp)
|
||||
.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Avatar(avatarData)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.fillMaxHeight(),
|
||||
.padding(start = 12.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
// ID
|
||||
@@ -82,7 +80,11 @@ fun UnresolvedUserRow(
|
||||
)
|
||||
|
||||
// Warning
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(top = 3.dp)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 3.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Error,
|
||||
contentDescription = "",
|
||||
@@ -141,7 +143,7 @@ fun CheckableUnresolvedUserRow(
|
||||
internal fun UnresolvedUserRowPreview() =
|
||||
ElementThemedPreview {
|
||||
val matrixUser = aMatrixUser()
|
||||
UnresolvedUserRow(matrixUser.getAvatarData(), matrixUser.userId.value)
|
||||
UnresolvedUserRow(matrixUser.getAvatarData(size = AvatarSize.UserListItem), matrixUser.userId.value)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -150,9 +152,12 @@ internal fun CheckableUnresolvedUserRowPreview() =
|
||||
ElementThemedPreview {
|
||||
val matrixUser = aMatrixUser()
|
||||
Column {
|
||||
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.Custom(36.dp)), matrixUser.userId.value)
|
||||
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.Custom(36.dp)), matrixUser.userId.value)
|
||||
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.Custom(36.dp)), matrixUser.userId.value, enabled = false)
|
||||
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.Custom(36.dp)), matrixUser.userId.value, enabled = false)
|
||||
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value)
|
||||
Divider()
|
||||
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value)
|
||||
Divider()
|
||||
CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value, enabled = false)
|
||||
Divider()
|
||||
CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(AvatarSize.UserListItem), matrixUser.userId.value, enabled = false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
fun MatrixUser.getAvatarData(size: AvatarSize = AvatarSize.MEDIUM) = AvatarData(
|
||||
fun MatrixUser.getAvatarData(size: AvatarSize) = AvatarData(
|
||||
id = userId.value,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
|
||||
@@ -70,7 +70,6 @@ import io.element.android.libraries.designsystem.VectorIcons
|
||||
import io.element.android.libraries.designsystem.modifiers.applyIf
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.theme.LocalColors
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
@@ -79,6 +78,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||
import io.element.android.libraries.theme.LocalColors
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -92,13 +92,15 @@ fun TextComposer(
|
||||
onSendMessage: (String) -> Unit = {},
|
||||
onResetComposerMode: () -> Unit = {},
|
||||
onComposerTextChange: (CharSequence) -> Unit = {},
|
||||
onAddAttachment:() -> Unit = {},
|
||||
onAddAttachment: () -> Unit = {},
|
||||
) {
|
||||
val text = composerText.orEmpty()
|
||||
Row(modifier.padding(
|
||||
horizontal = 12.dp,
|
||||
vertical = 8.dp
|
||||
), verticalAlignment = Alignment.Bottom) {
|
||||
Row(
|
||||
modifier.padding(
|
||||
horizontal = 12.dp,
|
||||
vertical = 8.dp
|
||||
), verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
AttachmentButton(onClick = onAddAttachment, modifier = Modifier.padding(vertical = 6.dp))
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
var lineCount by remember { mutableStateOf(0) }
|
||||
@@ -206,10 +208,12 @@ private fun EditingModeView(
|
||||
onResetComposerMode: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)) {
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
resourceId = VectorIcons.Edit,
|
||||
contentDescription = stringResource(CommonStrings.common_editing),
|
||||
@@ -248,16 +252,11 @@ private fun ReplyToModeView(
|
||||
onResetComposerMode: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val paddings = if (attachmentThumbnailInfo != null) {
|
||||
PaddingValues(start = 4.dp, end = 12.dp, top = 4.dp, bottom = 4.dp)
|
||||
} else {
|
||||
PaddingValues(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 4.dp)
|
||||
}
|
||||
Row(
|
||||
modifier
|
||||
.clip(RoundedCornerShape(13.dp))
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(paddings)
|
||||
.padding(4.dp)
|
||||
) {
|
||||
if (attachmentThumbnailInfo != null) {
|
||||
AttachmentThumbnail(
|
||||
@@ -267,45 +266,45 @@ private fun ReplyToModeView(
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(9.dp))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.SpaceEvenly) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
senderName,
|
||||
style = ElementTextStyles.Regular.caption2.copy(fontWeight = FontWeight.Medium),
|
||||
textAlign = TextAlign.Start,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(CommonStrings.action_close),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clickable(
|
||||
enabled = true,
|
||||
onClick = onResetComposerMode,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
indication = rememberRipple(bounded = false)
|
||||
),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Text(
|
||||
text = senderName,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = ElementTextStyles.Regular.caption2.copy(fontWeight = FontWeight.Medium),
|
||||
textAlign = TextAlign.Start,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = text.orEmpty(),
|
||||
style = ElementTextStyles.Regular.caption1,
|
||||
textAlign = TextAlign.Start,
|
||||
color = LocalColors.current.placeholder,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
maxLines = if (attachmentThumbnailInfo != null) 1 else 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(CommonStrings.action_close),
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp, top = 4.dp, start = 16.dp, bottom = 16.dp)
|
||||
.size(16.dp)
|
||||
.clickable(
|
||||
enabled = true,
|
||||
onClick = onResetComposerMode,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
indication = rememberRipple(bounded = false)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ class RoomListScreen(
|
||||
onCreateRoomClicked = {},
|
||||
onInvitesClicked = {},
|
||||
onRoomSettingsClicked = {},
|
||||
onMenuActionClicked = {},
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user