From 2241c947a458b7170a67ebb86768da7fb42774f3 Mon Sep 17 00:00:00 2001 From: yostyle Date: Wed, 21 Aug 2024 11:31:22 +0200 Subject: [PATCH] Hide user ids --- .../mentions/MentionSuggestionsPickerView.kt | 22 +- .../reactionsummary/ReactionSummaryView.kt | 18 +- .../user/editprofile/EditUserProfileView.kt | 15 +- .../roomdetails/impl/RoomDetailsView.kt | 2 +- .../moderation/RoomMembersModerationView.kt | 31 ++- .../changeroles/ChangeRolesView.kt | 21 +- .../impl/components/RoomSummaryRow.kt | 2 +- .../shared/UserProfileHeaderSection.kt | 19 +- .../userprofile/shared/UserProfileView.kt | 3 +- libraries/designsystem/build.gradle.kts | 1 + .../components/avatar/AvatarData.kt | 3 +- libraries/matrix/api/build.gradle.kts | 1 + .../libraries/matrix/api/core/UserIdExt.kt | 30 +++ .../libraries/matrix/api/room/RoomMember.kt | 5 +- libraries/matrixui/build.gradle.kts | 1 + .../matrix/ui/components/MatrixUserHeader.kt | 21 +- .../libraries/matrix/ui/components/UserRow.kt | 21 +- .../matrix/ui/messages/sender/SenderName.kt | 8 +- .../libraries/matrix/ui/model/InviteSender.kt | 2 +- .../matrix/ui/model/MatrixUserExtensions.kt | 3 +- libraries/tchaputils/build.gradle.kts | 11 + .../libraries/tchaputils/TchapPatterns.kt | 225 ++++++++++++++++++ .../libraries/tchaputils/TchapPatternsTest.kt | 123 ++++++++++ .../model/MarkdownTextEditorState.kt | 2 +- 24 files changed, 499 insertions(+), 91 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/fr/gouv/tchap/android/libraries/matrix/api/core/UserIdExt.kt create mode 100644 libraries/tchaputils/build.gradle.kts create mode 100644 libraries/tchaputils/src/main/java/fr/gouv/tchap/libraries/tchaputils/TchapPatterns.kt create mode 100644 libraries/tchaputils/src/test/java/fr/gouv/tchap/libraries/tchaputils/TchapPatternsTest.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt index 48c626680e..75b61abf09 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/mentions/MentionSuggestionsPickerView.kt @@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -105,12 +106,12 @@ private fun RoomMemberSuggestionItemView( } val title = when (memberSuggestion) { is ResolvedMentionSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title) - is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.displayName + is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.getBestName() // TCHAP should be applied in Element X } val subtitle = when (memberSuggestion) { is ResolvedMentionSuggestion.AtRoom -> "@room" - is ResolvedMentionSuggestion.Member -> memberSuggestion.roomMember.userId.value + is ResolvedMentionSuggestion.Member -> null // TCHAP hide the Matrix Id } Avatar(avatarData = avatarData, modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)) @@ -129,13 +130,16 @@ private fun RoomMemberSuggestionItemView( overflow = TextOverflow.Ellipsis, ) } - Text( - text = subtitle, - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + // TCHAP hide the Matrix Id + if (subtitle != null) { + Text( + text = subtitle, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt index c21d0deaa5..e4ba35b296 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt @@ -76,6 +76,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.matrix.ui.model.getBestName import kotlinx.coroutines.launch internal val REACTION_SUMMARY_LINE_HEIGHT = 25.sp @@ -156,7 +157,7 @@ private fun SheetContent( SenderRow( avatarData = user.getAvatarData(AvatarSize.UserListItem), - name = user.displayName ?: user.userId.value, + name = user.getBestName(), // TCHAP should be applied in Element X userId = user.userId.value, sentTime = sender.sentTime ) @@ -270,13 +271,14 @@ private fun SenderRow( style = ElementTheme.typography.fontBodySmRegular, ) } - Text( - text = userId, - color = MaterialTheme.colorScheme.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ElementTheme.typography.fontBodySmRegular, - ) + // TCHAP hide the Matrix Id +// Text( +// text = userId, +// color = MaterialTheme.colorScheme.secondary, +// maxLines = 1, +// overflow = TextOverflow.Ellipsis, +// style = ElementTheme.typography.fontBodySmRegular, +// ) } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt index d191ef2eee..6ab85a55ab 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt @@ -113,13 +113,14 @@ fun EditUserProfileView( onAvatarClick = { onAvatarClick() }, modifier = Modifier.align(Alignment.CenterHorizontally), ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - modifier = Modifier.fillMaxWidth(), - text = state.userId.value, - style = ElementTheme.typography.fontBodyLgRegular, - textAlign = TextAlign.Center, - ) + // TCHAP hide the Matrix Id +// Spacer(modifier = Modifier.height(16.dp)) +// Text( +// modifier = Modifier.fillMaxWidth(), +// text = state.userId.value, +// style = ElementTheme.typography.fontBodyLgRegular, +// textAlign = TextAlign.Center, +// ) Spacer(modifier = Modifier.height(40.dp)) LabelledOutlinedTextField( label = stringResource(R.string.screen_edit_profile_display_name), diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index fc2a051dc6..632b0fb386 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -368,7 +368,7 @@ private fun DmHeaderSection( ) TitleAndSubtitle( title = roomName, - subtitle = otherMember.userId.value, + subtitle = null, // TCHAP hide the Matrix Id ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt index a96e4ef46f..2d89d313fa 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/moderation/RoomMembersModerationView.kt @@ -222,29 +222,28 @@ private fun RoomMemberActionsBottomSheet( .padding(bottom = 28.dp) .align(Alignment.CenterHorizontally) ) - roomMember.displayName?.let { - Text( - text = it, - style = ElementTheme.typography.fontHeadingLgBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - .fillMaxWidth() - ) - } + // TCHAP hide the Matrix Id Text( - text = roomMember.userId.toString(), - style = ElementTheme.typography.fontBodyLgRegular, - color = ElementTheme.colors.textSecondary, + text = roomMember.getBestName(), + style = ElementTheme.typography.fontHeadingLgBold, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, modifier = Modifier - .padding(horizontal = 16.dp) + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) .fillMaxWidth() ) +// Text( +// text = roomMember.userId.toString(), +// style = ElementTheme.typography.fontBodyLgRegular, +// color = ElementTheme.colors.textSecondary, +// maxLines = 1, +// overflow = TextOverflow.Ellipsis, +// textAlign = TextAlign.Center, +// modifier = Modifier +// .padding(horizontal = 16.dp) +// .fillMaxWidth() +// ) Spacer(modifier = Modifier.height(32.dp)) for (action in actions) { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt index d89ad29b78..541638d753 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesView.kt @@ -378,16 +378,17 @@ private fun MemberRow( ) } } - // Id - userId?.let { - Text( - text = userId, - color = MaterialTheme.colorScheme.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ElementTheme.typography.fontBodySmRegular, - ) - } + // TCHAP hide the Matrix Id +// // Id +// userId?.let { +// Text( +// text = userId, +// color = MaterialTheme.colorScheme.secondary, +// maxLines = 1, +// overflow = TextOverflow.Ellipsis, +// style = ElementTheme.typography.fontBodySmRegular, +// ) +// } } trailingContent?.invoke() } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt index d80f1c7334..bf111aeb6b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt @@ -212,7 +212,7 @@ private fun InviteSubtitle( modifier: Modifier = Modifier ) { val subtitle = if (isDm) { - inviteSender?.userId?.value + null // TCHAP hide the Matrix Id } else { canonicalAlias?.value } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt index 78d87c275d..0a02326842 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt @@ -70,15 +70,16 @@ fun UserProfileHeaderSection( ) Spacer(modifier = Modifier.height(6.dp)) } - Text( - text = userId.value, - style = ElementTheme.typography.fontBodyLgRegular, - color = MaterialTheme.colorScheme.secondary, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - textAlign = TextAlign.Center, - ) + // TCHAP hide the Matrix Id +// Text( +// text = userId.value, +// style = ElementTheme.typography.fontBodyLgRegular, +// color = MaterialTheme.colorScheme.secondary, +// modifier = Modifier +// .fillMaxWidth() +// .padding(horizontal = 16.dp), +// textAlign = TextAlign.Center, +// ) Spacer(Modifier.height(40.dp)) } } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt index f147798b19..911a3a59f2 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import fr.gouv.tchap.android.libraries.matrix.api.core.toDisplayName import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs import io.element.android.features.userprofile.shared.blockuser.BlockUserSection import io.element.android.libraries.designsystem.components.async.AsyncActionView @@ -69,7 +70,7 @@ fun UserProfileView( UserProfileHeaderSection( avatarUrl = state.avatarUrl, userId = state.userId, - userName = state.userName, + userName = state.userName ?: state.userId.toDisplayName(), // TCHAP hide the Matrix Id openAvatarPreview = { avatarUrl -> openAvatarPreview(state.userName ?: state.userId.value, avatarUrl) }, diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 81abc3127f..0085c6e6ac 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -42,6 +42,7 @@ android { implementation(libs.vanniktech.blurhash) implementation(projects.libraries.architecture) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.tchaputils) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt index 331c0fad4a..1d77c77729 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.designsystem.components.avatar import androidx.compose.runtime.Immutable +import fr.gouv.tchap.libraries.tchaputils.TchapPatterns.toDisplayName @Immutable data class AvatarData( @@ -60,5 +61,5 @@ data class AvatarData( } fun AvatarData.getBestName(): String { - return name?.takeIf { it.isNotEmpty() } ?: id + return name?.takeIf { it.isNotEmpty() } ?: id.toDisplayName() } diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index b551dfa919..b54902ff3c 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -35,6 +35,7 @@ anvil { dependencies { implementation(projects.libraries.di) + implementation(projects.libraries.tchaputils) implementation(libs.dagger) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) diff --git a/libraries/matrix/api/src/main/kotlin/fr/gouv/tchap/android/libraries/matrix/api/core/UserIdExt.kt b/libraries/matrix/api/src/main/kotlin/fr/gouv/tchap/android/libraries/matrix/api/core/UserIdExt.kt new file mode 100644 index 0000000000..cb00bb4ac5 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/fr/gouv/tchap/android/libraries/matrix/api/core/UserIdExt.kt @@ -0,0 +1,30 @@ +/* + * MIT License + * + * Copyright (c) 2024. DINUM + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fr.gouv.tchap.android.libraries.matrix.api.core + +import fr.gouv.tchap.libraries.tchaputils.TchapPatterns.toDisplayName +import io.element.android.libraries.matrix.api.core.UserId + +fun UserId.toDisplayName() = this.value.toDisplayName() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt index 4415e51327..2ef227b777 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.api.room +import fr.gouv.tchap.android.libraries.matrix.api.core.toDisplayName import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser @@ -56,7 +57,7 @@ data class RoomMember( * Otherwise, the display name is returned. */ val disambiguatedDisplayName: String = when { - displayName == null -> userId.value + displayName == null -> userId.toDisplayName() // TCHAP hide the Matrix Id isNameAmbiguous -> "$displayName ($userId)" else -> displayName } @@ -77,7 +78,7 @@ enum class RoomMembershipState { * If the [RoomMember.displayName] is present and not empty it'll be used, otherwise the [RoomMember.userId] will be used. */ fun RoomMember.getBestName(): String { - return displayName?.takeIf { it.isNotEmpty() } ?: userId.value + return displayName?.takeIf { it.isNotEmpty() } ?: userId.toDisplayName() // TCHAP hide the Matrix Id } fun RoomMember.toMatrixUser() = MatrixUser( diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts index 6a8a799cf3..ca64dd341b 100644 --- a/libraries/matrixui/build.gradle.kts +++ b/libraries/matrixui/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.core) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.tchaputils) implementation(projects.libraries.testtags) implementation(libs.coil.compose) implementation(libs.coil.gif) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt index 581b70dfb1..91ad15bd7a 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt @@ -88,16 +88,17 @@ private fun MatrixUserHeaderContent( overflow = TextOverflow.Ellipsis, color = ElementTheme.materialColors.primary, ) - // Id - if (matrixUser.displayName.isNullOrEmpty().not()) { - Text( - text = matrixUser.userId.value, - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.materialColors.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } + // TCHAP hide the Matrix Id +// // Id +// if (matrixUser.displayName.isNullOrEmpty().not()) { +// Text( +// text = matrixUser.userId.value, +// style = ElementTheme.typography.fontBodyMdRegular, +// color = ElementTheme.materialColors.secondary, +// maxLines = 1, +// overflow = TextOverflow.Ellipsis +// ) +// } } } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt index f9eef6b990..96a18624f2 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt @@ -63,16 +63,17 @@ internal fun UserRow( color = MaterialTheme.colorScheme.primary, style = ElementTheme.typography.fontBodyLgRegular, ) - // Id - subtext?.let { - Text( - text = subtext, - color = MaterialTheme.colorScheme.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ElementTheme.typography.fontBodySmRegular, - ) - } + // TCHAP hide the Matrix Id +// // Id +// subtext?.let { +// Text( +// text = subtext, +// color = MaterialTheme.colorScheme.secondary, +// maxLines = 1, +// overflow = TextOverflow.Ellipsis, +// style = ElementTheme.typography.fontBodySmRegular, +// ) +// } } trailingContent?.invoke() } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt index 5ff84ff292..e1bdee4739 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt @@ -27,6 +27,8 @@ import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import fr.gouv.tchap.android.libraries.matrix.api.core.toDisplayName +import fr.gouv.tchap.libraries.tchaputils.TchapPatterns.toDisplayName import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -51,16 +53,16 @@ fun SenderName( is ProfileTimelineDetails.Error, ProfileTimelineDetails.Pending, ProfileTimelineDetails.Unavailable -> { - MainText(text = senderId.value, mode = senderNameMode) + MainText(text = senderId.toDisplayName(), mode = senderNameMode) // TCHAP hide the Matrix Id } is ProfileTimelineDetails.Ready -> { val displayName = senderProfile.displayName if (displayName.isNullOrEmpty()) { - MainText(text = senderId.value, mode = senderNameMode) + MainText(text = senderId.toDisplayName(), mode = senderNameMode) // TCHAP hide the Matrix Id } else { MainText(text = displayName, mode = senderNameMode) if (senderProfile.displayNameAmbiguous) { - SecondaryText(text = senderId.value, mode = senderNameMode) + SecondaryText(text = senderId.toDisplayName(), mode = senderNameMode) // TCHAP hide the Matrix Id } } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/InviteSender.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/InviteSender.kt index dcc8e6e327..8920c5f5c8 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/InviteSender.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/InviteSender.kt @@ -39,7 +39,7 @@ data class InviteSender( @Composable fun annotatedString(): AnnotatedString { return stringResource(R.string.screen_invites_invited_you, displayName, userId.value).let { text -> - val senderNameStart = LocalContext.current.getString(R.string.screen_invites_invited_you).indexOf("%1\$s") + val senderNameStart = LocalContext.current.getString(R.string.screen_invites_invited_you).indexOf("%1\$s") // TCHAP text should be changed to hide the user id AnnotatedString( text = text, spanStyles = listOf( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt index 813f4744b0..31e65ecc38 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUserExtensions.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.ui.model +import fr.gouv.tchap.android.libraries.matrix.api.core.toDisplayName import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.user.MatrixUser @@ -28,5 +29,5 @@ fun MatrixUser.getAvatarData(size: AvatarSize) = AvatarData( ) fun MatrixUser.getBestName(): String { - return displayName?.takeIf { it.isNotEmpty() } ?: userId.value + return displayName?.takeIf { it.isNotEmpty() } ?: userId.toDisplayName() // TCHAP hide the Matrix Id } diff --git a/libraries/tchaputils/build.gradle.kts b/libraries/tchaputils/build.gradle.kts new file mode 100644 index 0000000000..65de17dceb --- /dev/null +++ b/libraries/tchaputils/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "fr.gouv.tchap.libraries.tchaputils" +} + +dependencies { + testImplementation(libs.test.junit) +} diff --git a/libraries/tchaputils/src/main/java/fr/gouv/tchap/libraries/tchaputils/TchapPatterns.kt b/libraries/tchaputils/src/main/java/fr/gouv/tchap/libraries/tchaputils/TchapPatterns.kt new file mode 100644 index 0000000000..e8822c208b --- /dev/null +++ b/libraries/tchaputils/src/main/java/fr/gouv/tchap/libraries/tchaputils/TchapPatterns.kt @@ -0,0 +1,225 @@ +/* + * MIT License + * + * Copyright (c) 2024. DINUM + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fr.gouv.tchap.libraries.tchaputils + +import java.util.Locale + +object TchapPatterns { + + /** + * Get the homeserver name of a matrix identifier. + * The identifier type may be any matrix identifier type: user id, room id, ... + * For example in case of "@jean-philippe.martin-modernisation.fr:matrix.test.org", this will return "matrix.test.org". + * in case of "!AAAAAAA:matrix.test.org", this will return "matrix.test.org". + * + * @param mxId the matrix identifier. + * @return the homeserver name, if any. + */ + fun getHomeserverNameFromMxId(mxId: String): String = mxId.substringAfter(":", "") + + /** + * Get the Tchap display name of the homeserver mentioned in a matrix identifier. + * The identifier type may be any matrix identifier type: user id, room id, ... + * The returned name is capitalize. + * The Tchap HS display name is the component mentioned before the suffix "tchap.gouv.fr" + * For example in case of "@jean-philippe.martin-modernisation.fr:name1.tchap.gouv.fr", this will return "Name1". + * in case of "@jean-philippe.martin-modernisation.fr:agent.name2.tchap.gouv.fr", this will return "Name2". + * + * @param mxId the matrix identifier. + * @return the Tchap display name of the homeserver. + */ + fun getHomeserverDisplayNameFromMxId(mxId: String): String { + var homeserverName = getHomeserverNameFromMxId(mxId) + if (homeserverName.contains("tchap.gouv.fr")) { + homeserverName.split("\\.".toRegex()).let { + if (it.size >= 4) homeserverName = it[it.size - 4] + } + } + return homeserverName.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + } + + /** + * Tells whether a homeserver name corresponds to an external server or not. + * + * @param homeServerName the homeserver name to check. + * @return true if external. + */ + fun String.isExternalTchapServer() = this.isEmpty() || this.startsWith("e.") || this.startsWith("agent.externe.") + + /** + * Get name part of a display name by removing the domain part if any. + * For example in case of "Jean Martin `[Modernisation]`", this will return "Jean Martin". + * + * @param displayName the display name to compute. + * @return displayName without domain (or the display name itself if no domain has been found). + */ + fun getNameFromDisplayName(displayName: String): String { + return displayName.split(DISPLAY_NAME_FIRST_DELIMITER) + .first() + .trim() + } + + /** + * Get the room name from a display name according to the given room type. + * In the case of a direct message, it will remove the domain part of the display name. + * For example in case of "Jean Martin `[Modernisation]`", this will return "Jean Martin". + * + * Otherwise, it will keep the initial name. + * + * @param displayName the display name to compute. + * @param roomType the room type associated with the given display name. + * + * @return displayName without domain (or the display name itself if the room is not a DM). + */ +// fun getRoomNameFromDisplayName(displayName: String, roomType: TchapRoomType): String { +// return when (roomType) { +// TchapRoomType.DIRECT -> getNameFromDisplayName(displayName) +// else -> displayName +// } +// } + + /** + * Get the potential domain name from a display name. + * For example in case of "Jean Martin `[Modernisation]`", this will return "Modernisation". + * + * @param displayName the display name to compute. + * @return displayName without name, empty string if no domain is available. + */ + fun getDomainFromDisplayName(displayName: String): String { + return displayName.split(DISPLAY_NAME_FIRST_DELIMITER) + .elementAtOrNull(1) + ?.split(DISPLAY_NAME_SECOND_DELIMITER) + ?.first() + ?.trim() + ?: DEFAULT_EMPTY_STRING + } + + /** + * Build a display name from the tchap user identifier. + * We don't extract the domain for the moment in order to not display unexpected information. + * For example in case of "@jean-philippe.martin-modernisation.fr:matrix.org", this will return "Jean-Philippe Martin". + * Note: in case of an external user identifier, we return the local part of the id which corresponds to their email. + * + * @return displayName without domain, or null if the user identifier is not valid. + */ + fun String.toDisplayName(): String { + // Extract identifier from user ID. + val identifier = this.substringAfter('@').substringBefore(':') + val lastHyphenIndex = identifier.lastIndexOf('-') + + // Return the identifier as-is if no transformations were needed. + if (lastHyphenIndex == -1) return identifier + + return if (this.isExternalTchapUser()) { + // Handle external Tchap user case: replace single hyphen with '@'. + if (identifier.indexOf('-') == lastHyphenIndex) { + identifier.replaceRange(lastHyphenIndex..lastHyphenIndex, "@") + } else identifier + } else { + // Handle internal user case. + buildString { + var capitalizeNext = true + for (i in 0 until lastHyphenIndex) { + val char = identifier[i] + when { + (capitalizeNext && (char == '.' || char == '-')) -> continue + char == '.' -> { + // Replace the dot character by space character + append(' ') + capitalizeNext = true + } + char == '-' -> { + append(char) + capitalizeNext = true + } + capitalizeNext -> { + append(char.uppercaseChar()) + capitalizeNext = false + } + else -> append(char) + } + } + } + } + } + + /** + * Tells whether the provided tchap identifier corresponds to an extern user. + * Note: invalid tchap identifier will be considered as external. + * + * @param tchapUserId user identifier (ie. the matrix identifier). + * @return true if external. + */ + fun String.isExternalTchapUser(): Boolean { + val homeServerName = getHomeserverNameFromMxId(this) + return homeServerName.isExternalTchapServer() + } + + /** + * Create a room alias name with a prefix. + * + * @param prefix the alias name prefix. + * @return the suggested alias name. + */ + fun createRoomAliasName(prefix: String): String { + return prefix.trim().replace("[^a-zA-Z0-9]".toRegex(), "") + getRandomString(10) + } + + /** + * Create a room alias with a prefix. + * + * @param session the user's session. + * @param prefix the alias name prefix. + * @return the suggested alias. + */ + fun createRoomAlias(sessionId: String, prefix: String): String { + return "#${createRoomAliasName(prefix)}:${getHomeserverNameFromMxId(sessionId)}" + } + + /** + * Extract the local part of the given room alias. + * + * @param roomAlias the room alias to parse. + * @return the alias local part. + */ + fun extractRoomAliasName(roomAlias: String): String { + return roomAlias.substringAfter("#").substringBefore(":") + } + + /** + * Generate a random string of the given number of characters. + * + * @param length the random string length. + * @return the resulting random string. + */ + fun getRandomString(length: Int): String { + val charPool = ('a'..'z') + ('A'..'Z') + ('0'..'9') + return (1..length).map { charPool.random() }.joinToString("") + } + + private const val DISPLAY_NAME_FIRST_DELIMITER = "[" + private const val DISPLAY_NAME_SECOND_DELIMITER = "]" + private const val DEFAULT_EMPTY_STRING = "" +} diff --git a/libraries/tchaputils/src/test/java/fr/gouv/tchap/libraries/tchaputils/TchapPatternsTest.kt b/libraries/tchaputils/src/test/java/fr/gouv/tchap/libraries/tchaputils/TchapPatternsTest.kt new file mode 100644 index 0000000000..16d9673c2b --- /dev/null +++ b/libraries/tchaputils/src/test/java/fr/gouv/tchap/libraries/tchaputils/TchapPatternsTest.kt @@ -0,0 +1,123 @@ +/* + * MIT License + * + * Copyright (c) 2024. DINUM + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fr.gouv.tchap.libraries.tchaputils + +import fr.gouv.tchap.libraries.tchaputils.TchapPatterns.toDisplayName +import org.junit.Assert.assertEquals +import org.junit.Test + +class TchapPatternsTest { + + /** + * Test getting full name without domain. + */ + + @Test + fun `given a displayName if it contains brackets then return its first element after split`() { + // Given + val displayNameTest = "Nom Prenom [Modernisation]" + + // When + val result = fr.gouv.tchap.libraries.tchaputils.TchapPatterns.getNameFromDisplayName(displayNameTest) + + // Then + assertEquals(result, "Nom Prenom") + } + + @Test + fun `given a displayName if it doesn't contain brackets then return the original display name`() { + // Given + val displayNameTest = "Nom Prenom" + + // When + val result = fr.gouv.tchap.libraries.tchaputils.TchapPatterns.getNameFromDisplayName(displayNameTest) + + // Then + assertEquals(result, "Nom Prenom") + } + + /** + * Test getting domain only. + */ + + @Test + fun `given a displayName if it contains brackets then return domain name inside`() { + // Given + val displayNameTest = "Nom Prenom [Modernisation]" + + // When + val result = fr.gouv.tchap.libraries.tchaputils.TchapPatterns.getDomainFromDisplayName(displayNameTest) + + // Then + assertEquals(result, "Modernisation") + } + + @Test + fun `given a displayName if it doesn't contain brackets then return empty string`() { + // Given + val displayNameTest = "Nom Prenom" + + // When + val result = fr.gouv.tchap.libraries.tchaputils.TchapPatterns.getDomainFromDisplayName(displayNameTest) + + // Then + assertEquals(result, "") + } + + @Test + fun computeDisplayNameFromUserId_simple() { + assertEquals("Jean Martin", "@jean.martin-modernisation.fr:matrix.org".toDisplayName()) + } + + @Test + fun computeDisplayNameFromUserId_dash() { + assertEquals("Jean-Philippe Martin", "@jean-philippe.martin-modernisation.fr:matrix.org".toDisplayName()) + } + + @Test + fun computeDisplayNameFromUserId_dashes() { + assertEquals("Jean Martin De-La-Rampe", "@jean.martin.de-la-rampe-modernisation.gouv.fr:a.tchap.gouv.fr".toDisplayName()) + } + + @Test + fun computeDisplayNameFromUserId_emptydashes() { + assertEquals("Jean Martin De-La-Rampe", "@jean..martin..de--la--rampe-modernisation.gouv.fr:a.tchap.gouv.fr".toDisplayName()) + } + + @Test + fun computeDisplayNameFromUserId_dash_in_domain() { + assertEquals("Jerome Ploquin4-Developpement", "@jerome.ploquin4-developpement-durable.gouv.fr:a.tchap.gouv.fr".toDisplayName()) + } + + @Test + fun computeDisplayNameFromUserId_external_user() { + assertEquals("jerome.ploquin@otherdomain.fr", "@jerome.ploquin-otherdomain.fr:agent.externe.gouv.fr".toDisplayName()) + } + + @Test + fun computeDisplayNameFromUserId_external_user_dashes() { + assertEquals("jean-philippe.martin-other-domain.fr", "@jean-philippe.martin-other-domain.fr:agent.externe.gouv.fr".toDisplayName()) + } +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt index dd03c66366..e2ae4a58ef 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MarkdownTextEditorState.kt @@ -72,7 +72,7 @@ class MarkdownTextEditorState( } is ResolvedMentionSuggestion.Member -> { val currentText = SpannableStringBuilder(text.value()) - val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value + val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value // TCHAP check needed about mxid displaying val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return val mentionPill = mentionSpanProvider.getMentionSpanFor(text, link) currentText.replace(suggestion.start, suggestion.end, "@ ")