Skip to content

Commit

Permalink
refactor: make learn more links clickable for automation [WPB-5888] (#…
Browse files Browse the repository at this point in the history
…2923)

Co-authored-by: Michał Saleniuk <[email protected]>
Co-authored-by: Michał Saleniuk <[email protected]>
  • Loading branch information
3 people authored Apr 23, 2024
1 parent 114d60a commit 0acad2f
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
Expand Down Expand Up @@ -243,67 +244,67 @@ fun LoginErrorDialog(
) {
val dialogErrorData: LoginDialogErrorData = when (error) {
is LoginError.DialogError.InvalidCredentialsError -> LoginDialogErrorData(
stringResource(R.string.login_error_invalid_credentials_title),
stringResource(R.string.login_error_invalid_credentials_message),
onDialogDismiss
title = stringResource(R.string.login_error_invalid_credentials_title),
body = AnnotatedString(stringResource(R.string.login_error_invalid_credentials_message)),
onDismiss = onDialogDismiss
)

is LoginError.DialogError.UserAlreadyExists -> LoginDialogErrorData(
stringResource(R.string.login_error_user_already_logged_in_title),
stringResource(R.string.login_error_user_already_logged_in_message),
onDialogDismiss
title = stringResource(R.string.login_error_user_already_logged_in_title),
body = AnnotatedString(stringResource(R.string.login_error_user_already_logged_in_message)),
onDismiss = onDialogDismiss
)

is LoginError.DialogError.ProxyError -> {
LoginDialogErrorData(
stringResource(R.string.error_socket_title),
stringResource(R.string.error_socket_message),
onDialogDismiss
title = stringResource(R.string.error_socket_title),
body = AnnotatedString(stringResource(R.string.error_socket_message)),
onDismiss = onDialogDismiss
)
}

is LoginError.DialogError.GenericError -> {
val strings = error.coreFailure.dialogErrorStrings(LocalContext.current.resources)
LoginDialogErrorData(
strings.title,
strings.message,
strings.annotatedMessage,
onDialogDismiss
)
}

is LoginError.DialogError.InvalidSSOCodeError -> LoginDialogErrorData(
stringResource(R.string.login_error_invalid_credentials_title),
stringResource(R.string.login_error_invalid_sso_code),
onDialogDismiss
title = stringResource(R.string.login_error_invalid_credentials_title),
body = AnnotatedString(stringResource(R.string.login_error_invalid_sso_code)),
onDismiss = onDialogDismiss
)

is LoginError.DialogError.InvalidSSOCookie -> LoginDialogErrorData(
stringResource(R.string.login_sso_error_invalid_cookie_title),
stringResource(R.string.login_sso_error_invalid_cookie_message),
onDialogDismiss
title = stringResource(R.string.login_sso_error_invalid_cookie_title),
body = AnnotatedString(stringResource(R.string.login_sso_error_invalid_cookie_message)),
onDismiss = onDialogDismiss
)

is LoginError.DialogError.SSOResultError -> {
with(ssoLoginResult as DeepLinkResult.SSOLogin.Failure) {
LoginDialogErrorData(
stringResource(R.string.sso_error_dialog_title),
stringResource(R.string.sso_error_dialog_message, this.ssoError.errorCode),
onDialogDismiss
title = stringResource(R.string.sso_error_dialog_title),
body = AnnotatedString(stringResource(R.string.sso_error_dialog_message, this.ssoError.errorCode)),
onDismiss = onDialogDismiss
)
}
}

is LoginError.DialogError.ServerVersionNotSupported -> LoginDialogErrorData(
title = stringResource(R.string.api_versioning_server_version_not_supported_title),
body = stringResource(R.string.api_versioning_server_version_not_supported_message),
body = AnnotatedString(stringResource(R.string.api_versioning_server_version_not_supported_message)),
onDismiss = onDialogDismiss,
actionTextId = R.string.label_close,
dismissOnClickOutside = false
)

is LoginError.DialogError.ClientUpdateRequired -> LoginDialogErrorData(
title = stringResource(R.string.api_versioning_client_update_required_title),
body = stringResource(R.string.api_versioning_client_update_required_message),
body = AnnotatedString(stringResource(R.string.api_versioning_client_update_required_message)),
onDismiss = onDialogDismiss,
actionTextId = R.string.label_update,
onAction = updateTheApp,
Expand All @@ -313,9 +314,9 @@ fun LoginErrorDialog(
LoginError.DialogError.PasswordNeededToRegisterClient -> TODO()

else -> LoginDialogErrorData(
stringResource(R.string.error_unknown_title),
stringResource(R.string.error_unknown_message),
onDialogDismiss
title = stringResource(R.string.error_unknown_title),
body = AnnotatedString(stringResource(R.string.error_unknown_message)),
onDismiss = onDialogDismiss
)
}

Expand All @@ -338,7 +339,7 @@ fun LoginErrorDialog(

data class LoginDialogErrorData(
val title: String,
val body: String,
val body: AnnotatedString,
val onDismiss: () -> Unit,
@StringRes val actionTextId: Int = R.string.label_ok,
val onAction: () -> Unit = onDismiss,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.ui.common

import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Dp
import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.ui.PreviewMultipleThemes

@Composable
private fun PreviewTextWithLinkSuffixBuilder(
textLines: List<String> = listOf("This is a text with a link"),
linkText: String = "link",
calculateWidth: (lastTextLineWidthDp: Dp, linkWidthDp: Dp) -> Dp
) {
val textStyle = MaterialTheme.wireTypography.body01
val linkStyle = MaterialTheme.wireTypography.body02.copy(textDecoration = TextDecoration.Underline)
val textMeasurer = rememberTextMeasurer()
val lastTextLineLayoutResult = textMeasurer.measure(text = "${textLines.last()} ", style = textStyle)
val linkLayoutResult = textMeasurer.measure(text = linkText, style = linkStyle)
val density = LocalDensity.current
val lastTextLineWidthDp = with(density) { lastTextLineLayoutResult.size.width.toDp() }
val linkWidthDp = with(density) { linkLayoutResult.size.width.toDp() }
TextWithLinkSuffix(
text = AnnotatedString(textLines.joinToString(separator = "\n")),
linkText = linkText,
onLinkClick = {},
modifier = Modifier.width(calculateWidth(lastTextLineWidthDp, linkWidthDp))
)
}

@PreviewMultipleThemes
@Composable
fun PreviewTextWithLinkSuffixWithoutALink() = WireTheme {
TextWithLinkSuffix(text = AnnotatedString("This is a text without a link"))
}

@PreviewMultipleThemes
@Composable
fun PreviewTextWithLinkSuffixFittingInSameLine() = WireTheme {
PreviewTextWithLinkSuffixBuilder { lastTextLineWidthDp, linkWidthDp -> lastTextLineWidthDp + linkWidthDp }
}

@PreviewMultipleThemes
@Composable
fun PreviewTextWithLinkSuffixNotFittingInSameLine() = WireTheme {
PreviewTextWithLinkSuffixBuilder { lastTextLineWidthDp, linkWidthDp -> lastTextLineWidthDp + (linkWidthDp / 2) }
}

@PreviewMultipleThemes
@Composable
fun PreviewTextWithLinkSuffixMultilineFittingInLastLine() = WireTheme {
PreviewTextWithLinkSuffixBuilder(
textLines = listOf("This is a text with a link", "This is a text with a"),
linkText = "link",
) { lastTextLineWidthDp, linkWidthDp -> lastTextLineWidthDp + linkWidthDp }
}

@PreviewMultipleThemes
@Composable
fun PreviewTextWithLinkSuffixMultilineNotFittingInLastLine() = WireTheme {
PreviewTextWithLinkSuffixBuilder(
textLines = listOf("This is a text with a", "This is a text with a"),
linkText = "link"
) { lastTextLineWidthDp, linkWidthDp -> lastTextLineWidthDp + (linkWidthDp / 2) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
Expand All @@ -41,11 +40,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import com.wire.android.R
import com.wire.android.ui.common.TextWithLinkSuffix
import com.wire.android.ui.common.button.WireSecondaryButton
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.spacers.VerticalSpace
Expand Down Expand Up @@ -104,40 +101,18 @@ fun SystemMessageItem(
errorColor = MaterialTheme.wireColorScheme.error,
isErrorString = message.addingFailed,
)
val learnMoreAnnotatedString = message.messageContent.learnMoreResId?.let {
val learnMoreLink = stringResource(id = message.messageContent.learnMoreResId)
val learnMoreText = stringResource(id = R.string.label_learn_more)
buildAnnotatedString {
append(learnMoreText)
addStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline
),
start = 0,
end = learnMoreText.length
)
addStringAnnotation(tag = TAG_LEARN_MORE, annotation = learnMoreLink, start = 0, end = learnMoreText.length)
}
}
val fullAnnotatedString = when {
learnMoreAnnotatedString == null -> annotatedString
message.messageContent.expandable && expanded -> annotatedString + AnnotatedString("\n") + learnMoreAnnotatedString
message.messageContent.expandable && !expanded -> annotatedString
else -> annotatedString + AnnotatedString(" ") + learnMoreAnnotatedString
}
val learnMoreLink = message.messageContent.learnMoreResId?.let { stringResource(id = it) }

ClickableText(
modifier = Modifier.defaultMinSize(minHeight = dimensions().spacing20x),
text = fullAnnotatedString,
onClick = { offset ->
fullAnnotatedString.getStringAnnotations(TAG_LEARN_MORE, offset, offset)
.firstOrNull()?.let { result -> CustomTabsHelper.launchUrl(context, result.item) }
},
style = MaterialTheme.wireTypography.body02,
TextWithLinkSuffix(
text = annotatedString,
linkText = learnMoreLink?.let { stringResource(id = R.string.label_learn_more) },
textColor = MaterialTheme.wireColorScheme.secondaryText,
linkColor = MaterialTheme.wireColorScheme.onBackground,
onLinkClick = { learnMoreLink?.let { CustomTabsHelper.launchUrl(context, it) } },
onTextLayout = {
centerOfFirstLine = if (it.lineCount == 0) 0f else ((it.getLineTop(0) + it.getLineBottom(0)) / 2)
}
},
modifier = Modifier.defaultMinSize(minHeight = dimensions().spacing20x),
)

if (message.messageContent.expandable) {
Expand Down Expand Up @@ -371,4 +346,3 @@ private fun SystemMessage.MemberFailedToAdd.toFailedToAddMarkdownText(

private const val EXPANDABLE_THRESHOLD = 4
private const val SINGLE_EXPANDABLE_THRESHOLD = 1
private const val TAG_LEARN_MORE = "tag_learn_more"
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,13 @@ package com.wire.android.ui.home.newconversation.common

import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import com.wire.android.R
import com.wire.android.ui.common.DialogTextSuffixLink
import com.wire.android.ui.common.WireDialog
import com.wire.android.ui.common.WireDialogButtonProperties
import com.wire.android.ui.common.WireDialogButtonType
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.markdown.MarkdownConstants
import com.wire.android.ui.theme.WireTheme
import com.wire.android.util.DialogAnnotatedErrorStrings
import com.wire.android.util.DialogErrorStrings
import com.wire.android.util.ui.PreviewMultipleThemes

@Composable
Expand All @@ -40,51 +35,34 @@ fun CreateGroupErrorDialog(
onAccept: () -> Unit,
onCancel: () -> Unit
) {
val dialogStrings = when (error) {
is CreateGroupState.Error.LackingConnection -> DialogAnnotatedErrorStrings(
stringResource(R.string.error_no_network_title),
buildAnnotatedString { append(stringResource(R.string.error_no_network_message)) }
)
val (dialogStrings, dialogSuffixLink) = when (error) {
is CreateGroupState.Error.LackingConnection -> DialogErrorStrings(
title = stringResource(R.string.error_no_network_title),
message = stringResource(R.string.error_no_network_message),
) to null

is CreateGroupState.Error.Unknown -> DialogAnnotatedErrorStrings(
stringResource(R.string.error_unknown_title),
buildAnnotatedString { append(stringResource(R.string.error_unknown_message)) }
)
is CreateGroupState.Error.Unknown -> DialogErrorStrings(
title = stringResource(R.string.error_unknown_title),
message = stringResource(R.string.error_unknown_message),
) to null

is CreateGroupState.Error.ConflictedBackends -> DialogAnnotatedErrorStrings(
is CreateGroupState.Error.ConflictedBackends -> DialogErrorStrings(
title = stringResource(id = R.string.group_can_not_be_created_title),
annotatedMessage = buildAnnotatedString {
val description = stringResource(
message = stringResource(
id = R.string.group_can_not_be_created_federation_conflict_description,
error.domains.dropLast(1).joinToString(", "),
error.domains.last()
)
val learnMore = stringResource(id = R.string.label_learn_more)

append(description)
append(' ')

withStyle(
style = SpanStyle(
color = colorsScheme().primary,
textDecoration = TextDecoration.Underline
)
) {
append(learnMore)
}
addStringAnnotation(
tag = MarkdownConstants.TAG_URL,
annotation = stringResource(id = R.string.url_message_details_offline_backends_learn_more),
start = description.length + 1,
end = description.length + 1 + learnMore.length
)
}
),
) to DialogTextSuffixLink(
linkText = stringResource(id = R.string.label_learn_more),
linkUrl = stringResource(id = R.string.url_message_details_offline_backends_learn_more)
)
}

WireDialog(
dialogStrings.title,
dialogStrings.annotatedMessage,
title = dialogStrings.title,
text = dialogStrings.annotatedMessage,
textSuffixLink = dialogSuffixLink,
onDismiss = onDismiss,
buttonsHorizontalAlignment = false,
optionButton1Properties = WireDialogButtonProperties(
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/kotlin/com/wire/android/util/CoreFailureUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ fun CoreFailure.uiText(): UIText = when (this) {
else -> UIText.StringResource(R.string.error_unknown_message)
}

data class DialogErrorStrings(val title: String, val message: String)
data class DialogAnnotatedErrorStrings(val title: String, val annotatedMessage: AnnotatedString)
data class DialogErrorStrings(val title: String, val annotatedMessage: AnnotatedString) {
constructor(title: String, message: String) : this(title, AnnotatedString(message))
}
Loading

0 comments on commit 0acad2f

Please sign in to comment.