Merge pull request #678 from vector-im/feature/bma/designRoomList
Iterate on design
This commit is contained in:
commit
9451c9e635
349 changed files with 2137 additions and 1049 deletions
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:01d54896176fd9da4f544d98c675a63135a014ef31eeecdc288f4d10958b6a21
|
||||
size 100991
|
||||
oid sha256:add92c2e183badc5c0abc15a3364f820e24c4f3b4fa1fd9f33af174a6da74345
|
||||
size 101388
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8793296957822aa445599229ff8d7c7c079abfe7af02673381d44aed61a177fe
|
||||
size 52697
|
||||
oid sha256:7a6baba1197b01eb91e2dc6720482b97d9d0cdd5aaffa0d19d3e6b0860dadcf8
|
||||
size 52502
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e16706d7108c057a068d884a429fa60cf5992b0cadf4444e08634c94bcf7a8f
|
||||
size 80797
|
||||
oid sha256:9db3469badeb7f71b5f9cb6df6b230158f8d383e48dae2b615e94b885b1f2145
|
||||
size 81076
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:373a1f1025a4c609c5a9ae5800993891d12bcb5ee7d997e889095963cba24f56
|
||||
size 95919
|
||||
oid sha256:71eb9758ec399c188098a37ba527e3e32cf46aacc0296a4c9366b2b8d168e2b8
|
||||
size 91495
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e0c78298f09afcd5ee96cfef6bea6a7f3eeb0432d4fcaa13258fe1b4c3a978e9
|
||||
size 80302
|
||||
oid sha256:f85f0a3b5bb3edd6d864a2a211437a278f6bb7a9b66367a9ed4d596f673dc197
|
||||
size 80258
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5c8d11bb4603264829024e23996342e8c86651a8ee06fc1415546d9784a09916
|
||||
size 94975
|
||||
oid sha256:10fd32722844c7638b5200794ef021e1e25954d161a5a0f8e79207c481c4ef33
|
||||
size 90229
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cbc5d13ec61d0b5748e477663997f85ee692c4ad37b6578a1e74b93345fb22bf
|
||||
size 20658
|
||||
oid sha256:10422ab5d89261c0f2c4ec1060d265c43ef4aa25ccda22f2a203cc835db0b4e3
|
||||
size 22233
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1756353f38da7887c053a286d17867b38784b4a04831b2a2ff7fbeb5a008c060
|
||||
size 27946
|
||||
oid sha256:aa46a03ec95b34565744c3de2e94b41bb471dcae8cbdd426b8ff9c7478054575
|
||||
size 28046
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:146bf1144c320343f8f7420ba0218f787282fc660f9853badf5078f0fd763874
|
||||
size 20941
|
||||
oid sha256:145536794bb3e499a8efcd406d419ee6e8e858e8a10468f53401eb4f22e49f57
|
||||
size 22449
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60c8046af4cc6f298c0ccb57caacb1e1b262577a0acf78cba711bb3592952903
|
||||
size 28799
|
||||
oid sha256:5ba974bbeecee2446135a95eca579bea9935379c27abf272763a8cadf1e906c8
|
||||
size 28850
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1f720600c73dde446c99b95e8b4c0c5fe4dbfa901b4cb0ed6c3306fbddbdecd5
|
||||
size 10238
|
||||
oid sha256:517708e2df231f0c849c45d8951273db716f8e57cc6c22a5276d012fc08653b3
|
||||
size 11784
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:59b5b4abb3d0a52870fbc48964a85dd7c36054a6e1ccca35f3c845cfc360cacd
|
||||
size 9251
|
||||
oid sha256:b5ee6026d78583d2c802ed200c70715afd94d390e0a00936da3986f8dece1b31
|
||||
size 10803
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4d5c94e5d8ec1dda8ac7e40ed26b9253e1c76a0f2b6d31f75de2462b2648426
|
||||
size 142100
|
||||
oid sha256:6ec7c0893d93a5b1c6b42e13826ac9978363d7acc0943e66db0312552b71d8db
|
||||
size 143797
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5334397dff4a0a4980884ac9433827d6e566147ac2d214e8dab83a90fab692b2
|
||||
size 277318
|
||||
oid sha256:18db29d75fc1b8a2f79e03ebd4b7080ae76bdd01256f5a577c7fcaa1bc52f0f2
|
||||
size 278367
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d4d5c94e5d8ec1dda8ac7e40ed26b9253e1c76a0f2b6d31f75de2462b2648426
|
||||
size 142100
|
||||
oid sha256:6ec7c0893d93a5b1c6b42e13826ac9978363d7acc0943e66db0312552b71d8db
|
||||
size 143797
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5334397dff4a0a4980884ac9433827d6e566147ac2d214e8dab83a90fab692b2
|
||||
size 277318
|
||||
oid sha256:18db29d75fc1b8a2f79e03ebd4b7080ae76bdd01256f5a577c7fcaa1bc52f0f2
|
||||
size 278367
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue