Merge pull request #4891 from element-hq/feature/fga/tombstoned-room-decoration
Change : add tombstoned room decoration
This commit is contained in:
commit
7bde9bc0c8
48 changed files with 449 additions and 274 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:19f6f6dcdf49e9ffd2fbb0ea81c3fe0374faf79cecccc812ea1e1d8c05578671
|
||||
size 22344
|
||||
oid sha256:8823f1cca4a240779ade39486854332689d114efe2d2213a9ab0720113c7f04b
|
||||
size 22353
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:12266ee1fc3477aec0f47e731294ccd2f93ecf4a3a1fada6cf4cb8ba6f7430ac
|
||||
size 22661
|
||||
oid sha256:49916050cd93d9b4aa4a90aa3ecd19bec6095e8b7cb472a3543867c0ce6f9ffa
|
||||
size 22783
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:79e6ab4ac13eeb03cb02843a122b519a6410aeaad40e5f272d0b09f2547af6e7
|
||||
size 62973
|
||||
oid sha256:45ead771fc133b1c992d090f35c93f0de4166b52966b818d8a401e75e5b63caf
|
||||
size 62982
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:62475d0e6b37cff42980135438faeccebe74bc110f517c12616351be8469c448
|
||||
size 55208
|
||||
oid sha256:4493799d889522a540435c10f0bf3642a438c38d6b219491afff4a72a169ce1c
|
||||
size 58416
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3241f342c62665d1a343d085ec9174e0e6555219ce51e74f4199bea74f641183
|
||||
size 65429
|
||||
oid sha256:5864ca4671dee1caef50d0444ffdeb0c7d2a5d9ed28ebc64937efbc469f6031a
|
||||
size 65288
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f4209f327f3a7cb3fbb0de25230604857e9d0e39b7ca0b2bec948656f08b2e08
|
||||
size 54804
|
||||
oid sha256:b44213fdccb6b194466d3daa7474b77378f96ab9b35559c0ba392dde49ecf175
|
||||
size 58055
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60571dc08867db9ad2f6528e95cc65df0eb57b9840bf8e96e6e1fbd802ad3a26
|
||||
size 38778
|
||||
oid sha256:f3cc4924fbbb2fea5937bf481b1322da14e1d2c8ea2cf4351fd6d0d222357738
|
||||
size 41304
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:69d9aaf46305802861df8be0341abd73068376893bfd7aef43d7b8f3251350dd
|
||||
size 38735
|
||||
oid sha256:60571dc08867db9ad2f6528e95cc65df0eb57b9840bf8e96e6e1fbd802ad3a26
|
||||
size 38778
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:69d9aaf46305802861df8be0341abd73068376893bfd7aef43d7b8f3251350dd
|
||||
size 38735
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cabfc7c5bf6f9b612e933cad211b8261b236cd981488cfa081e0226940626339
|
||||
size 39499
|
||||
oid sha256:63c75fdca8960f015caaa11bbb970c6e7621e87afbe7761d23db875664c55cd1
|
||||
size 42360
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf96db08ac0382185c34863b38603fe55eb3aab691fa3c42b6d73e4167ce8f82
|
||||
size 39383
|
||||
oid sha256:cabfc7c5bf6f9b612e933cad211b8261b236cd981488cfa081e0226940626339
|
||||
size 39499
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cf96db08ac0382185c34863b38603fe55eb3aab691fa3c42b6d73e4167ce8f82
|
||||
size 39383
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:26ec46ce726cb5a36accb280cc019a1a93d065bc6f6c74027d42efb4f7709f6a
|
||||
size 14566
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc7cf18c82801dd7aea6383a0f17fdbba11e92f9e4cead38ac609d7a2238837f
|
||||
size 14203
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c6afcae51ee69dcdb9dbf60c8f72e55b9b07e4feb72dfba0a40ffeebed6ab681
|
||||
size 25730
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:374b810a6ef7011ec1270b7d4faddc8feacef10d167a5b2fc2c920d87f7d8e2c
|
||||
size 27831
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0645d454bcaac9e38fe9b6400347ee0fc92a71ef531f72b9ec527f644e050862
|
||||
size 5488
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:50abd66f2c58d8cc18a96c73120a779882cd121049e473646518ff6841e25e30
|
||||
size 5106
|
||||
Loading…
Add table
Add a link
Reference in a new issue