mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-28 14:19:47 +00:00
ui: use markdown when showing profile bio and group purpose (#6126)
* ios: use markdown when showing profile bio and group purpose * android, desktop: use markdown when showing profile bio and group purpose
This commit is contained in:
@@ -111,6 +111,7 @@ struct ChatInfoView: View {
|
||||
@State private var sendReceiptsUserDefault = true
|
||||
@State private var progressIndicator = false
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State private var showSecrets: Set<Int> = []
|
||||
|
||||
enum ChatInfoViewAlert: Identifiable {
|
||||
case clearChatAlert
|
||||
@@ -397,10 +398,11 @@ struct ChatInfoView: View {
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" {
|
||||
Text(descr)
|
||||
.font(.subheadline)
|
||||
let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background)
|
||||
msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(4)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
@@ -120,13 +120,14 @@ struct MsgContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func msgTextResultView(_ r: MsgTextResult, _ t: Text, showSecrets: Binding<Set<Int>>? = nil) -> some View {
|
||||
func msgTextResultView(_ r: MsgTextResult, _ t: Text, showSecrets: Binding<Set<Int>>? = nil, centered: Bool = false, smallFont: Bool = false) -> some View {
|
||||
t.if(r.hasSecrets, transform: hiddenSecretsView)
|
||||
.if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets)) }
|
||||
.if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets, centered: centered, smallFont: smallFont)) }
|
||||
}
|
||||
|
||||
// smallFont parameter is used to pad height, otherwise CTFrameGetLines fails to see them as lines - it's needed if font is not .body
|
||||
@inline(__always)
|
||||
private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding<Set<Int>>? = nil) -> some View {
|
||||
private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding<Set<Int>>? = nil, centered: Bool, smallFont: Bool) -> some View {
|
||||
return GeometryReader { g in
|
||||
Rectangle()
|
||||
.fill(Color.clear)
|
||||
@@ -135,17 +136,29 @@ private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding<Set<In
|
||||
let t = event.translation
|
||||
if t.width * t.width + t.height * t.height > 100 { return }
|
||||
let framesetter = CTFramesetterCreateWithAttributedString(s as CFAttributedString)
|
||||
let path = CGPath(rect: CGRect(origin: .zero, size: g.size), transform: nil)
|
||||
let paddedSize = smallFont ? CGSize(width: g.size.width, height: g.size.height + 1.0) : g.size
|
||||
let path = CGPath(rect: CGRect(origin: .zero, size: paddedSize), transform: nil)
|
||||
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, s.length), path, nil)
|
||||
let point = CGPoint(x: event.location.x, y: g.size.height - event.location.y) // Flip y for UIKit
|
||||
var index: CFIndex?
|
||||
if let lines = CTFrameGetLines(frame) as? [CTLine] {
|
||||
var origins = [CGPoint](repeating: .zero, count: lines.count)
|
||||
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
|
||||
var maxWidth: CGFloat = 0
|
||||
if centered {
|
||||
for line in lines {
|
||||
let bounds = CTLineGetBoundsWithOptions(line, .useOpticalBounds)
|
||||
if bounds.width > maxWidth {
|
||||
maxWidth = bounds.width
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0 ..< lines.count {
|
||||
let bounds = CTLineGetBoundsWithOptions(lines[i], .useOpticalBounds)
|
||||
if bounds.offsetBy(dx: origins[i].x, dy: origins[i].y).contains(point) {
|
||||
index = CTLineGetStringIndexForPosition(lines[i], point)
|
||||
let offsetX = centered ? (maxWidth - bounds.width) / 2 : 0
|
||||
if bounds.offsetBy(dx: origins[i].x + offsetX, dy: origins[i].y).contains(point) {
|
||||
let relativePoint = centered ? CGPoint(x: point.x - origins[i].x - offsetX, y: point.y - origins[i].y) : point
|
||||
index = CTLineGetStringIndexForPosition(lines[i], relativePoint)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -207,6 +220,31 @@ private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret")
|
||||
|
||||
typealias MsgTextResult = (string: NSMutableAttributedString, hasSecrets: Bool, handleTaps: Bool)
|
||||
|
||||
@inline(__always)
|
||||
func markdownText(
|
||||
_ s: String,
|
||||
textStyle: UIFont.TextStyle = .body,
|
||||
sender: String? = nil,
|
||||
preview: Bool = false,
|
||||
mentions: [String: CIMention]? = nil,
|
||||
userMemberId: String? = nil,
|
||||
showSecrets: Set<Int>? = nil,
|
||||
backgroundColor: Color
|
||||
) -> MsgTextResult {
|
||||
messageText(
|
||||
s,
|
||||
parseSimpleXMarkdown(s),
|
||||
textStyle: textStyle,
|
||||
sender: sender,
|
||||
preview: preview,
|
||||
mentions: mentions,
|
||||
userMemberId: userMemberId,
|
||||
showSecrets: showSecrets,
|
||||
backgroundColor: UIColor(backgroundColor)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func messageText(
|
||||
_ text: String,
|
||||
_ formattedText: [FormattedText]?,
|
||||
|
||||
@@ -230,7 +230,7 @@ struct ChatItemInfoView: View {
|
||||
private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View {
|
||||
let backgroundColor = chatItemFrameColor(ci, theme)
|
||||
return VStack(alignment: .leading, spacing: 4) {
|
||||
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil, backgroundColor: UIColor(backgroundColor))
|
||||
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil, backgroundColor: backgroundColor)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(backgroundColor)
|
||||
@@ -258,7 +258,7 @@ struct ChatItemInfoView: View {
|
||||
.frame(maxWidth: maxWidth, alignment: .leading)
|
||||
}
|
||||
|
||||
@ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil, backgroundColor: UIColor) -> some View {
|
||||
@ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil, backgroundColor: Color) -> some View {
|
||||
if text != "" {
|
||||
TextBubble(text: text, formattedText: formattedText, sender: sender, mentions: ci.mentions, userMemberId: userMemberId, backgroundColor: backgroundColor)
|
||||
} else {
|
||||
@@ -275,11 +275,11 @@ struct ChatItemInfoView: View {
|
||||
var sender: String? = nil
|
||||
var mentions: [String: CIMention]?
|
||||
var userMemberId: String?
|
||||
var backgroundColor: UIColor
|
||||
var backgroundColor: Color
|
||||
@State private var showSecrets: Set<Int> = []
|
||||
|
||||
var body: some View {
|
||||
let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: backgroundColor)
|
||||
let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: UIColor(backgroundColor))
|
||||
return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets)
|
||||
}
|
||||
}
|
||||
@@ -305,7 +305,7 @@ struct ChatItemInfoView: View {
|
||||
private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View {
|
||||
let backgroundColor = quotedMsgFrameColor(qi, theme)
|
||||
return VStack(alignment: .leading, spacing: 4) {
|
||||
textBubble(qi.text, qi.formattedText, qi.getSender(nil), backgroundColor: UIColor(backgroundColor))
|
||||
textBubble(qi.text, qi.formattedText, qi.getSender(nil), backgroundColor: backgroundColor)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(quotedMsgFrameColor(qi, theme))
|
||||
|
||||
@@ -403,7 +403,7 @@ struct ChatView: View {
|
||||
private func connectInProgressView(_ s: String) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
|
||||
|
||||
HStack(spacing: 12) {
|
||||
ProgressView()
|
||||
Text(s)
|
||||
@@ -823,6 +823,7 @@ struct ChatView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness
|
||||
@Binding @ObservedObject var chat: Chat
|
||||
@State private var showSecrets: Set<Int> = []
|
||||
|
||||
var body: some View {
|
||||
let v = VStack(spacing: 8) {
|
||||
@@ -846,8 +847,8 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
if let shortDescr = chat.chatInfo.shortDescr {
|
||||
Text(shortDescr)
|
||||
.font(.subheadline)
|
||||
let r = markdownText(shortDescr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background)
|
||||
msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(4)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -34,6 +34,7 @@ struct GroupChatInfoView: View {
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State private var searchText: String = ""
|
||||
@FocusState private var searchFocussed
|
||||
@State private var showSecrets: Set<Int> = []
|
||||
|
||||
enum GroupChatInfoViewAlert: Identifiable {
|
||||
case deleteGroupAlert
|
||||
@@ -253,10 +254,11 @@ struct GroupChatInfoView: View {
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" {
|
||||
Text(descr)
|
||||
.font(.subheadline)
|
||||
let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background)
|
||||
msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(4)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
@@ -59,7 +59,7 @@ struct GroupWelcomeView: View {
|
||||
}
|
||||
|
||||
private func textPreview() -> some View {
|
||||
let r = messageText(welcomeText, parseSimpleXMarkdown(welcomeText), sender: nil, mentions: nil, userMemberId: nil, showSecrets: showSecrets, backgroundColor: UIColor(theme.colors.background))
|
||||
let r = markdownText(welcomeText, showSecrets: showSecrets, backgroundColor: theme.colors.background)
|
||||
return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets)
|
||||
.frame(minHeight: 130, alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
@@ -274,7 +274,7 @@ struct ChatPreviewView: View {
|
||||
|
||||
private func messageDraft(_ draft: ComposeState) -> (Text, Bool) {
|
||||
let msg = draft.message
|
||||
let r = messageText(msg, parseSimpleXMarkdown(msg), sender: nil, preview: true, mentions: draft.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(theme.colors.background))
|
||||
let r = markdownText(msg, preview: true, mentions: draft.mentions, backgroundColor: theme.colors.background)
|
||||
return (image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary)
|
||||
+ attachment()
|
||||
+ Text(AttributedString(r.string)),
|
||||
|
||||
@@ -4320,7 +4320,6 @@ sealed class MsgChatLink {
|
||||
|
||||
@Serializable
|
||||
class FormattedText(val text: String, val format: Format? = null) {
|
||||
// TODO make it dependent on simplexLinkMode preference
|
||||
fun link(mode: SimplexLinkMode): String? = when (format) {
|
||||
is Format.Uri -> if (text.startsWith("http://", ignoreCase = true) || text.startsWith("https://", ignoreCase = true)) text else "https://$text"
|
||||
is Format.SimplexLink -> if (mode == SimplexLinkMode.BROWSER) text else format.simplexUri
|
||||
@@ -4329,7 +4328,6 @@ class FormattedText(val text: String, val format: Format? = null) {
|
||||
else -> null
|
||||
}
|
||||
|
||||
// TODO make it dependent on simplexLinkMode preference
|
||||
fun viewText(mode: SimplexLinkMode): String =
|
||||
if (format is Format.SimplexLink && mode == SimplexLinkMode.DESCRIPTION) simplexLinkText(format.linkType, format.smpHosts) else text
|
||||
|
||||
|
||||
+8
-7
@@ -41,6 +41,7 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.group.ChatTTLOption
|
||||
import chat.simplex.common.views.chat.item.MarkdownText
|
||||
import chat.simplex.common.views.chatlist.updateChatSettings
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -759,16 +760,16 @@ fun ChatInfoDescription(c: NamedChat, displayName: String, copyNameToClipboard:
|
||||
}
|
||||
val descr = c.shortDescr?.trim()
|
||||
if (descr != null && descr != "") {
|
||||
val copyDescr = { copyNameToClipboard(descr) }
|
||||
Text(
|
||||
MarkdownText(
|
||||
descr,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
parseToMarkdown(descr),
|
||||
toggleSecrets = true,
|
||||
style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center),
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
lineHeight = 21.sp,
|
||||
modifier = Modifier.padding(top = DEFAULT_PADDING_HALF).combinedClickable(onClick = copyDescr, onLongClick = copyDescr).onRightClick(copyDescr)
|
||||
uriHandler = LocalUriHandler.current,
|
||||
modifier = Modifier.padding(top = DEFAULT_PADDING_HALF),
|
||||
linkMode = chatModel.simplexLinkMode.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -1853,16 +1853,16 @@ fun BoxScope.ChatItemsList(
|
||||
|
||||
val descr = chatInfo.shortDescr?.trim()
|
||||
if (descr != null && descr != "") {
|
||||
Text(
|
||||
MarkdownText(
|
||||
descr,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
parseToMarkdown(descr),
|
||||
toggleSecrets = true,
|
||||
style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center),
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
lineHeight = 21.sp,
|
||||
modifier = Modifier
|
||||
.padding(top = DEFAULT_PADDING_HALF)
|
||||
uriHandler = LocalUriHandler.current,
|
||||
modifier = Modifier.padding(top = DEFAULT_PADDING_HALF),
|
||||
linkMode = linkMode
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1871,6 +1871,7 @@ fun BoxScope.ChatItemsList(
|
||||
Text(
|
||||
contextStr,
|
||||
style = MaterialTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.padding(top = DEFAULT_PADDING)
|
||||
)
|
||||
|
||||
+1
@@ -1248,6 +1248,7 @@ fun ComposeView(
|
||||
SimpleButtonIconEnded(
|
||||
text = stringResource(MR.strings.compose_view_connect),
|
||||
icon = painterResource(icon),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
disabled = composeState.value.inProgress,
|
||||
click = { withApi { sendRequest() } }
|
||||
|
||||
Reference in New Issue
Block a user