Merge pull request #4891 from element-hq/feature/fga/tombstoned-room-decoration
Change : add tombstoned room decoration
This commit is contained in:
@@ -152,11 +152,8 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
|
||||
val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
|
||||
|
||||
val roomName: AsyncData<String> by remember {
|
||||
derivedStateOf { roomInfo.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
||||
}
|
||||
val roomAvatar: AsyncData<AvatarData> by remember {
|
||||
derivedStateOf { AsyncData.Success(roomInfo.avatarData()) }
|
||||
val roomAvatar by remember {
|
||||
derivedStateOf { roomInfo.avatarData() }
|
||||
}
|
||||
val heroes by remember {
|
||||
derivedStateOf { roomInfo.heroes().toPersistentList() }
|
||||
@@ -245,7 +242,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
|
||||
return MessagesState(
|
||||
roomId = room.roomId,
|
||||
roomName = roomName,
|
||||
roomName = roomInfo.name,
|
||||
roomAvatar = roomAvatar,
|
||||
heroes = heroes,
|
||||
composerState = composerState,
|
||||
@@ -292,7 +289,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
return AvatarData(
|
||||
id = id.value,
|
||||
name = name,
|
||||
url = avatarUrl ?: room.info().avatarUrl,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.TimelineRoom
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
@Immutable
|
||||
data class MessagesState(
|
||||
val roomId: RoomId,
|
||||
val roomName: AsyncData<String>,
|
||||
val roomAvatar: AsyncData<AvatarData>,
|
||||
val roomName: String?,
|
||||
val roomAvatar: AvatarData,
|
||||
val heroes: ImmutableList<AvatarData>,
|
||||
val userEventPermissions: UserEventPermissions,
|
||||
val composerState: MessageComposerState,
|
||||
@@ -59,4 +59,6 @@ data class MessagesState(
|
||||
val roomMemberModerationState: RoomMemberModerationState,
|
||||
val successorRoom: SuccessorRoom?,
|
||||
val eventSink: (MessagesEvents) -> Unit
|
||||
)
|
||||
) {
|
||||
val isTombstoned = successorRoom != null
|
||||
}
|
||||
|
||||
@@ -58,10 +58,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||
aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
|
||||
aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)),
|
||||
aMessagesState(showReinvitePrompt = true),
|
||||
aMessagesState(
|
||||
roomName = AsyncData.Uninitialized,
|
||||
roomAvatar = AsyncData.Uninitialized,
|
||||
),
|
||||
aMessagesState(roomName = null),
|
||||
aMessagesState(composerState = aMessageComposerState(showTextFormatting = true)),
|
||||
aMessagesState(
|
||||
enableVoiceMessages = true,
|
||||
@@ -86,15 +83,15 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||
currentPinnedMessageIndex = 0,
|
||||
),
|
||||
),
|
||||
aMessagesState(roomName = AsyncData.Success("A DM with a very looong name"), dmUserVerificationState = IdentityState.Verified),
|
||||
aMessagesState(roomName = AsyncData.Success("A DM with a very looong name"), dmUserVerificationState = IdentityState.VerificationViolation),
|
||||
aMessagesState(roomName = "A DM with a very looong name", dmUserVerificationState = IdentityState.Verified),
|
||||
aMessagesState(roomName = "A DM with a very looong name", dmUserVerificationState = IdentityState.VerificationViolation),
|
||||
aMessagesState(successorRoom = SuccessorRoom(RoomId("!id:domain"), null)),
|
||||
)
|
||||
}
|
||||
|
||||
fun aMessagesState(
|
||||
roomName: AsyncData<String> = AsyncData.Success("Room name"),
|
||||
roomAvatar: AsyncData<AvatarData> = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
|
||||
roomName: String? = "Room name",
|
||||
roomAvatar: AvatarData = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom),
|
||||
userEventPermissions: UserEventPermissions = aUserEventPermissions(),
|
||||
composerState: MessageComposerState = aMessageComposerState(
|
||||
textEditorState = aTextEditorStateRich(initialText = "Hello", initialFocus = true),
|
||||
|
||||
@@ -83,10 +83,8 @@ import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorVi
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
@@ -194,8 +192,9 @@ fun MessagesView(
|
||||
Column {
|
||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||
MessagesViewTopBar(
|
||||
roomName = state.roomName.dataOrNull(),
|
||||
roomAvatar = state.roomAvatar.dataOrNull(),
|
||||
roomName = state.roomName,
|
||||
roomAvatar = state.roomAvatar,
|
||||
isTombstoned = state.isTombstoned,
|
||||
heroes = state.heroes,
|
||||
roomCallState = state.roomCallState,
|
||||
dmUserIdentityState = state.dmUserVerificationState,
|
||||
@@ -450,8 +449,8 @@ private fun MessagesViewComposerBottomSheetContents(
|
||||
}
|
||||
}),
|
||||
roomId = state.roomId,
|
||||
roomName = state.roomName.dataOrNull(),
|
||||
roomAvatarData = state.roomAvatar.dataOrNull(),
|
||||
roomName = state.roomName,
|
||||
roomAvatarData = state.roomAvatar,
|
||||
suggestions = state.composerState.suggestions,
|
||||
onSelectSuggestion = {
|
||||
state.composerState.eventSink(MessageComposerEvents.InsertSuggestion(it))
|
||||
@@ -491,7 +490,8 @@ private fun MessagesViewComposerBottomSheetContents(
|
||||
@Composable
|
||||
private fun MessagesViewTopBar(
|
||||
roomName: String?,
|
||||
roomAvatar: AvatarData?,
|
||||
roomAvatar: AvatarData,
|
||||
isTombstoned: Boolean,
|
||||
heroes: ImmutableList<AvatarData>,
|
||||
roomCallState: RoomCallState,
|
||||
dmUserIdentityState: IdentityState?,
|
||||
@@ -513,19 +513,13 @@ private fun MessagesViewTopBar(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val titleModifier = Modifier.weight(1f, fill = false)
|
||||
if (roomName != null && roomAvatar != null) {
|
||||
RoomAvatarAndNameRow(
|
||||
roomName = roomName,
|
||||
roomAvatar = roomAvatar,
|
||||
heroes = heroes,
|
||||
modifier = titleModifier
|
||||
)
|
||||
} else {
|
||||
IconTitlePlaceholdersRowMolecule(
|
||||
iconSize = AvatarSize.TimelineRoom.dp,
|
||||
modifier = titleModifier
|
||||
)
|
||||
}
|
||||
RoomAvatarAndNameRow(
|
||||
roomName = roomName,
|
||||
roomAvatar = roomAvatar,
|
||||
isTombstoned = isTombstoned,
|
||||
heroes = heroes,
|
||||
modifier = titleModifier
|
||||
)
|
||||
|
||||
when (dmUserIdentityState) {
|
||||
IdentityState.Verified -> {
|
||||
@@ -559,23 +553,26 @@ private fun MessagesViewTopBar(
|
||||
|
||||
@Composable
|
||||
private fun RoomAvatarAndNameRow(
|
||||
roomName: String,
|
||||
roomName: String?,
|
||||
roomAvatar: AvatarData,
|
||||
heroes: ImmutableList<AvatarData>,
|
||||
isTombstoned: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CompositeAvatar(
|
||||
RoomAvatar(
|
||||
avatarData = roomAvatar,
|
||||
heroes = heroes,
|
||||
isTombstoned = isTombstoned,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
text = roomName,
|
||||
text = roomName ?: stringResource(CommonStrings.common_no_room_name),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
fontStyle = FontStyle.Italic.takeIf { roomName == null },
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
@@ -586,9 +583,9 @@ private fun RoomAvatarAndNameRow(
|
||||
private fun CantSendMessageBanner() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(ElementTheme.colors.bgSubtleSecondary)
|
||||
.padding(16.dp),
|
||||
.fillMaxWidth()
|
||||
.background(ElementTheme.colors.bgSubtleSecondary)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.R
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
@@ -45,7 +46,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
fun SuggestionsPickerView(
|
||||
roomId: RoomId,
|
||||
roomName: String?,
|
||||
roomAvatarData: AvatarData?,
|
||||
roomAvatarData: AvatarData,
|
||||
suggestions: ImmutableList<ResolvedSuggestion>,
|
||||
onSelectSuggestion: (ResolvedSuggestion) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -155,7 +156,7 @@ internal fun SuggestionsPickerViewPreview() {
|
||||
SuggestionsPickerView(
|
||||
roomId = RoomId("!room:matrix.org"),
|
||||
roomName = "Room",
|
||||
roomAvatarData = null,
|
||||
roomAvatarData = anAvatarData(),
|
||||
suggestions = persistentListOf(
|
||||
ResolvedSuggestion.AtRoom,
|
||||
ResolvedSuggestion.Member(roomMember),
|
||||
|
||||
@@ -119,9 +119,9 @@ class MessagesPresenterTest {
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = consumeItemsUntilTimeout().last()
|
||||
assertThat(initialState.roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(initialState.roomName).isEqualTo(AsyncData.Success(""))
|
||||
assertThat(initialState.roomName).isEqualTo("")
|
||||
assertThat(initialState.roomAvatar)
|
||||
.isEqualTo(AsyncData.Success(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom)))
|
||||
.isEqualTo(AvatarData(id = A_ROOM_ID.value, name = "", url = AN_AVATAR_URL, size = AvatarSize.TimelineRoom))
|
||||
assertThat(initialState.userEventPermissions.canSendMessage).isTrue()
|
||||
assertThat(initialState.userEventPermissions.canRedactOwn).isTrue()
|
||||
assertThat(initialState.hasNetworkConnection).isTrue()
|
||||
|
||||
@@ -110,7 +110,7 @@ class MessagesViewTest {
|
||||
state = state,
|
||||
onRoomDetailsClick = callback,
|
||||
)
|
||||
rule.onNodeWithText(state.roomName.dataOrNull().orEmpty(), useUnmergedTree = true).performClick()
|
||||
rule.onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||
@@ -97,7 +97,7 @@ fun EditDefaultNotificationSettingView(
|
||||
Text(text = subtitle)
|
||||
},
|
||||
leadingContent = ListItemContent.Custom {
|
||||
CompositeAvatar(
|
||||
RoomAvatar(
|
||||
avatarData = summary.avatarData,
|
||||
heroes = summary.heroesAvatar,
|
||||
)
|
||||
|
||||
@@ -210,6 +210,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,
|
||||
hasMemberVerificationViolations = hasMemberVerificationViolations,
|
||||
canReportRoom = canReportRoom,
|
||||
isTombstoned = roomInfo.successorRoom != null,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ data class RoomDetailsState(
|
||||
val canShowSecurityAndPrivacy: Boolean,
|
||||
val hasMemberVerificationViolations: Boolean,
|
||||
val canReportRoom: Boolean,
|
||||
val isTombstoned: Boolean,
|
||||
val eventSink: (RoomDetailsEvent) -> Unit
|
||||
) {
|
||||
val roomBadges = buildList {
|
||||
|
||||
@@ -53,6 +53,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
||||
aRoomDetailsState(knockRequestsCount = null, canShowKnockRequests = true),
|
||||
aRoomDetailsState(knockRequestsCount = 4, canShowKnockRequests = true),
|
||||
aRoomDetailsState(hasMemberVerificationViolations = true),
|
||||
aRoomDetailsState(isTombstoned = true),
|
||||
aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFIED),
|
||||
aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFICATION_VIOLATION),
|
||||
// Add other state here
|
||||
@@ -118,6 +119,7 @@ fun aRoomDetailsState(
|
||||
canShowSecurityAndPrivacy: Boolean = true,
|
||||
hasMemberVerificationViolations: Boolean = false,
|
||||
canReportRoom: Boolean = true,
|
||||
isTombstoned: Boolean = false,
|
||||
eventSink: (RoomDetailsEvent) -> Unit = {},
|
||||
) = RoomDetailsState(
|
||||
roomId = roomId,
|
||||
@@ -148,6 +150,7 @@ fun aRoomDetailsState(
|
||||
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,
|
||||
hasMemberVerificationViolations = hasMemberVerificationViolations,
|
||||
canReportRoom = canReportRoom,
|
||||
isTombstoned = isTombstoned,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRow
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.DmAvatars
|
||||
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.button.MainActionButton
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
@@ -138,6 +138,7 @@ fun RoomDetailsView(
|
||||
roomName = state.roomName,
|
||||
roomAlias = state.roomAlias,
|
||||
heroes = state.heroes,
|
||||
isTombstoned = state.isTombstoned,
|
||||
openAvatarPreview = { avatarUrl ->
|
||||
openAvatarPreview(state.roomName, avatarUrl)
|
||||
},
|
||||
@@ -380,6 +381,7 @@ private fun RoomHeaderSection(
|
||||
roomName: String,
|
||||
roomAlias: RoomAlias?,
|
||||
heroes: ImmutableList<MatrixUser>,
|
||||
isTombstoned: Boolean,
|
||||
openAvatarPreview: (url: String) -> Unit,
|
||||
onSubtitleClick: (String) -> Unit,
|
||||
) {
|
||||
@@ -389,11 +391,12 @@ private fun RoomHeaderSection(
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CompositeAvatar(
|
||||
RoomAvatar(
|
||||
avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader),
|
||||
heroes = heroes.map { user ->
|
||||
user.getAvatarData(size = AvatarSize.RoomHeader)
|
||||
}.toPersistentList(),
|
||||
isTombstoned = isTombstoned,
|
||||
modifier = Modifier
|
||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||
.testTag(TestTags.roomDetailAvatar)
|
||||
|
||||
@@ -44,7 +44,7 @@ import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvid
|
||||
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||
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.CompositeAvatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
@@ -121,7 +121,7 @@ internal fun RoomSummaryRow(
|
||||
timestamp = room.timestamp,
|
||||
isHighlighted = room.isHighlighted
|
||||
)
|
||||
LastMessageAndIndicatorRow(room = room)
|
||||
MessagePreviewAndIndicatorRow(room = room)
|
||||
}
|
||||
}
|
||||
RoomSummaryDisplayType.KNOCKED -> {
|
||||
@@ -184,10 +184,11 @@ private fun RoomSummaryScaffoldRow(
|
||||
.padding(horizontal = 16.dp, vertical = 11.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
) {
|
||||
CompositeAvatar(
|
||||
RoomAvatar(
|
||||
avatarData = room.avatarData,
|
||||
heroes = room.heroes,
|
||||
hideAvatarImages = hideAvatarImage,
|
||||
isTombstoned = room.isTombstoned,
|
||||
hideAvatarImage = hideAvatarImage,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
@@ -255,7 +256,7 @@ private fun InviteSubtitle(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LastMessageAndIndicatorRow(
|
||||
private fun MessagePreviewAndIndicatorRow(
|
||||
room: RoomListRoomSummary,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -263,12 +264,15 @@ private fun LastMessageAndIndicatorRow(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = spacedBy(28.dp)
|
||||
) {
|
||||
// Last Message
|
||||
val attributedLastMessage = room.lastMessage as? AnnotatedString
|
||||
?: AnnotatedString(room.lastMessage.orEmpty().toString())
|
||||
val messagePreview = if (room.isTombstoned) {
|
||||
stringResource(R.string.screen_roomlist_tombstoned_room_description)
|
||||
} else {
|
||||
room.lastMessage.orEmpty()
|
||||
}
|
||||
val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.toString())
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = attributedLastMessage,
|
||||
text = annotatedMessagePreview,
|
||||
color = ElementTheme.roomListRoomMessage(),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
minLines = 2,
|
||||
|
||||
@@ -67,6 +67,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||
heroes = roomInfo.heroes.map { user ->
|
||||
user.getAvatarData(size = AvatarSize.RoomListItem)
|
||||
}.toImmutableList(),
|
||||
isTombstoned = roomInfo.successorRoom != null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ data class RoomListRoomSummary(
|
||||
val isDm: Boolean,
|
||||
val isFavorite: Boolean,
|
||||
val inviteSender: InviteSender?,
|
||||
val isTombstoned: Boolean,
|
||||
val heroes: ImmutableList<AvatarData>,
|
||||
) {
|
||||
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
||||
|
||||
@@ -110,6 +110,11 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||
name = "A knocked room with alias",
|
||||
canonicalAlias = RoomAlias("#knockable:matrix.org"),
|
||||
displayType = RoomSummaryDisplayType.KNOCKED,
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
name = "A tombstoned room",
|
||||
displayType = RoomSummaryDisplayType.ROOM,
|
||||
isTombstoned = true,
|
||||
)
|
||||
),
|
||||
).flatten()
|
||||
@@ -145,6 +150,7 @@ internal fun aRoomListRoomSummary(
|
||||
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||
canonicalAlias: RoomAlias? = null,
|
||||
heroes: List<AvatarData> = emptyList(),
|
||||
isTombstoned: Boolean = false,
|
||||
) = RoomListRoomSummary(
|
||||
id = id,
|
||||
roomId = RoomId(id),
|
||||
@@ -165,4 +171,5 @@ internal fun aRoomListRoomSummary(
|
||||
displayType = displayType,
|
||||
canonicalAlias = canonicalAlias,
|
||||
heroes = heroes.toImmutableList(),
|
||||
isTombstoned = isTombstoned,
|
||||
)
|
||||
|
||||
@@ -84,6 +84,7 @@ internal fun createRoomListRoomSummary(
|
||||
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||
heroes: List<AvatarData> = emptyList(),
|
||||
timestamp: String? = null,
|
||||
isTombstoned: Boolean = false,
|
||||
) = RoomListRoomSummary(
|
||||
id = A_ROOM_ID.value,
|
||||
roomId = A_ROOM_ID,
|
||||
@@ -104,4 +105,5 @@ internal fun createRoomListRoomSummary(
|
||||
inviteSender = null,
|
||||
isDm = false,
|
||||
heroes = heroes.toPersistentList(),
|
||||
isTombstoned = isTombstoned,
|
||||
)
|
||||
|
||||
@@ -370,7 +370,7 @@ fun Modifier.avatarBloom(
|
||||
val initialsBitmap = initialsBitmap(
|
||||
width = BloomDefaults.ENCODE_SIZE_PX.toDp(),
|
||||
height = BloomDefaults.ENCODE_SIZE_PX.toDp(),
|
||||
text = avatarData.initial,
|
||||
text = avatarData.initialLetter,
|
||||
textColor = avatarColors.foreground,
|
||||
backgroundColor = avatarColors.background,
|
||||
)
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
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
|
||||
@@ -21,21 +19,16 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.SubcomposeAsyncImageContent
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.designsystem.text.toSp
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import timber.log.Timber
|
||||
@@ -50,21 +43,18 @@ fun Avatar(
|
||||
// If true, will show initials even if avatarData.url is not null
|
||||
hideImage: Boolean = false,
|
||||
) {
|
||||
val commonModifier = modifier
|
||||
.size(forcedAvatarSize ?: avatarData.size.dp)
|
||||
.clip(CircleShape)
|
||||
if (avatarData.url.isNullOrBlank() || hideImage) {
|
||||
InitialsAvatar(
|
||||
InitialLetterAvatar(
|
||||
avatarData = avatarData,
|
||||
forcedAvatarSize = forcedAvatarSize,
|
||||
modifier = commonModifier,
|
||||
modifier = modifier,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
} else {
|
||||
ImageAvatar(
|
||||
avatarData = avatarData,
|
||||
forcedAvatarSize = forcedAvatarSize,
|
||||
modifier = commonModifier,
|
||||
modifier = modifier,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
@@ -77,11 +67,14 @@ private fun ImageAvatar(
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
val size = forcedAvatarSize ?: avatarData.size.dp
|
||||
SubcomposeAsyncImage(
|
||||
model = avatarData,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
) {
|
||||
val collectedState by painter.state.collectAsState()
|
||||
when (val state = collectedState) {
|
||||
@@ -90,13 +83,13 @@ private fun ImageAvatar(
|
||||
SideEffect {
|
||||
Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
|
||||
}
|
||||
InitialsAvatar(
|
||||
InitialLetterAvatar(
|
||||
avatarData = avatarData,
|
||||
forcedAvatarSize = forcedAvatarSize,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
else -> InitialsAvatar(
|
||||
else -> InitialLetterAvatar(
|
||||
avatarData = avatarData,
|
||||
forcedAvatarSize = forcedAvatarSize,
|
||||
contentDescription = contentDescription,
|
||||
@@ -106,33 +99,20 @@ private fun ImageAvatar(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InitialsAvatar(
|
||||
private fun InitialLetterAvatar(
|
||||
avatarData: AvatarData,
|
||||
forcedAvatarSize: Dp?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val avatarColors = AvatarColorsProvider.provide(avatarData.id)
|
||||
Box(
|
||||
modifier.background(color = avatarColors.background)
|
||||
) {
|
||||
val fontSize = (forcedAvatarSize ?: avatarData.size.dp).toSp() / 2
|
||||
val originalFont = ElementTheme.typography.fontHeadingMdBold
|
||||
val ratio = fontSize.value / originalFont.fontSize.value
|
||||
val lineHeight = originalFont.lineHeight * ratio
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clearAndSetSemantics {
|
||||
contentDescription?.let {
|
||||
this.contentDescription = it
|
||||
}
|
||||
}
|
||||
.align(Alignment.Center),
|
||||
text = avatarData.initial,
|
||||
style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp),
|
||||
color = avatarColors.foreground,
|
||||
)
|
||||
}
|
||||
TextAvatar(
|
||||
text = avatarData.initialLetter,
|
||||
size = forcedAvatarSize ?: avatarData.size.dp,
|
||||
colors = avatarColors,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Avatars)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.avatar
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.util.Collections
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
private const val MAX_AVATAR_COUNT = 4
|
||||
|
||||
@Composable
|
||||
fun AvatarCluster(
|
||||
avatars: ImmutableList<AvatarData>,
|
||||
modifier: Modifier = Modifier,
|
||||
hideAvatarImages: Boolean = false,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
val limitedAvatars = avatars.take(MAX_AVATAR_COUNT)
|
||||
val numberOfAvatars = limitedAvatars.size
|
||||
if (numberOfAvatars == 4) {
|
||||
// Swap 2 and 3 so that the 4th avatar is at the bottom right
|
||||
Collections.swap(limitedAvatars, 2, 3)
|
||||
}
|
||||
when (numberOfAvatars) {
|
||||
0 -> {
|
||||
error("Unsupported number of avatars: 0")
|
||||
}
|
||||
1 -> {
|
||||
Avatar(
|
||||
avatarData = limitedAvatars[0],
|
||||
modifier = modifier,
|
||||
contentDescription = contentDescription,
|
||||
hideImage = hideAvatarImages
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val size = limitedAvatars.first().size
|
||||
val angle = 2 * Math.PI / numberOfAvatars
|
||||
val offsetRadius = when (numberOfAvatars) {
|
||||
2 -> size.dp.value / 4.2
|
||||
3 -> size.dp.value / 4.0
|
||||
4 -> size.dp.value / 3.1
|
||||
else -> error("Unsupported number of heroes: $numberOfAvatars")
|
||||
}
|
||||
val heroAvatarSize = when (numberOfAvatars) {
|
||||
2 -> size.dp / 2.2f
|
||||
3 -> size.dp / 2.4f
|
||||
4 -> size.dp / 2.2f
|
||||
else -> error("Unsupported number of heroes: $numberOfAvatars")
|
||||
}
|
||||
val angleOffset = when (numberOfAvatars) {
|
||||
2 -> PI
|
||||
3 -> 7 * PI / 6
|
||||
4 -> 13 * PI / 4
|
||||
else -> error("Unsupported number of heroes: $numberOfAvatars")
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size.dp)
|
||||
.semantics {
|
||||
this.contentDescription = contentDescription.orEmpty()
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
limitedAvatars.forEachIndexed { index, heroAvatar ->
|
||||
val xOffset = (offsetRadius * cos(angle * index.toDouble() + angleOffset)).dp
|
||||
val yOffset = (offsetRadius * sin(angle * index.toDouble() + angleOffset)).dp
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(heroAvatarSize)
|
||||
.offset(
|
||||
x = xOffset,
|
||||
y = yOffset,
|
||||
)
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = heroAvatar,
|
||||
forcedAvatarSize = heroAvatarSize,
|
||||
hideImage = hideAvatarImages,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Avatars)
|
||||
@Composable
|
||||
internal fun AvatarClusterPreview() = ElementThemedPreview {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
for (ngOfAvatars in 1..5) {
|
||||
AvatarCluster(
|
||||
avatars = List(ngOfAvatars) { anAvatarData(it) }.toPersistentList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun anAvatarData(i: Int) = anAvatarData(
|
||||
id = ('A' + i).toString(),
|
||||
name = ('A' + i).toString()
|
||||
)
|
||||
@@ -18,7 +18,7 @@ data class AvatarData(
|
||||
val url: String? = null,
|
||||
val size: AvatarSize,
|
||||
) {
|
||||
val initial by lazy {
|
||||
val initialLetter by lazy {
|
||||
// For roomIds, use "#" as initial
|
||||
(name?.takeIf { it.isNotBlank() } ?: id.takeIf { !it.startsWith("!") } ?: "#")
|
||||
.let { dn ->
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.avatar
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.util.Collections
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
@Composable
|
||||
fun CompositeAvatar(
|
||||
avatarData: AvatarData,
|
||||
heroes: ImmutableList<AvatarData>,
|
||||
modifier: Modifier = Modifier,
|
||||
hideAvatarImages: Boolean = false,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
if (avatarData.url != null || heroes.isEmpty()) {
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
modifier = modifier,
|
||||
contentDescription = contentDescription,
|
||||
hideImage = hideAvatarImages
|
||||
)
|
||||
} else {
|
||||
val limitedHeroes = heroes.take(4)
|
||||
val numberOfHeroes = limitedHeroes.size
|
||||
if (numberOfHeroes == 4) {
|
||||
// Swap 2 and 3 so that the 4th hero is at the bottom right
|
||||
Collections.swap(limitedHeroes, 2, 3)
|
||||
}
|
||||
when (numberOfHeroes) {
|
||||
0 -> {
|
||||
error("Unsupported number of heroes: 0")
|
||||
}
|
||||
1 -> {
|
||||
Avatar(
|
||||
avatarData = heroes[0],
|
||||
modifier = modifier,
|
||||
contentDescription = contentDescription,
|
||||
hideImage = hideAvatarImages
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val angle = 2 * Math.PI / numberOfHeroes
|
||||
val offsetRadius = when (numberOfHeroes) {
|
||||
2 -> avatarData.size.dp.value / 4.2
|
||||
3 -> avatarData.size.dp.value / 4.0
|
||||
4 -> avatarData.size.dp.value / 3.1
|
||||
else -> error("Unsupported number of heroes: $numberOfHeroes")
|
||||
}
|
||||
val heroAvatarSize = when (numberOfHeroes) {
|
||||
2 -> avatarData.size.dp / 2.2f
|
||||
3 -> avatarData.size.dp / 2.4f
|
||||
4 -> avatarData.size.dp / 2.2f
|
||||
else -> error("Unsupported number of heroes: $numberOfHeroes")
|
||||
}
|
||||
val angleOffset = when (numberOfHeroes) {
|
||||
2 -> PI
|
||||
3 -> 7 * PI / 6
|
||||
4 -> 13 * PI / 4
|
||||
else -> error("Unsupported number of heroes: $numberOfHeroes")
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(avatarData.size.dp)
|
||||
.semantics {
|
||||
this.contentDescription = contentDescription.orEmpty()
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
limitedHeroes.forEachIndexed { index, heroAvatar ->
|
||||
val xOffset = (offsetRadius * cos(angle * index.toDouble() + angleOffset)).dp
|
||||
val yOffset = (offsetRadius * sin(angle * index.toDouble() + angleOffset)).dp
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(heroAvatarSize)
|
||||
.offset(
|
||||
x = xOffset,
|
||||
y = yOffset,
|
||||
)
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = heroAvatar,
|
||||
forcedAvatarSize = heroAvatarSize,
|
||||
hideImage = hideAvatarImages,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Avatars)
|
||||
@Composable
|
||||
internal fun CompositeAvatarPreview() = ElementThemedPreview {
|
||||
val mainAvatar = anAvatarData(
|
||||
id = "Zac",
|
||||
name = "Zac",
|
||||
size = AvatarSize.RoomListItem,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
repeat(6) { nbOfHeroes ->
|
||||
CompositeAvatar(
|
||||
avatarData = mainAvatar,
|
||||
heroes = List(nbOfHeroes) { aHeroAvatarData(it) }.toPersistentList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun aHeroAvatarData(i: Int) = anAvatarData(
|
||||
id = ('A' + i).toString(),
|
||||
name = ('A' + i).toString()
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.avatar
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun RoomAvatar(
|
||||
avatarData: AvatarData,
|
||||
heroes: ImmutableList<AvatarData>,
|
||||
modifier: Modifier = Modifier,
|
||||
isTombstoned: Boolean = false,
|
||||
hideAvatarImage: Boolean = false,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
when {
|
||||
isTombstoned -> {
|
||||
TombstonedRoomAvatar(
|
||||
size = avatarData.size,
|
||||
modifier = modifier,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
avatarData.url != null || heroes.isEmpty() -> {
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
modifier = modifier,
|
||||
contentDescription = contentDescription,
|
||||
hideImage = hideAvatarImage
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
AvatarCluster(
|
||||
avatars = heroes,
|
||||
modifier = modifier,
|
||||
hideAvatarImages = hideAvatarImage,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.avatar
|
||||
|
||||
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.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.compound.theme.AvatarColors
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
import io.element.android.libraries.designsystem.text.toSp
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
internal fun TextAvatar(
|
||||
text: String,
|
||||
size: Dp,
|
||||
colors: AvatarColors,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.background(color = colors.background)
|
||||
) {
|
||||
val fontSize = size.toSp() / 2
|
||||
val originalFont = ElementTheme.typography.fontHeadingMdBold
|
||||
val ratio = fontSize.value / originalFont.fontSize.value
|
||||
val lineHeight = originalFont.lineHeight * ratio
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clearAndSetSemantics {
|
||||
contentDescription?.let {
|
||||
this.contentDescription = it
|
||||
}
|
||||
}
|
||||
.align(Alignment.Center),
|
||||
text = text,
|
||||
style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp),
|
||||
color = colors.foreground,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Avatars)
|
||||
@Composable
|
||||
internal fun TextAvatarPreview() = ElementPreview {
|
||||
TextAvatar(
|
||||
text = "AB",
|
||||
size = 40.dp,
|
||||
colors = AvatarColors(
|
||||
background = ElementTheme.colors.bgSubtlePrimary,
|
||||
foreground = ElementTheme.colors.iconPrimary,
|
||||
),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components.avatar
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import io.element.android.compound.theme.AvatarColors
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||
|
||||
@Composable
|
||||
fun TombstonedRoomAvatar(
|
||||
size: AvatarSize,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
TextAvatar(
|
||||
text = "!",
|
||||
size = size.dp,
|
||||
colors = AvatarColors(
|
||||
background = ElementTheme.colors.bgSubtlePrimary,
|
||||
foreground = ElementTheme.colors.iconTertiary
|
||||
),
|
||||
modifier = modifier,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.Avatars)
|
||||
@Composable
|
||||
internal fun TombstonedRoomAvatarPreview() = ElementPreview {
|
||||
TombstonedRoomAvatar(
|
||||
size = AvatarSize.RoomListItem,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
@@ -14,30 +14,30 @@ class AvatarDataTest {
|
||||
@Test
|
||||
fun `initial with text should get the first char, uppercased`() {
|
||||
val data = AvatarData("id", "test", null, AvatarSize.InviteSender)
|
||||
assertThat(data.initial).isEqualTo("T")
|
||||
assertThat(data.initialLetter).isEqualTo("T")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial with leading whitespace should get the first non-whitespace char, uppercased`() {
|
||||
val data = AvatarData("id", " test", null, AvatarSize.InviteSender)
|
||||
assertThat(data.initial).isEqualTo("T")
|
||||
assertThat(data.initialLetter).isEqualTo("T")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial with long emoji should get the full emoji`() {
|
||||
val data = AvatarData("id", "\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08 Test", null, AvatarSize.InviteSender)
|
||||
assertThat(data.initial).isEqualTo("\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08")
|
||||
assertThat(data.initialLetter).isEqualTo("\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial with short emoji should get the emoji`() {
|
||||
val data = AvatarData("id", "✂ Test", null, AvatarSize.InviteSender)
|
||||
assertThat(data.initial).isEqualTo("✂")
|
||||
assertThat(data.initialLetter).isEqualTo("✂")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial with a single letter should take that letter`() {
|
||||
val data = AvatarData("id", "T", null, AvatarSize.InviteSender)
|
||||
assertThat(data.initial).isEqualTo("T")
|
||||
assertThat(data.initialLetter).isEqualTo("T")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,12 @@ fun aSelectRoomInfo(
|
||||
canonicalAlias: RoomAlias? = null,
|
||||
avatarUrl: String? = null,
|
||||
heroes: ImmutableList<MatrixUser> = persistentListOf(),
|
||||
isTombstoned: Boolean = false,
|
||||
) = SelectRoomInfo(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
canonicalAlias = canonicalAlias,
|
||||
avatarUrl = avatarUrl,
|
||||
heroes = heroes,
|
||||
isTombstoned = isTombstoned,
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
@@ -53,9 +53,10 @@ fun SelectedRoom(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CompositeAvatar(
|
||||
RoomAvatar(
|
||||
avatarData = roomInfo.getAvatarData(AvatarSize.SelectedRoom),
|
||||
heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(),
|
||||
isTombstoned = roomInfo.isTombstoned,
|
||||
)
|
||||
Text(
|
||||
// If name is null, we do not have space to render "No room name", so just use `#` here.
|
||||
|
||||
@@ -21,6 +21,7 @@ data class SelectRoomInfo(
|
||||
val canonicalAlias: RoomAlias?,
|
||||
val avatarUrl: String?,
|
||||
val heroes: ImmutableList<MatrixUser>,
|
||||
val isTombstoned: Boolean,
|
||||
) {
|
||||
fun getAvatarData(size: AvatarSize) = AvatarData(
|
||||
id = roomId.value,
|
||||
@@ -36,4 +37,5 @@ fun RoomSummary.toSelectRoomInfo() = SelectRoomInfo(
|
||||
avatarUrl = info.avatarUrl,
|
||||
heroes = info.heroes,
|
||||
canonicalAlias = info.canonicalAlias,
|
||||
isTombstoned = info.successorRoom != null,
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -214,11 +214,12 @@ private fun RoomSummaryView(
|
||||
.heightIn(56.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CompositeAvatar(
|
||||
RoomAvatar(
|
||||
avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem),
|
||||
heroes = roomInfo.heroes.map { user ->
|
||||
user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem)
|
||||
}.toPersistentList()
|
||||
}.toPersistentList(),
|
||||
isTombstoned = roomInfo.isTombstoned,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user