Merge pull request #4891 from element-hq/feature/fga/tombstoned-room-decoration
Change : add tombstoned room decoration
This commit is contained in:
commit
b2e17622ed
48 changed files with 449 additions and 274 deletions
|
|
@ -152,11 +152,8 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
|
val userEventPermissions by userEventPermissions(syncUpdateFlow.value)
|
||||||
|
|
||||||
val roomName: AsyncData<String> by remember {
|
val roomAvatar by remember {
|
||||||
derivedStateOf { roomInfo.name?.let { AsyncData.Success(it) } ?: AsyncData.Uninitialized }
|
derivedStateOf { roomInfo.avatarData() }
|
||||||
}
|
|
||||||
val roomAvatar: AsyncData<AvatarData> by remember {
|
|
||||||
derivedStateOf { AsyncData.Success(roomInfo.avatarData()) }
|
|
||||||
}
|
}
|
||||||
val heroes by remember {
|
val heroes by remember {
|
||||||
derivedStateOf { roomInfo.heroes().toPersistentList() }
|
derivedStateOf { roomInfo.heroes().toPersistentList() }
|
||||||
|
|
@ -245,7 +242,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
return MessagesState(
|
return MessagesState(
|
||||||
roomId = room.roomId,
|
roomId = room.roomId,
|
||||||
roomName = roomName,
|
roomName = roomInfo.name,
|
||||||
roomAvatar = roomAvatar,
|
roomAvatar = roomAvatar,
|
||||||
heroes = heroes,
|
heroes = heroes,
|
||||||
composerState = composerState,
|
composerState = composerState,
|
||||||
|
|
@ -292,7 +289,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
return AvatarData(
|
return AvatarData(
|
||||||
id = id.value,
|
id = id.value,
|
||||||
name = name,
|
name = name,
|
||||||
url = avatarUrl ?: room.info().avatarUrl,
|
url = avatarUrl,
|
||||||
size = AvatarSize.TimelineRoom
|
size = AvatarSize.TimelineRoom
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
@Immutable
|
@Immutable
|
||||||
data class MessagesState(
|
data class MessagesState(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val roomName: AsyncData<String>,
|
val roomName: String?,
|
||||||
val roomAvatar: AsyncData<AvatarData>,
|
val roomAvatar: AvatarData,
|
||||||
val heroes: ImmutableList<AvatarData>,
|
val heroes: ImmutableList<AvatarData>,
|
||||||
val userEventPermissions: UserEventPermissions,
|
val userEventPermissions: UserEventPermissions,
|
||||||
val composerState: MessageComposerState,
|
val composerState: MessageComposerState,
|
||||||
|
|
@ -59,4 +59,6 @@ data class MessagesState(
|
||||||
val roomMemberModerationState: RoomMemberModerationState,
|
val roomMemberModerationState: RoomMemberModerationState,
|
||||||
val successorRoom: SuccessorRoom?,
|
val successorRoom: SuccessorRoom?,
|
||||||
val eventSink: (MessagesEvents) -> Unit
|
val eventSink: (MessagesEvents) -> Unit
|
||||||
)
|
) {
|
||||||
|
val isTombstoned = successorRoom != null
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
|
aMessagesState(composerState = aMessageComposerState(showAttachmentSourcePicker = true)),
|
||||||
aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)),
|
aMessagesState(userEventPermissions = aUserEventPermissions(canSendMessage = false)),
|
||||||
aMessagesState(showReinvitePrompt = true),
|
aMessagesState(showReinvitePrompt = true),
|
||||||
aMessagesState(
|
aMessagesState(roomName = null),
|
||||||
roomName = AsyncData.Uninitialized,
|
|
||||||
roomAvatar = AsyncData.Uninitialized,
|
|
||||||
),
|
|
||||||
aMessagesState(composerState = aMessageComposerState(showTextFormatting = true)),
|
aMessagesState(composerState = aMessageComposerState(showTextFormatting = true)),
|
||||||
aMessagesState(
|
aMessagesState(
|
||||||
enableVoiceMessages = true,
|
enableVoiceMessages = true,
|
||||||
|
|
@ -86,15 +83,15 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
currentPinnedMessageIndex = 0,
|
currentPinnedMessageIndex = 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
aMessagesState(roomName = AsyncData.Success("A DM with a very looong name"), dmUserVerificationState = IdentityState.Verified),
|
aMessagesState(roomName = "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.VerificationViolation),
|
||||||
aMessagesState(successorRoom = SuccessorRoom(RoomId("!id:domain"), null)),
|
aMessagesState(successorRoom = SuccessorRoom(RoomId("!id:domain"), null)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aMessagesState(
|
fun aMessagesState(
|
||||||
roomName: AsyncData<String> = AsyncData.Success("Room name"),
|
roomName: String? = "Room name",
|
||||||
roomAvatar: AsyncData<AvatarData> = AsyncData.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)),
|
roomAvatar: AvatarData = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom),
|
||||||
userEventPermissions: UserEventPermissions = aUserEventPermissions(),
|
userEventPermissions: UserEventPermissions = aUserEventPermissions(),
|
||||||
composerState: MessageComposerState = aMessageComposerState(
|
composerState: MessageComposerState = aMessageComposerState(
|
||||||
textEditorState = aTextEditorStateRich(initialText = "Hello", initialFocus = true),
|
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.features.roomcall.api.RoomCallState
|
||||||
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
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.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.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -194,8 +192,9 @@ fun MessagesView(
|
||||||
Column {
|
Column {
|
||||||
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
ConnectivityIndicatorView(isOnline = state.hasNetworkConnection)
|
||||||
MessagesViewTopBar(
|
MessagesViewTopBar(
|
||||||
roomName = state.roomName.dataOrNull(),
|
roomName = state.roomName,
|
||||||
roomAvatar = state.roomAvatar.dataOrNull(),
|
roomAvatar = state.roomAvatar,
|
||||||
|
isTombstoned = state.isTombstoned,
|
||||||
heroes = state.heroes,
|
heroes = state.heroes,
|
||||||
roomCallState = state.roomCallState,
|
roomCallState = state.roomCallState,
|
||||||
dmUserIdentityState = state.dmUserVerificationState,
|
dmUserIdentityState = state.dmUserVerificationState,
|
||||||
|
|
@ -450,8 +449,8 @@ private fun MessagesViewComposerBottomSheetContents(
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
roomId = state.roomId,
|
roomId = state.roomId,
|
||||||
roomName = state.roomName.dataOrNull(),
|
roomName = state.roomName,
|
||||||
roomAvatarData = state.roomAvatar.dataOrNull(),
|
roomAvatarData = state.roomAvatar,
|
||||||
suggestions = state.composerState.suggestions,
|
suggestions = state.composerState.suggestions,
|
||||||
onSelectSuggestion = {
|
onSelectSuggestion = {
|
||||||
state.composerState.eventSink(MessageComposerEvents.InsertSuggestion(it))
|
state.composerState.eventSink(MessageComposerEvents.InsertSuggestion(it))
|
||||||
|
|
@ -491,7 +490,8 @@ private fun MessagesViewComposerBottomSheetContents(
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessagesViewTopBar(
|
private fun MessagesViewTopBar(
|
||||||
roomName: String?,
|
roomName: String?,
|
||||||
roomAvatar: AvatarData?,
|
roomAvatar: AvatarData,
|
||||||
|
isTombstoned: Boolean,
|
||||||
heroes: ImmutableList<AvatarData>,
|
heroes: ImmutableList<AvatarData>,
|
||||||
roomCallState: RoomCallState,
|
roomCallState: RoomCallState,
|
||||||
dmUserIdentityState: IdentityState?,
|
dmUserIdentityState: IdentityState?,
|
||||||
|
|
@ -513,19 +513,13 @@ private fun MessagesViewTopBar(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
val titleModifier = Modifier.weight(1f, fill = false)
|
val titleModifier = Modifier.weight(1f, fill = false)
|
||||||
if (roomName != null && roomAvatar != null) {
|
RoomAvatarAndNameRow(
|
||||||
RoomAvatarAndNameRow(
|
roomName = roomName,
|
||||||
roomName = roomName,
|
roomAvatar = roomAvatar,
|
||||||
roomAvatar = roomAvatar,
|
isTombstoned = isTombstoned,
|
||||||
heroes = heroes,
|
heroes = heroes,
|
||||||
modifier = titleModifier
|
modifier = titleModifier
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
IconTitlePlaceholdersRowMolecule(
|
|
||||||
iconSize = AvatarSize.TimelineRoom.dp,
|
|
||||||
modifier = titleModifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
when (dmUserIdentityState) {
|
when (dmUserIdentityState) {
|
||||||
IdentityState.Verified -> {
|
IdentityState.Verified -> {
|
||||||
|
|
@ -559,23 +553,26 @@ private fun MessagesViewTopBar(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RoomAvatarAndNameRow(
|
private fun RoomAvatarAndNameRow(
|
||||||
roomName: String,
|
roomName: String?,
|
||||||
roomAvatar: AvatarData,
|
roomAvatar: AvatarData,
|
||||||
heroes: ImmutableList<AvatarData>,
|
heroes: ImmutableList<AvatarData>,
|
||||||
|
isTombstoned: Boolean,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
CompositeAvatar(
|
RoomAvatar(
|
||||||
avatarData = roomAvatar,
|
avatarData = roomAvatar,
|
||||||
heroes = heroes,
|
heroes = heroes,
|
||||||
|
isTombstoned = isTombstoned,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
text = roomName,
|
text = roomName ?: stringResource(CommonStrings.common_no_room_name),
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
fontStyle = FontStyle.Italic.takeIf { roomName == null },
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|
@ -586,9 +583,9 @@ private fun RoomAvatarAndNameRow(
|
||||||
private fun CantSendMessageBanner() {
|
private fun CantSendMessageBanner() {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(ElementTheme.colors.bgSubtleSecondary)
|
.background(ElementTheme.colors.bgSubtleSecondary)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center
|
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.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
|
|
@ -45,7 +46,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
fun SuggestionsPickerView(
|
fun SuggestionsPickerView(
|
||||||
roomId: RoomId,
|
roomId: RoomId,
|
||||||
roomName: String?,
|
roomName: String?,
|
||||||
roomAvatarData: AvatarData?,
|
roomAvatarData: AvatarData,
|
||||||
suggestions: ImmutableList<ResolvedSuggestion>,
|
suggestions: ImmutableList<ResolvedSuggestion>,
|
||||||
onSelectSuggestion: (ResolvedSuggestion) -> Unit,
|
onSelectSuggestion: (ResolvedSuggestion) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -155,7 +156,7 @@ internal fun SuggestionsPickerViewPreview() {
|
||||||
SuggestionsPickerView(
|
SuggestionsPickerView(
|
||||||
roomId = RoomId("!room:matrix.org"),
|
roomId = RoomId("!room:matrix.org"),
|
||||||
roomName = "Room",
|
roomName = "Room",
|
||||||
roomAvatarData = null,
|
roomAvatarData = anAvatarData(),
|
||||||
suggestions = persistentListOf(
|
suggestions = persistentListOf(
|
||||||
ResolvedSuggestion.AtRoom,
|
ResolvedSuggestion.AtRoom,
|
||||||
ResolvedSuggestion.Member(roomMember),
|
ResolvedSuggestion.Member(roomMember),
|
||||||
|
|
|
||||||
|
|
@ -119,9 +119,9 @@ class MessagesPresenterTest {
|
||||||
presenter.testWithLifecycleOwner {
|
presenter.testWithLifecycleOwner {
|
||||||
val initialState = consumeItemsUntilTimeout().last()
|
val initialState = consumeItemsUntilTimeout().last()
|
||||||
assertThat(initialState.roomId).isEqualTo(A_ROOM_ID)
|
assertThat(initialState.roomId).isEqualTo(A_ROOM_ID)
|
||||||
assertThat(initialState.roomName).isEqualTo(AsyncData.Success(""))
|
assertThat(initialState.roomName).isEqualTo("")
|
||||||
assertThat(initialState.roomAvatar)
|
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.canSendMessage).isTrue()
|
||||||
assertThat(initialState.userEventPermissions.canRedactOwn).isTrue()
|
assertThat(initialState.userEventPermissions.canRedactOwn).isTrue()
|
||||||
assertThat(initialState.hasNetworkConnection).isTrue()
|
assertThat(initialState.hasNetworkConnection).isTrue()
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ class MessagesViewTest {
|
||||||
state = state,
|
state = state,
|
||||||
onRoomDetailsClick = callback,
|
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 androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.avatar.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.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
|
|
@ -97,7 +97,7 @@ fun EditDefaultNotificationSettingView(
|
||||||
Text(text = subtitle)
|
Text(text = subtitle)
|
||||||
},
|
},
|
||||||
leadingContent = ListItemContent.Custom {
|
leadingContent = ListItemContent.Custom {
|
||||||
CompositeAvatar(
|
RoomAvatar(
|
||||||
avatarData = summary.avatarData,
|
avatarData = summary.avatarData,
|
||||||
heroes = summary.heroesAvatar,
|
heroes = summary.heroesAvatar,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,
|
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,
|
||||||
hasMemberVerificationViolations = hasMemberVerificationViolations,
|
hasMemberVerificationViolations = hasMemberVerificationViolations,
|
||||||
canReportRoom = canReportRoom,
|
canReportRoom = canReportRoom,
|
||||||
|
isTombstoned = roomInfo.successorRoom != null,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ data class RoomDetailsState(
|
||||||
val canShowSecurityAndPrivacy: Boolean,
|
val canShowSecurityAndPrivacy: Boolean,
|
||||||
val hasMemberVerificationViolations: Boolean,
|
val hasMemberVerificationViolations: Boolean,
|
||||||
val canReportRoom: Boolean,
|
val canReportRoom: Boolean,
|
||||||
|
val isTombstoned: Boolean,
|
||||||
val eventSink: (RoomDetailsEvent) -> Unit
|
val eventSink: (RoomDetailsEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
val roomBadges = buildList {
|
val roomBadges = buildList {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
||||||
aRoomDetailsState(knockRequestsCount = null, canShowKnockRequests = true),
|
aRoomDetailsState(knockRequestsCount = null, canShowKnockRequests = true),
|
||||||
aRoomDetailsState(knockRequestsCount = 4, canShowKnockRequests = true),
|
aRoomDetailsState(knockRequestsCount = 4, canShowKnockRequests = true),
|
||||||
aRoomDetailsState(hasMemberVerificationViolations = true),
|
aRoomDetailsState(hasMemberVerificationViolations = true),
|
||||||
|
aRoomDetailsState(isTombstoned = true),
|
||||||
aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFIED),
|
aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFIED),
|
||||||
aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFICATION_VIOLATION),
|
aDmRoomDetailsState(dmRoomMemberVerificationState = UserProfileVerificationState.VERIFICATION_VIOLATION),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
|
|
@ -118,6 +119,7 @@ fun aRoomDetailsState(
|
||||||
canShowSecurityAndPrivacy: Boolean = true,
|
canShowSecurityAndPrivacy: Boolean = true,
|
||||||
hasMemberVerificationViolations: Boolean = false,
|
hasMemberVerificationViolations: Boolean = false,
|
||||||
canReportRoom: Boolean = true,
|
canReportRoom: Boolean = true,
|
||||||
|
isTombstoned: Boolean = false,
|
||||||
eventSink: (RoomDetailsEvent) -> Unit = {},
|
eventSink: (RoomDetailsEvent) -> Unit = {},
|
||||||
) = RoomDetailsState(
|
) = RoomDetailsState(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
|
|
@ -148,6 +150,7 @@ fun aRoomDetailsState(
|
||||||
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,
|
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,
|
||||||
hasMemberVerificationViolations = hasMemberVerificationViolations,
|
hasMemberVerificationViolations = hasMemberVerificationViolations,
|
||||||
canReportRoom = canReportRoom,
|
canReportRoom = canReportRoom,
|
||||||
|
isTombstoned = isTombstoned,
|
||||||
eventSink = eventSink,
|
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.ClickableLinkText
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.DmAvatars
|
import io.element.android.libraries.designsystem.components.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.BackButton
|
||||||
import io.element.android.libraries.designsystem.components.button.MainActionButton
|
import io.element.android.libraries.designsystem.components.button.MainActionButton
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
|
|
@ -138,6 +138,7 @@ fun RoomDetailsView(
|
||||||
roomName = state.roomName,
|
roomName = state.roomName,
|
||||||
roomAlias = state.roomAlias,
|
roomAlias = state.roomAlias,
|
||||||
heroes = state.heroes,
|
heroes = state.heroes,
|
||||||
|
isTombstoned = state.isTombstoned,
|
||||||
openAvatarPreview = { avatarUrl ->
|
openAvatarPreview = { avatarUrl ->
|
||||||
openAvatarPreview(state.roomName, avatarUrl)
|
openAvatarPreview(state.roomName, avatarUrl)
|
||||||
},
|
},
|
||||||
|
|
@ -380,6 +381,7 @@ private fun RoomHeaderSection(
|
||||||
roomName: String,
|
roomName: String,
|
||||||
roomAlias: RoomAlias?,
|
roomAlias: RoomAlias?,
|
||||||
heroes: ImmutableList<MatrixUser>,
|
heroes: ImmutableList<MatrixUser>,
|
||||||
|
isTombstoned: Boolean,
|
||||||
openAvatarPreview: (url: String) -> Unit,
|
openAvatarPreview: (url: String) -> Unit,
|
||||||
onSubtitleClick: (String) -> Unit,
|
onSubtitleClick: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -389,11 +391,12 @@ private fun RoomHeaderSection(
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
CompositeAvatar(
|
RoomAvatar(
|
||||||
avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader),
|
avatarData = AvatarData(roomId.value, roomName, avatarUrl, AvatarSize.RoomHeader),
|
||||||
heroes = heroes.map { user ->
|
heroes = heroes.map { user ->
|
||||||
user.getAvatarData(size = AvatarSize.RoomHeader)
|
user.getAvatarData(size = AvatarSize.RoomHeader)
|
||||||
}.toPersistentList(),
|
}.toPersistentList(),
|
||||||
|
isTombstoned = isTombstoned,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||||
.testTag(TestTags.roomDetailAvatar)
|
.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.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||||
import io.element.android.libraries.core.extensions.orEmpty
|
import io.element.android.libraries.core.extensions.orEmpty
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||||
import io.element.android.libraries.designsystem.components.avatar.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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
|
@ -121,7 +121,7 @@ internal fun RoomSummaryRow(
|
||||||
timestamp = room.timestamp,
|
timestamp = room.timestamp,
|
||||||
isHighlighted = room.isHighlighted
|
isHighlighted = room.isHighlighted
|
||||||
)
|
)
|
||||||
LastMessageAndIndicatorRow(room = room)
|
MessagePreviewAndIndicatorRow(room = room)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RoomSummaryDisplayType.KNOCKED -> {
|
RoomSummaryDisplayType.KNOCKED -> {
|
||||||
|
|
@ -184,10 +184,11 @@ private fun RoomSummaryScaffoldRow(
|
||||||
.padding(horizontal = 16.dp, vertical = 11.dp)
|
.padding(horizontal = 16.dp, vertical = 11.dp)
|
||||||
.height(IntrinsicSize.Min),
|
.height(IntrinsicSize.Min),
|
||||||
) {
|
) {
|
||||||
CompositeAvatar(
|
RoomAvatar(
|
||||||
avatarData = room.avatarData,
|
avatarData = room.avatarData,
|
||||||
heroes = room.heroes,
|
heroes = room.heroes,
|
||||||
hideAvatarImages = hideAvatarImage,
|
isTombstoned = room.isTombstoned,
|
||||||
|
hideAvatarImage = hideAvatarImage,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -255,7 +256,7 @@ private fun InviteSubtitle(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LastMessageAndIndicatorRow(
|
private fun MessagePreviewAndIndicatorRow(
|
||||||
room: RoomListRoomSummary,
|
room: RoomListRoomSummary,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -263,12 +264,15 @@ private fun LastMessageAndIndicatorRow(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = spacedBy(28.dp)
|
horizontalArrangement = spacedBy(28.dp)
|
||||||
) {
|
) {
|
||||||
// Last Message
|
val messagePreview = if (room.isTombstoned) {
|
||||||
val attributedLastMessage = room.lastMessage as? AnnotatedString
|
stringResource(R.string.screen_roomlist_tombstoned_room_description)
|
||||||
?: AnnotatedString(room.lastMessage.orEmpty().toString())
|
} else {
|
||||||
|
room.lastMessage.orEmpty()
|
||||||
|
}
|
||||||
|
val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.toString())
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
text = attributedLastMessage,
|
text = annotatedMessagePreview,
|
||||||
color = ElementTheme.roomListRoomMessage(),
|
color = ElementTheme.roomListRoomMessage(),
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
minLines = 2,
|
minLines = 2,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||||
heroes = roomInfo.heroes.map { user ->
|
heroes = roomInfo.heroes.map { user ->
|
||||||
user.getAvatarData(size = AvatarSize.RoomListItem)
|
user.getAvatarData(size = AvatarSize.RoomListItem)
|
||||||
}.toImmutableList(),
|
}.toImmutableList(),
|
||||||
|
isTombstoned = roomInfo.successorRoom != null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ data class RoomListRoomSummary(
|
||||||
val isDm: Boolean,
|
val isDm: Boolean,
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean,
|
||||||
val inviteSender: InviteSender?,
|
val inviteSender: InviteSender?,
|
||||||
|
val isTombstoned: Boolean,
|
||||||
val heroes: ImmutableList<AvatarData>,
|
val heroes: ImmutableList<AvatarData>,
|
||||||
) {
|
) {
|
||||||
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,11 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||||
name = "A knocked room with alias",
|
name = "A knocked room with alias",
|
||||||
canonicalAlias = RoomAlias("#knockable:matrix.org"),
|
canonicalAlias = RoomAlias("#knockable:matrix.org"),
|
||||||
displayType = RoomSummaryDisplayType.KNOCKED,
|
displayType = RoomSummaryDisplayType.KNOCKED,
|
||||||
|
),
|
||||||
|
aRoomListRoomSummary(
|
||||||
|
name = "A tombstoned room",
|
||||||
|
displayType = RoomSummaryDisplayType.ROOM,
|
||||||
|
isTombstoned = true,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
).flatten()
|
).flatten()
|
||||||
|
|
@ -145,6 +150,7 @@ internal fun aRoomListRoomSummary(
|
||||||
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||||
canonicalAlias: RoomAlias? = null,
|
canonicalAlias: RoomAlias? = null,
|
||||||
heroes: List<AvatarData> = emptyList(),
|
heroes: List<AvatarData> = emptyList(),
|
||||||
|
isTombstoned: Boolean = false,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = id,
|
id = id,
|
||||||
roomId = RoomId(id),
|
roomId = RoomId(id),
|
||||||
|
|
@ -165,4 +171,5 @@ internal fun aRoomListRoomSummary(
|
||||||
displayType = displayType,
|
displayType = displayType,
|
||||||
canonicalAlias = canonicalAlias,
|
canonicalAlias = canonicalAlias,
|
||||||
heroes = heroes.toImmutableList(),
|
heroes = heroes.toImmutableList(),
|
||||||
|
isTombstoned = isTombstoned,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ internal fun createRoomListRoomSummary(
|
||||||
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||||
heroes: List<AvatarData> = emptyList(),
|
heroes: List<AvatarData> = emptyList(),
|
||||||
timestamp: String? = null,
|
timestamp: String? = null,
|
||||||
|
isTombstoned: Boolean = false,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = A_ROOM_ID.value,
|
id = A_ROOM_ID.value,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
|
|
@ -104,4 +105,5 @@ internal fun createRoomListRoomSummary(
|
||||||
inviteSender = null,
|
inviteSender = null,
|
||||||
isDm = false,
|
isDm = false,
|
||||||
heroes = heroes.toPersistentList(),
|
heroes = heroes.toPersistentList(),
|
||||||
|
isTombstoned = isTombstoned,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,7 @@ fun Modifier.avatarBloom(
|
||||||
val initialsBitmap = initialsBitmap(
|
val initialsBitmap = initialsBitmap(
|
||||||
width = BloomDefaults.ENCODE_SIZE_PX.toDp(),
|
width = BloomDefaults.ENCODE_SIZE_PX.toDp(),
|
||||||
height = BloomDefaults.ENCODE_SIZE_PX.toDp(),
|
height = BloomDefaults.ENCODE_SIZE_PX.toDp(),
|
||||||
text = avatarData.initial,
|
text = avatarData.initialLetter,
|
||||||
textColor = avatarColors.foreground,
|
textColor = avatarColors.foreground,
|
||||||
backgroundColor = avatarColors.background,
|
backgroundColor = avatarColors.background,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.designsystem.components.avatar
|
package io.element.android.libraries.designsystem.components.avatar
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
|
@ -21,21 +19,16 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
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.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import coil3.compose.AsyncImagePainter
|
import coil3.compose.AsyncImagePainter
|
||||||
import coil3.compose.SubcomposeAsyncImage
|
import coil3.compose.SubcomposeAsyncImage
|
||||||
import coil3.compose.SubcomposeAsyncImageContent
|
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.colors.AvatarColorsProvider
|
||||||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
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.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -50,21 +43,18 @@ fun Avatar(
|
||||||
// If true, will show initials even if avatarData.url is not null
|
// If true, will show initials even if avatarData.url is not null
|
||||||
hideImage: Boolean = false,
|
hideImage: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val commonModifier = modifier
|
|
||||||
.size(forcedAvatarSize ?: avatarData.size.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
if (avatarData.url.isNullOrBlank() || hideImage) {
|
if (avatarData.url.isNullOrBlank() || hideImage) {
|
||||||
InitialsAvatar(
|
InitialLetterAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
forcedAvatarSize = forcedAvatarSize,
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
modifier = commonModifier,
|
modifier = modifier,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ImageAvatar(
|
ImageAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
forcedAvatarSize = forcedAvatarSize,
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
modifier = commonModifier,
|
modifier = modifier,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -77,11 +67,14 @@ private fun ImageAvatar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentDescription: String? = null,
|
contentDescription: String? = null,
|
||||||
) {
|
) {
|
||||||
|
val size = forcedAvatarSize ?: avatarData.size.dp
|
||||||
SubcomposeAsyncImage(
|
SubcomposeAsyncImage(
|
||||||
model = avatarData,
|
model = avatarData,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(CircleShape)
|
||||||
) {
|
) {
|
||||||
val collectedState by painter.state.collectAsState()
|
val collectedState by painter.state.collectAsState()
|
||||||
when (val state = collectedState) {
|
when (val state = collectedState) {
|
||||||
|
|
@ -90,13 +83,13 @@ private fun ImageAvatar(
|
||||||
SideEffect {
|
SideEffect {
|
||||||
Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
|
Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
|
||||||
}
|
}
|
||||||
InitialsAvatar(
|
InitialLetterAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
forcedAvatarSize = forcedAvatarSize,
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> InitialsAvatar(
|
else -> InitialLetterAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
forcedAvatarSize = forcedAvatarSize,
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
|
|
@ -106,33 +99,20 @@ private fun ImageAvatar(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun InitialsAvatar(
|
private fun InitialLetterAvatar(
|
||||||
avatarData: AvatarData,
|
avatarData: AvatarData,
|
||||||
forcedAvatarSize: Dp?,
|
forcedAvatarSize: Dp?,
|
||||||
contentDescription: String?,
|
contentDescription: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val avatarColors = AvatarColorsProvider.provide(avatarData.id)
|
val avatarColors = AvatarColorsProvider.provide(avatarData.id)
|
||||||
Box(
|
TextAvatar(
|
||||||
modifier.background(color = avatarColors.background)
|
text = avatarData.initialLetter,
|
||||||
) {
|
size = forcedAvatarSize ?: avatarData.size.dp,
|
||||||
val fontSize = (forcedAvatarSize ?: avatarData.size.dp).toSp() / 2
|
colors = avatarColors,
|
||||||
val originalFont = ElementTheme.typography.fontHeadingMdBold
|
contentDescription = contentDescription,
|
||||||
val ratio = fontSize.value / originalFont.fontSize.value
|
modifier = modifier
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(group = PreviewGroup.Avatars)
|
@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 url: String? = null,
|
||||||
val size: AvatarSize,
|
val size: AvatarSize,
|
||||||
) {
|
) {
|
||||||
val initial by lazy {
|
val initialLetter by lazy {
|
||||||
// For roomIds, use "#" as initial
|
// For roomIds, use "#" as initial
|
||||||
(name?.takeIf { it.isNotBlank() } ?: id.takeIf { !it.startsWith("!") } ?: "#")
|
(name?.takeIf { it.isNotBlank() } ?: id.takeIf { !it.startsWith("!") } ?: "#")
|
||||||
.let { dn ->
|
.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
|
@Test
|
||||||
fun `initial with text should get the first char, uppercased`() {
|
fun `initial with text should get the first char, uppercased`() {
|
||||||
val data = AvatarData("id", "test", null, AvatarSize.InviteSender)
|
val data = AvatarData("id", "test", null, AvatarSize.InviteSender)
|
||||||
assertThat(data.initial).isEqualTo("T")
|
assertThat(data.initialLetter).isEqualTo("T")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial with leading whitespace should get the first non-whitespace char, uppercased`() {
|
fun `initial with leading whitespace should get the first non-whitespace char, uppercased`() {
|
||||||
val data = AvatarData("id", " test", null, AvatarSize.InviteSender)
|
val data = AvatarData("id", " test", null, AvatarSize.InviteSender)
|
||||||
assertThat(data.initial).isEqualTo("T")
|
assertThat(data.initialLetter).isEqualTo("T")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial with long emoji should get the full emoji`() {
|
fun `initial with long emoji should get the full emoji`() {
|
||||||
val data = AvatarData("id", "\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08 Test", null, AvatarSize.InviteSender)
|
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
|
@Test
|
||||||
fun `initial with short emoji should get the emoji`() {
|
fun `initial with short emoji should get the emoji`() {
|
||||||
val data = AvatarData("id", "✂ Test", null, AvatarSize.InviteSender)
|
val data = AvatarData("id", "✂ Test", null, AvatarSize.InviteSender)
|
||||||
assertThat(data.initial).isEqualTo("✂")
|
assertThat(data.initialLetter).isEqualTo("✂")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial with a single letter should take that letter`() {
|
fun `initial with a single letter should take that letter`() {
|
||||||
val data = AvatarData("id", "T", null, AvatarSize.InviteSender)
|
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,
|
canonicalAlias: RoomAlias? = null,
|
||||||
avatarUrl: String? = null,
|
avatarUrl: String? = null,
|
||||||
heroes: ImmutableList<MatrixUser> = persistentListOf(),
|
heroes: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
|
isTombstoned: Boolean = false,
|
||||||
) = SelectRoomInfo(
|
) = SelectRoomInfo(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
name = name,
|
name = name,
|
||||||
canonicalAlias = canonicalAlias,
|
canonicalAlias = canonicalAlias,
|
||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
heroes = heroes,
|
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.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
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.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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
|
@ -53,9 +53,10 @@ fun SelectedRoom(
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
CompositeAvatar(
|
RoomAvatar(
|
||||||
avatarData = roomInfo.getAvatarData(AvatarSize.SelectedRoom),
|
avatarData = roomInfo.getAvatarData(AvatarSize.SelectedRoom),
|
||||||
heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(),
|
heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(),
|
||||||
|
isTombstoned = roomInfo.isTombstoned,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
// If name is null, we do not have space to render "No room name", so just use `#` here.
|
// 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 canonicalAlias: RoomAlias?,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String?,
|
||||||
val heroes: ImmutableList<MatrixUser>,
|
val heroes: ImmutableList<MatrixUser>,
|
||||||
|
val isTombstoned: Boolean,
|
||||||
) {
|
) {
|
||||||
fun getAvatarData(size: AvatarSize) = AvatarData(
|
fun getAvatarData(size: AvatarSize) = AvatarData(
|
||||||
id = roomId.value,
|
id = roomId.value,
|
||||||
|
|
@ -36,4 +37,5 @@ fun RoomSummary.toSelectRoomInfo() = SelectRoomInfo(
|
||||||
avatarUrl = info.avatarUrl,
|
avatarUrl = info.avatarUrl,
|
||||||
heroes = info.heroes,
|
heroes = info.heroes,
|
||||||
canonicalAlias = info.canonicalAlias,
|
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 androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
|
import io.element.android.libraries.designsystem.components.avatar.RoomAvatar
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
|
@ -214,11 +214,12 @@ private fun RoomSummaryView(
|
||||||
.heightIn(56.dp),
|
.heightIn(56.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
CompositeAvatar(
|
RoomAvatar(
|
||||||
avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem),
|
avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem),
|
||||||
heroes = roomInfo.heroes.map { user ->
|
heroes = roomInfo.heroes.map { user ->
|
||||||
user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem)
|
user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem)
|
||||||
}.toPersistentList()
|
}.toPersistentList(),
|
||||||
|
isTombstoned = roomInfo.isTombstoned,
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:19f6f6dcdf49e9ffd2fbb0ea81c3fe0374faf79cecccc812ea1e1d8c05578671
|
oid sha256:8823f1cca4a240779ade39486854332689d114efe2d2213a9ab0720113c7f04b
|
||||||
size 22344
|
size 22353
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:12266ee1fc3477aec0f47e731294ccd2f93ecf4a3a1fada6cf4cb8ba6f7430ac
|
oid sha256:49916050cd93d9b4aa4a90aa3ecd19bec6095e8b7cb472a3543867c0ce6f9ffa
|
||||||
size 22661
|
size 22783
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:79e6ab4ac13eeb03cb02843a122b519a6410aeaad40e5f272d0b09f2547af6e7
|
oid sha256:45ead771fc133b1c992d090f35c93f0de4166b52966b818d8a401e75e5b63caf
|
||||||
size 62973
|
size 62982
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:62475d0e6b37cff42980135438faeccebe74bc110f517c12616351be8469c448
|
oid sha256:4493799d889522a540435c10f0bf3642a438c38d6b219491afff4a72a169ce1c
|
||||||
size 55208
|
size 58416
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3241f342c62665d1a343d085ec9174e0e6555219ce51e74f4199bea74f641183
|
oid sha256:5864ca4671dee1caef50d0444ffdeb0c7d2a5d9ed28ebc64937efbc469f6031a
|
||||||
size 65429
|
size 65288
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f4209f327f3a7cb3fbb0de25230604857e9d0e39b7ca0b2bec948656f08b2e08
|
oid sha256:b44213fdccb6b194466d3daa7474b77378f96ab9b35559c0ba392dde49ecf175
|
||||||
size 54804
|
size 58055
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:60571dc08867db9ad2f6528e95cc65df0eb57b9840bf8e96e6e1fbd802ad3a26
|
oid sha256:f3cc4924fbbb2fea5937bf481b1322da14e1d2c8ea2cf4351fd6d0d222357738
|
||||||
size 38778
|
size 41304
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:69d9aaf46305802861df8be0341abd73068376893bfd7aef43d7b8f3251350dd
|
oid sha256:60571dc08867db9ad2f6528e95cc65df0eb57b9840bf8e96e6e1fbd802ad3a26
|
||||||
size 38735
|
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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cabfc7c5bf6f9b612e933cad211b8261b236cd981488cfa081e0226940626339
|
oid sha256:63c75fdca8960f015caaa11bbb970c6e7621e87afbe7761d23db875664c55cd1
|
||||||
size 39499
|
size 42360
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cf96db08ac0382185c34863b38603fe55eb3aab691fa3c42b6d73e4167ce8f82
|
oid sha256:cabfc7c5bf6f9b612e933cad211b8261b236cd981488cfa081e0226940626339
|
||||||
size 39383
|
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