ios: only handle taps on messages with links or secrets, use image for secret markdown (#5885)

* ios: use image for secret markdown

* remove unnecessary ViewBuilders
This commit is contained in:
Evgeny 2025-05-11 14:15:14 +01:00 committed by GitHub
parent d338696035
commit 8d54acef92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 352 additions and 264 deletions

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "vertical_logo_x1.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "vertical_logo_x2.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "vertical_logo_x3.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -74,7 +74,7 @@ struct ContentView: View {
}
}
@ViewBuilder func allViews() -> some View {
func allViews() -> some View {
ZStack {
let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
@ -209,7 +209,7 @@ struct ContentView: View {
}
}
@ViewBuilder private func activeCallInteractiveArea(_ call: Call) -> some View {
private func activeCallInteractiveArea(_ call: Call) -> some View {
HStack {
Text(call.contact.displayName).font(.body).foregroundColor(.white)
Spacer()

View file

@ -467,7 +467,7 @@ struct ActiveCallOverlay: View {
.disabled(call.initialCallType == .audio && client.activeCall?.peerHasOldVersion == true)
}
@ViewBuilder private func flipCameraButton() -> some View {
private func flipCameraButton() -> some View {
controlButton(call, "arrow.triangle.2.circlepath", padding: 12) {
Task {
if await WebRTCClient.isAuthorized(for: .video) {
@ -477,11 +477,11 @@ struct ActiveCallOverlay: View {
}
}
@ViewBuilder private func controlButton(_ call: Call, _ imageName: String, padding: CGFloat, _ perform: @escaping () -> Void) -> some View {
private func controlButton(_ call: Call, _ imageName: String, padding: CGFloat, _ perform: @escaping () -> Void) -> some View {
callButton(imageName, call.peerMediaSources.hasVideo ? Color.black.opacity(0.2) : Color.white.opacity(0.2), padding: padding, perform)
}
@ViewBuilder private func audioDevicePickerButton() -> some View {
private func audioDevicePickerButton() -> some View {
AudioDevicePicker()
.opacity(0.8)
.scaleEffect(2)

View file

@ -50,7 +50,7 @@ struct CICallItemView: View {
Image(systemName: "phone.connection").foregroundColor(.green)
}
@ViewBuilder private func endedCallIcon(_ sent: Bool) -> some View {
private func endedCallIcon(_ sent: Bool) -> some View {
HStack {
Image(systemName: "phone.down")
Text(durationText(duration)).foregroundColor(theme.colors.secondary)

View file

@ -46,7 +46,7 @@ struct CILinkView: View {
func openBrowserAlert(uri: URL) {
showAlert(
NSLocalizedString("Open in browser?", comment: "alert title"),
NSLocalizedString("Open link?", comment: "alert title"),
message: uri.absoluteString,
actions: {[
UIAlertAction(

View file

@ -68,7 +68,7 @@ struct CIRcvDecryptionError: View {
}
}
@ViewBuilder private func viewBody() -> some View {
private func viewBody() -> some View {
Group {
if case let .direct(contact) = chat.chatInfo,
let contactStats = contact.activeConn?.connectionStats {

View file

@ -201,6 +201,7 @@ struct FramedItemView: View {
}
@ViewBuilder private func ciQuoteView(_ qi: CIQuote) -> some View {
let backgroundColor = chatItemFrameContextColor(chatItem, theme)
let v = ZStack(alignment: .topTrailing) {
switch (qi.content) {
case let .image(_, image):
@ -242,7 +243,8 @@ struct FramedItemView: View {
// if enable this always, size of the framed voice message item will be incorrect after end of playback
.overlay { if case .voice = chatItem.content.msgContent {} else { DetermineWidth() } }
.frame(minWidth: msgWidth, alignment: .leading)
.background(chatItemFrameContextColor(chatItem, theme))
.background(backgroundColor)
.environment(\.containerBackground, UIColor(backgroundColor))
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
v.frame(maxWidth: mediaWidth, alignment: .leading)
} else {
@ -308,6 +310,7 @@ struct FramedItemView: View {
rightToLeft: rtl,
prefix: txtPrefix
)
.environment(\.containerBackground, UIColor(chatItemFrameColor(ci, theme)))
.multilineTextAlignment(rtl ? .trailing : .leading)
.padding(.vertical, 6)
.padding(.horizontal, 12)

View file

@ -26,6 +26,7 @@ private func typing(_ theme: AppTheme, _ descr: UIFontDescriptor, _ ws: [UIFont.
struct MsgContentView: View {
@ObservedObject var chat: Chat
@Environment(\.showTimestamp) var showTimestamp: Bool
@Environment(\.containerBackground) var containerBackground: UIColor
@EnvironmentObject var theme: AppTheme
var text: String
var formattedText: [FormattedText]? = nil
@ -92,7 +93,8 @@ struct MsgContentView: View {
@inline(__always)
private func msgContentView() -> some View {
let s = messageText(text, formattedText, textStyle: textStyle, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, secondaryColor: theme.colors.secondary, prefix: prefix)
let r = messageText(text, formattedText, textStyle: textStyle, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: containerBackground, prefix: prefix)
let s = r.string
let t: Text
if let mt = meta {
if mt.isLive {
@ -102,7 +104,7 @@ struct MsgContentView: View {
} else {
t = Text(AttributedString(s))
}
return t.overlay(handleTextLinks(s, showSecrets: $showSecrets))
return msgTextResultView(r, t, showSecrets: $showSecrets)
}
@inline(__always)
@ -118,7 +120,13 @@ struct MsgContentView: View {
}
}
func handleTextLinks(_ s: NSAttributedString, showSecrets: Binding<Set<Int>>? = nil) -> some View {
func msgTextResultView(_ r: MsgTextResult, _ t: Text, showSecrets: Binding<Set<Int>>? = nil) -> some View {
t.if(r.hasSecrets, transform: hiddenSecretsView)
.if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets)) }
}
@inline(__always)
private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding<Set<Int>>? = nil) -> some View {
return GeometryReader { g in
Rectangle()
.fill(Color.clear)
@ -174,13 +182,43 @@ func handleTextLinks(_ s: NSAttributedString, showSecrets: Binding<Set<Int>>? =
}
}
func hiddenSecretsView<V: View>(_ v: V) -> some View {
v.overlay(
GeometryReader { g in
let size = (g.size.width + g.size.height) / 1.4142
Image("vertical_logo")
.resizable(resizingMode: .tile)
.frame(width: size, height: size)
.rotationEffect(.degrees(45), anchor: .center)
.position(x: g.size.width / 2, y: g.size.height / 2)
.clipped()
.saturation(0.65)
.opacity(0.35)
}
.mask(v)
)
}
private let linkAttrKey = NSAttributedString.Key("chat.simplex.app.link")
private let webLinkAttrKey = NSAttributedString.Key("chat.simplex.app.webLink")
private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret")
func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: UIFont.TextStyle = .body, sender: String?, preview: Bool = false, mentions: [String: CIMention]?, userMemberId: String?, showSecrets: Set<Int>?, secondaryColor: Color, prefix: NSAttributedString? = nil) -> NSMutableAttributedString {
typealias MsgTextResult = (string: NSMutableAttributedString, hasSecrets: Bool, handleTaps: Bool)
func messageText(
_ text: String,
_ formattedText: [FormattedText]?,
textStyle: UIFont.TextStyle = .body,
sender: String?,
preview: Bool = false,
mentions: [String: CIMention]?,
userMemberId: String?,
showSecrets: Set<Int>?,
backgroundColor: UIColor,
prefix: NSAttributedString? = nil
) -> MsgTextResult {
let res = NSMutableAttributedString()
let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
let font = UIFont.preferredFont(forTextStyle: textStyle)
@ -188,7 +226,10 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: U
.font: font,
.foregroundColor: UIColor.label
]
let secretColor = backgroundColor.withAlphaComponent(1)
var link: [NSAttributedString.Key: Any]?
var hasSecrets = false
var handleTaps = false
if let sender {
if preview {
@ -230,14 +271,16 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: U
if let showSecrets {
if !showSecrets.contains(secretIdx) {
attrs[.foregroundColor] = UIColor.clear
attrs[.backgroundColor] = UIColor.secondarySystemFill // secretColor
attrs[.backgroundColor] = secretColor
}
attrs[secretAttrKey] = secretIdx
secretIdx += 1
handleTaps = true
} else {
attrs[.foregroundColor] = UIColor.clear
attrs[.backgroundColor] = UIColor.secondarySystemFill
attrs[.backgroundColor] = secretColor
}
hasSecrets = true
case let .colored(color):
if let c = color.uiColor {
attrs[.foregroundColor] = UIColor(c)
@ -247,11 +290,13 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: U
if !preview {
attrs[linkAttrKey] = NSURL(string: ft.text)
attrs[webLinkAttrKey] = true
handleTaps = true
}
case let .simplexLink(linkType, simplexUri, smpHosts):
attrs = linkAttrs()
if !preview {
attrs[linkAttrKey] = NSURL(string: simplexUri)
handleTaps = true
}
if case .description = privacySimplexLinkModeDefault.get() {
t = simplexLinkText(linkType, smpHosts)
@ -278,11 +323,13 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: U
attrs = linkAttrs()
if !preview {
attrs[linkAttrKey] = NSURL(string: "mailto:" + ft.text)
handleTaps = true
}
case .phone:
attrs = linkAttrs()
if !preview {
attrs[linkAttrKey] = NSURL(string: "tel:" + t.replacingOccurrences(of: " ", with: ""))
handleTaps = true
}
case .none: ()
}
@ -292,7 +339,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: U
res.append(NSMutableAttributedString(string: text, attributes: plain))
}
return res
return (string: res, hasSecrets: hasSecrets, handleTaps: handleTaps)
func linkAttrs() -> [NSAttributedString.Key: Any] {
link = link ?? [

View file

@ -41,7 +41,7 @@ struct ChatItemForwardingView: View {
.alert(item: $alert) { $0.alert }
}
@ViewBuilder private func forwardListView() -> some View {
private func forwardListView() -> some View {
VStack(alignment: .leading) {
if !chatsToForwardTo.isEmpty {
List {

View file

@ -131,9 +131,9 @@ struct ChatItemInfoView: View {
}
}
@ViewBuilder private func details() -> some View {
private func details() -> some View {
let meta = ci.meta
VStack(alignment: .leading, spacing: 16) {
return VStack(alignment: .leading, spacing: 16) {
Text(title)
.font(.largeTitle)
.bold()
@ -197,7 +197,7 @@ struct ChatItemInfoView: View {
}
}
@ViewBuilder private func historyTab() -> some View {
private func historyTab() -> some View {
GeometryReader { g in
let maxWidth = (g.size.width - 32) * 0.84
ScrollView {
@ -227,12 +227,13 @@ struct ChatItemInfoView: View {
}
}
@ViewBuilder private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View {
VStack(alignment: .leading, spacing: 4) {
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
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))
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(ci, theme))
.background(backgroundColor)
.modifier(ChatItemClipped())
.contextMenu {
if itemVersion.msgContent.text != "" {
@ -257,9 +258,9 @@ struct ChatItemInfoView: View {
.frame(maxWidth: maxWidth, alignment: .leading)
}
@ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil) -> some View {
@ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil, backgroundColor: UIColor) -> some View {
if text != "" {
TextBubble(text: text, formattedText: formattedText, sender: sender, mentions: ci.mentions, userMemberId: userMemberId)
TextBubble(text: text, formattedText: formattedText, sender: sender, mentions: ci.mentions, userMemberId: userMemberId, backgroundColor: backgroundColor)
} else {
Text("no text")
.italic()
@ -274,15 +275,16 @@ struct ChatItemInfoView: View {
var sender: String? = nil
var mentions: [String: CIMention]?
var userMemberId: String?
var backgroundColor: UIColor
@State private var showSecrets: Set<Int> = []
var body: some View {
let s = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, secondaryColor: theme.colors.secondary)
Text(AttributedString(s)).overlay(handleTextLinks(s, showSecrets: $showSecrets))
let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: backgroundColor)
return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets)
}
}
@ViewBuilder private func quoteTab(_ qi: CIQuote) -> some View {
private func quoteTab(_ qi: CIQuote) -> some View {
GeometryReader { g in
let maxWidth = (g.size.width - 32) * 0.84
ScrollView {
@ -300,9 +302,10 @@ struct ChatItemInfoView: View {
}
}
@ViewBuilder private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View {
VStack(alignment: .leading, spacing: 4) {
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
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))
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(quotedMsgFrameColor(qi, theme))
@ -335,7 +338,7 @@ struct ChatItemInfoView: View {
: theme.appColors.receivedMessage
}
@ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
details()
@ -373,7 +376,7 @@ struct ChatItemInfoView: View {
}
}
@ViewBuilder private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View {
private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View {
HStack {
ChatInfoImage(chat: Chat(chatInfo: forwardedFromItem.chatInfo), size: 48)
.padding(.trailing, 6)
@ -404,7 +407,7 @@ struct ChatItemInfoView: View {
}
}
@ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
details()
@ -419,7 +422,7 @@ struct ChatItemInfoView: View {
.frame(maxHeight: .infinity, alignment: .top)
}
@ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
LazyVStack(alignment: .leading, spacing: 12) {
let mss = membersStatuses(memberDeliveryStatuses)
if !mss.isEmpty {

View file

@ -18,6 +18,10 @@ extension EnvironmentValues {
static let defaultValue: Bool = true
}
struct ContainerBackground: EnvironmentKey {
static let defaultValue: UIColor = .clear
}
var showTimestamp: Bool {
get { self[ShowTimestamp.self] }
set { self[ShowTimestamp.self] = newValue }
@ -27,6 +31,11 @@ extension EnvironmentValues {
get { self[Revealed.self] }
set { self[Revealed.self] = newValue }
}
var containerBackground: UIColor {
get { self[ContainerBackground.self] }
set { self[ContainerBackground.self] = newValue }
}
}
struct ChatItemView: View {

View file

@ -71,10 +71,9 @@ struct ChatView: View {
}
}
@ViewBuilder
private var viewBody: some View {
let cInfo = chat.chatInfo
ZStack {
return ZStack {
let wallpaperImage = theme.wallpaper.type.image
let wallpaperType = theme.wallpaper.type
let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background)
@ -1528,9 +1527,9 @@ struct ChatView: View {
}
}
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View {
func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View {
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
VStack(alignment: alignment.horizontal, spacing: 3) {
return VStack(alignment: alignment.horizontal, spacing: 3) {
HStack {
if ci.chatDir.sent {
goToItemButton(true)

View file

@ -70,8 +70,10 @@ struct ContextItemView: View {
.lineLimit(lines)
}
private func contextMsgPreview(_ contextItem: ChatItem) -> Text {
return attachment() + Text(AttributedString(messageText(contextItem.text, contextItem.formattedText, sender: nil, preview: true, mentions: contextItem.mentions, userMemberId: nil, showSecrets: nil, secondaryColor: theme.colors.secondary)))
private func contextMsgPreview(_ contextItem: ChatItem) -> some View {
let r = messageText(contextItem.text, contextItem.formattedText, sender: nil, preview: true, mentions: contextItem.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(background))
let t = attachment() + Text(AttributedString(r.string))
return t.if(r.hasSecrets, transform: hiddenSecretsView)
func attachment() -> Text {
let isFileLoaded = if let fileSource = getLoadedFileSource(contextItem.file) {

View file

@ -145,9 +145,9 @@ struct AddGroupMembersViewCommon: View {
return dummy
}()
@ViewBuilder private func inviteMembersButton() -> some View {
private func inviteMembersButton() -> some View {
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Invite to group" : "Invite to chat"
Button {
return Button {
inviteMembers()
} label: {
HStack {

View file

@ -292,9 +292,9 @@ struct GroupChatInfoView: View {
.disabled(!groupInfo.ready || chat.chatItems.isEmpty)
}
@ViewBuilder private func addMembersActionButton(width: CGFloat) -> some View {
if chat.chatInfo.incognito {
private func addMembersActionButton(width: CGFloat) -> some View {
ZStack {
if chat.chatInfo.incognito {
InfoViewButton(image: "link.badge.plus", title: "invite", width: width) {
groupLinkNavLinkActive = true
}
@ -306,10 +306,7 @@ struct GroupChatInfoView: View {
}
.frame(width: 1, height: 1)
.hidden()
}
.disabled(!groupInfo.ready)
} else {
ZStack {
InfoViewButton(image: "person.fill.badge.plus", title: "invite", width: width) {
addMembersNavLinkActive = true
}
@ -322,8 +319,8 @@ struct GroupChatInfoView: View {
.frame(width: 1, height: 1)
.hidden()
}
.disabled(!groupInfo.ready)
}
.disabled(!groupInfo.ready)
}
private func muteButton(width: CGFloat, nextNtfMode: MsgFilter) -> some View {
@ -569,9 +566,9 @@ struct GroupChatInfoView: View {
}
}
@ViewBuilder private func leaveGroupButton() -> some View {
private func leaveGroupButton() -> some View {
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group" : "Leave chat"
Button(role: .destructive) {
return Button(role: .destructive) {
alert = .leaveGroupAlert
} label: {
Label(label, systemImage: "rectangle.portrait.and.arrow.right")

View file

@ -18,6 +18,7 @@ struct GroupWelcomeView: View {
@State private var editMode = true
@FocusState private var keyboardVisible: Bool
@State private var showSaveDialog = false
@State private var showSecrets: Set<Int> = []
let maxByteCount = 1200
@ -58,9 +59,8 @@ struct GroupWelcomeView: View {
}
private func textPreview() -> some View {
let s = messageText(welcomeText, parseSimpleXMarkdown(welcomeText), sender: nil, mentions: nil, userMemberId: nil, showSecrets: nil, secondaryColor: theme.colors.secondary)
return Text(AttributedString(s))
.overlay(handleTextLinks(s))
let r = messageText(welcomeText, parseSimpleXMarkdown(welcomeText), sender: nil, mentions: nil, userMemberId: nil, showSecrets: showSecrets, backgroundColor: UIColor(theme.colors.background))
return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets)
.frame(minHeight: 130, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
}

View file

@ -90,7 +90,7 @@ struct ChatListNavLink: View {
.actionSheet(item: $actionSheet) { $0.actionSheet }
}
@ViewBuilder private func contactNavLink(_ contact: Contact) -> some View {
private func contactNavLink(_ contact: Contact) -> some View {
Group {
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
@ -243,7 +243,7 @@ struct ChatListNavLink: View {
}
}
@ViewBuilder private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View {
private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View {
NavLinkPlain(
chatId: chat.chatInfo.id,
selection: $chatModel.chatId,

View file

@ -335,9 +335,9 @@ struct ChatListView: View {
}
}
@ViewBuilder private var chatList: some View {
private var chatList: some View {
let cs = filteredChats()
ZStack {
return ZStack {
ScrollViewReader { scrollProxy in
List {
if !chatModel.chats.isEmpty {
@ -804,7 +804,7 @@ struct TagsView: View {
}
}
@ViewBuilder private func expandedPresetTagsFiltersView() -> some View {
private func expandedPresetTagsFiltersView() -> some View {
ForEach(PresetTag.allCases, id: \.id) { tag in
if (chatTagsModel.presetTags[tag] ?? 0) > 0 {
expandedTagFilterView(tag)

View file

@ -187,13 +187,14 @@ struct ChatPreviewView: View {
.kerning(-2)
}
private func chatPreviewLayout(_ text: Text?, draft: Bool = false, _ hasFilePreview: Bool = false) -> some View {
private func chatPreviewLayout(_ text: Text?, draft: Bool = false, hasFilePreview: Bool = false, hasSecrets: Bool) -> some View {
ZStack(alignment: .topTrailing) {
let s = chat.chatStats
let mentionWidth: CGFloat = if s.unreadMentions > 0 && s.unreadCount > 1 { dynamicSize(userFont).unreadCorner } else { 0 }
let t = text
.lineLimit(userFont <= .xxxLarge ? 2 : 1)
.multilineTextAlignment(.leading)
.if(hasSecrets, transform: hiddenSecretsView)
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.leading, hasFilePreview ? 0 : 8)
.padding(.trailing, mentionWidth + (hasFilePreview ? 38 : 36))
@ -259,11 +260,13 @@ struct ChatPreviewView: View {
}
}
private func messageDraft(_ draft: ComposeState) -> Text {
private func messageDraft(_ draft: ComposeState) -> (Text, Bool) {
let msg = draft.message
return image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary)
let r = messageText(msg, parseSimpleXMarkdown(msg), sender: nil, preview: true, mentions: draft.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(theme.colors.background))
return (image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary)
+ attachment()
+ Text(AttributedString(messageText(msg, parseSimpleXMarkdown(msg), sender: nil, preview: true, mentions: draft.mentions, userMemberId: nil, showSecrets: nil, secondaryColor: theme.colors.secondary)))
+ Text(AttributedString(r.string)),
r.hasSecrets)
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
Text(Image(systemName: s)).foregroundColor(color) + textSpace
@ -279,10 +282,11 @@ struct ChatPreviewView: View {
}
}
func chatItemPreview(_ cItem: ChatItem) -> Text {
func chatItemPreview(_ cItem: ChatItem) -> (Text, Bool) {
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
return Text(AttributedString(messageText(itemText, itemFormattedText, sender: cItem.memberDisplayName, preview: true, mentions: cItem.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, showSecrets: nil, secondaryColor: theme.colors.secondary, prefix: prefix())))
let r = messageText(itemText, itemFormattedText, sender: cItem.memberDisplayName, preview: true, mentions: cItem.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, showSecrets: nil, backgroundColor: UIColor(theme.colors.background), prefix: prefix())
return (Text(AttributedString(r.string)), r.hasSecrets)
// same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey;
// can be refactored into a single function if functions calling these are changed to return same type
@ -319,9 +323,11 @@ struct ChatPreviewView: View {
@ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?, _ hasFilePreview: Bool = false) -> some View {
if chatModel.draftChatId == chat.id, let draft = chatModel.draft {
chatPreviewLayout(messageDraft(draft), draft: true, hasFilePreview)
let (t, hasSecrets) = messageDraft(draft)
chatPreviewLayout(t, draft: true, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets)
} else if let cItem = cItem {
chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem), hasFilePreview)
let (t, hasSecrets) = chatItemPreview(cItem)
chatPreviewLayout(itemStatusMark(cItem) + t, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets)
} else {
switch (chat.chatInfo) {
case let .direct(contact):
@ -399,7 +405,7 @@ struct ChatPreviewView: View {
: chatPreviewInfoText("you are invited to group")
}
@ViewBuilder private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View {
private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View {
Text(text)
.frame(maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
.padding([.leading, .trailing], 8)
@ -479,7 +485,7 @@ struct ChatPreviewView: View {
}
}
@ViewBuilder func groupReportsIcon(size: CGFloat) -> some View {
func groupReportsIcon(size: CGFloat) -> some View {
Image(systemName: "flag")
.resizable()
.scaledToFit()

View file

@ -245,7 +245,7 @@ struct ServersSummaryView: View {
}
}
@ViewBuilder private func smpServersListView(
private func smpServersListView(
_ servers: [SMPServerSummary],
_ statsStartedAt: Date,
_ header: LocalizedStringKey? = nil,
@ -256,7 +256,7 @@ struct ServersSummaryView: View {
? serverAddress($0.smpServer) < serverAddress($1.smpServer)
: $0.hasSubs && !$1.hasSubs
}
Section {
return Section {
ForEach(sortedServers) { server in
smpServerView(server, statsStartedAt)
}
@ -318,14 +318,14 @@ struct ServersSummaryView: View {
return onionHosts == .require ? .indigo : .accentColor
}
@ViewBuilder private func xftpServersListView(
private func xftpServersListView(
_ servers: [XFTPServerSummary],
_ statsStartedAt: Date,
_ header: LocalizedStringKey? = nil,
_ footer: LocalizedStringKey? = nil
) -> some View {
let sortedServers = servers.sorted { serverAddress($0.xftpServer) < serverAddress($1.xftpServer) }
Section {
return Section {
ForEach(sortedServers) { server in
xftpServerView(server, statsStartedAt)
}

View file

@ -138,7 +138,7 @@ struct TagListView: View {
}
}
@ViewBuilder private func radioButton(selected: Bool) -> some View {
private func radioButton(selected: Bool) -> some View {
Image(systemName: selected ? "checkmark.circle.fill" : "circle")
.imageScale(.large)
.foregroundStyle(selected ? Color.accentColor : Color(.tertiaryLabel))

View file

@ -140,9 +140,9 @@ struct ContactListNavLink: View {
}
}
@ViewBuilder private func previewTitle(_ contact: Contact, titleColor: Color) -> some View {
private func previewTitle(_ contact: Contact, titleColor: Color) -> some View {
let t = Text(chat.chatInfo.chatViewName).foregroundColor(titleColor)
(
return (
contact.verified == true
? verifiedIcon + t
: t

View file

@ -28,7 +28,7 @@ struct DatabaseErrorView: View {
}
}
@ViewBuilder private func databaseErrorView() -> some View {
private func databaseErrorView() -> some View {
VStack(alignment: .center, spacing: 20) {
switch status {
case let .errorNotADatabase(dbFile):

View file

@ -28,7 +28,7 @@ struct PasscodeEntry: View {
}
}
@ViewBuilder private func passwordView() -> some View {
private func passwordView() -> some View {
Text(
password == ""
? " "

View file

@ -85,7 +85,7 @@ struct NewChatSheet: View {
}
}
@ViewBuilder private func viewBody(_ showArchive: Bool) -> some View {
private func viewBody(_ showArchive: Bool) -> some View {
List {
HStack {
ContactsListSearchBar(
@ -258,7 +258,7 @@ struct ContactsList: View {
}
}
@ViewBuilder private func noResultSection(text: String) -> some View {
private func noResultSection(text: String) -> some View {
Section {
Text(text)
.foregroundColor(theme.colors.secondary)

View file

@ -506,7 +506,7 @@ private struct ActiveProfilePicker: View {
}
}
@ViewBuilder private func profilerPickerUserOption(_ user: User) -> some View {
private func profilerPickerUserOption(_ user: User) -> some View {
Button {
if selectedProfile == user && incognitoEnabled {
incognitoEnabled = false

View file

@ -304,11 +304,11 @@ struct ChooseServerOperators: View {
}
}
@ViewBuilder private func operatorCheckView(_ serverOperator: ServerOperator) -> some View {
private func operatorCheckView(_ serverOperator: ServerOperator) -> some View {
let checked = selectedOperatorIds.contains(serverOperator.operatorId)
let icon = checked ? "checkmark.circle.fill" : "circle"
let iconColor = checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme)
HStack(spacing: 10) {
return HStack(spacing: 10) {
Image(serverOperator.largeLogo(colorScheme))
.resizable()
.scaledToFit()

View file

@ -38,9 +38,9 @@ struct OperatorView: View {
.allowsHitTesting(!testing)
}
@ViewBuilder private func operatorView() -> some View {
private func operatorView() -> some View {
let duplicateHosts = findDuplicateHosts(serverErrors)
VStack {
return VStack {
List {
Section {
infoViewLink()
@ -500,14 +500,14 @@ struct SingleOperatorUsageConditionsView: View {
}
}
@ViewBuilder private func acceptConditionsButton() -> some View {
private func acceptConditionsButton() -> some View {
let operatorIds = ChatModel.shared.conditions.serverOperators
.filter {
$0.operatorId == userServers[operatorIndex].operator_.operatorId || // Opened operator
($0.enabled && !$0.conditionsAcceptance.conditionsAccepted) // Other enabled operators with conditions not accepted
}
.map { $0.operatorId }
Button {
return Button {
acceptForOperators(operatorIds, operatorIndex)
} label: {
Text("Accept conditions")

View file

@ -38,9 +38,9 @@ struct YourServersView: View {
.allowsHitTesting(!testing)
}
@ViewBuilder private func yourServersView() -> some View {
private func yourServersView() -> some View {
let duplicateHosts = findDuplicateHosts(serverErrors)
List {
return List {
if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty {
Section {
ForEach($userServers[operatorIndex].smpServers) { srv in

View file

@ -280,9 +280,9 @@ struct SettingsView: View {
}
}
@ViewBuilder func settingsView() -> some View {
let user = chatModel.currentUser
func settingsView() -> some View {
List {
let user = chatModel.currentUser
Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) {
NavigationLink {
NotificationsView()

View file

@ -133,7 +133,6 @@ struct UserProfile: View {
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
}
@ViewBuilder
private func overlayButton(
_ systemName: String,
edge: Edge.Set,

View file

@ -221,11 +221,11 @@ struct UserProfilesView: View {
!user.hidden ? nil : trimmedSearchTextOrPassword
}
@ViewBuilder private func profileActionView(_ action: UserProfileAction) -> some View {
private func profileActionView(_ action: UserProfileAction) -> some View {
let passwordValid = actionPassword == actionPassword.trimmingCharacters(in: .whitespaces)
let passwordField = PassphraseField(key: $actionPassword, placeholder: "Profile password", valid: passwordValid)
let actionEnabled: (User) -> Bool = { user in actionPassword != "" && passwordValid && correctPassword(user, actionPassword) }
List {
return List {
switch action {
case let .deleteUser(user, delSMPQueues):
actionHeader("Delete profile", user)

View file

@ -5290,8 +5290,8 @@ Requires compatible VPN.</source>
<target>Отвори група</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5093,8 +5093,8 @@ Vyžaduje povolení sítě VPN.</target>
<source>Open group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5572,8 +5572,8 @@ Dies erfordert die Aktivierung eines VPNs.</target>
<target>Gruppe öffnen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5573,9 +5573,9 @@ Requires compatible VPN.</target>
<target>Open group</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<target>Open in browser?</target>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<target>Open link?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5572,8 +5572,8 @@ Requiere activación de la VPN.</target>
<target>Grupo abierto</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5069,8 +5069,8 @@ Edellyttää VPN:n sallimista.</target>
<source>Open group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5537,8 +5537,8 @@ Nécessite l'activation d'un VPN.</target>
<target>Ouvrir le groupe</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5572,8 +5572,8 @@ VPN engedélyezése szükséges.</target>
<target>Csoport megnyitása</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5572,8 +5572,8 @@ Richiede l'attivazione della VPN.</target>
<target>Apri gruppo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5146,8 +5146,8 @@ VPN を有効にする必要があります。</target>
<source>Open group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5572,8 +5572,8 @@ Vereist het inschakelen van VPN.</target>
<target>Open groep</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5449,8 +5449,8 @@ Wymaga włączenia VPN.</target>
<target>Grupa otwarta</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5538,8 +5538,8 @@ Requires compatible VPN.</source>
<target>Открыть группу</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5048,8 +5048,8 @@ Requires compatible VPN.</source>
<source>Open group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5462,8 +5462,8 @@ VPN'nin etkinleştirilmesi gerekir.</target>
<target>Grubu aç</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5479,8 +5479,8 @@ Requires compatible VPN.</source>
<target>Відкрита група</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -5373,8 +5373,8 @@ Requires compatible VPN.</source>
<target>打开群</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open in browser?" xml:space="preserve">
<source>Open in browser?</source>
<trans-unit id="Open link?" xml:space="preserve">
<source>Open link?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Open migration to another device" xml:space="preserve">

View file

@ -160,7 +160,7 @@ struct ShareView: View {
}
}
@ViewBuilder private func linkPreview(_ linkPreview: LinkPreview) -> some View {
private func linkPreview(_ linkPreview: LinkPreview) -> some View {
previewArea {
HStack(alignment: .center, spacing: 8) {
if let uiImage = imageFromBase64(linkPreview.image) {