mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 12:19:54 +00:00
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:
parent
d338696035
commit
8d54acef92
53 changed files with 352 additions and 264 deletions
23
apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/Contents.json
vendored
Normal file
23
apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x1.png
vendored
Normal file
BIN
apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x1.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x2.png
vendored
Normal file
BIN
apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x2.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x3.png
vendored
Normal file
BIN
apps/ios/Shared/Assets.xcassets/vertical_logo.imageset/vertical_logo_x3.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2 KiB |
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -87,7 +87,7 @@ struct FramedItemView: View {
|
|||
.overlay(DetermineWidth())
|
||||
.accessibilityLabel("")
|
||||
}
|
||||
}
|
||||
}
|
||||
.background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) }
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -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 ?? [
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
ZStack {
|
||||
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 {
|
||||
} else {
|
||||
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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
+ attachment()
|
||||
+ Text(AttributedString(messageText(msg, parseSimpleXMarkdown(msg), sender: nil, preview: true, mentions: draft.mentions, userMemberId: nil, showSecrets: nil, secondaryColor: theme.colors.secondary)))
|
||||
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(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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -28,7 +28,7 @@ struct PasscodeEntry: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func passwordView() -> some View {
|
||||
private func passwordView() -> some View {
|
||||
Text(
|
||||
password == ""
|
||||
? " "
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -280,159 +280,159 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func settingsView() -> some View {
|
||||
let user = chatModel.currentUser
|
||||
List {
|
||||
Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) {
|
||||
NavigationLink {
|
||||
NotificationsView()
|
||||
.navigationTitle("Notifications")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
HStack {
|
||||
notificationsIcon()
|
||||
Text("Notifications")
|
||||
}
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
NavigationLink {
|
||||
NetworkAndServers()
|
||||
.navigationTitle("Network & servers")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") }
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
NavigationLink {
|
||||
CallSettings()
|
||||
.navigationTitle("Your calls")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") }
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
NavigationLink {
|
||||
PrivacySettings()
|
||||
.navigationTitle("Your privacy")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("lock", color: theme.colors.secondary) { Text("Privacy & security") }
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
if UIApplication.shared.supportsAlternateIcons {
|
||||
NavigationLink {
|
||||
AppearanceSettings()
|
||||
.navigationTitle("Appearance")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("sun.max", color: theme.colors.secondary) { Text("Appearance") }
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
func settingsView() -> some View {
|
||||
List {
|
||||
let user = chatModel.currentUser
|
||||
Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) {
|
||||
NavigationLink {
|
||||
NotificationsView()
|
||||
.navigationTitle("Notifications")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
HStack {
|
||||
notificationsIcon()
|
||||
Text("Notifications")
|
||||
}
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) {
|
||||
chatDatabaseRow()
|
||||
NavigationLink {
|
||||
MigrateFromDevice(showProgressOnSettings: $showProgress)
|
||||
.toolbar {
|
||||
// Redaction broken for `.navigationTitle` - using a toolbar item instead.
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text("Migrate device").font(.headline)
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") }
|
||||
}
|
||||
NavigationLink {
|
||||
NetworkAndServers()
|
||||
.navigationTitle("Network & servers")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") }
|
||||
}
|
||||
|
||||
Section(header: Text("Help").foregroundColor(theme.colors.secondary)) {
|
||||
if let user = user {
|
||||
NavigationLink {
|
||||
ChatHelp(dismissSettingsSheet: dismiss)
|
||||
.navigationTitle("Welcome \(user.displayName)!")
|
||||
.modifier(ThemedBackground())
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
} label: {
|
||||
settingsRow("questionmark", color: theme.colors.secondary) { Text("How to use it") }
|
||||
}
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
NavigationLink {
|
||||
CallSettings()
|
||||
.navigationTitle("Your calls")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") }
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
NavigationLink {
|
||||
PrivacySettings()
|
||||
.navigationTitle("Your privacy")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("lock", color: theme.colors.secondary) { Text("Privacy & security") }
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
if UIApplication.shared.supportsAlternateIcons {
|
||||
NavigationLink {
|
||||
WhatsNewView(viaSettings: true, updatedConditions: false)
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
AppearanceSettings()
|
||||
.navigationTitle("Appearance")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("plus", color: theme.colors.secondary) { Text("What's new") }
|
||||
settingsRow("sun.max", color: theme.colors.secondary) { Text("Appearance") }
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) {
|
||||
chatDatabaseRow()
|
||||
NavigationLink {
|
||||
MigrateFromDevice(showProgressOnSettings: $showProgress)
|
||||
.toolbar {
|
||||
// Redaction broken for `.navigationTitle` - using a toolbar item instead.
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text("Migrate device").font(.headline)
|
||||
}
|
||||
}
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") }
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Help").foregroundColor(theme.colors.secondary)) {
|
||||
if let user = user {
|
||||
NavigationLink {
|
||||
SimpleXInfo(onboarding: false)
|
||||
.navigationBarTitle("", displayMode: .inline)
|
||||
ChatHelp(dismissSettingsSheet: dismiss)
|
||||
.navigationTitle("Welcome \(user.displayName)!")
|
||||
.modifier(ThemedBackground())
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
} label: {
|
||||
settingsRow("info", color: theme.colors.secondary) { Text("About SimpleX Chat") }
|
||||
settingsRow("questionmark", color: theme.colors.secondary) { Text("How to use it") }
|
||||
}
|
||||
settingsRow("number", color: theme.colors.secondary) {
|
||||
Button("Send questions and ideas") {
|
||||
dismiss()
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(simplexTeamURL)
|
||||
}
|
||||
}
|
||||
NavigationLink {
|
||||
WhatsNewView(viaSettings: true, updatedConditions: false)
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
settingsRow("plus", color: theme.colors.secondary) { Text("What's new") }
|
||||
}
|
||||
NavigationLink {
|
||||
SimpleXInfo(onboarding: false)
|
||||
.navigationBarTitle("", displayMode: .inline)
|
||||
.modifier(ThemedBackground())
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
} label: {
|
||||
settingsRow("info", color: theme.colors.secondary) { Text("About SimpleX Chat") }
|
||||
}
|
||||
settingsRow("number", color: theme.colors.secondary) {
|
||||
Button("Send questions and ideas") {
|
||||
dismiss()
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(simplexTeamURL)
|
||||
}
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
settingsRow("envelope", color: theme.colors.secondary) { Text("[Send us email](mailto:chat@simplex.chat)") }
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
settingsRow("envelope", color: theme.colors.secondary) { Text("[Send us email](mailto:chat@simplex.chat)") }
|
||||
}
|
||||
|
||||
Section(header: Text("Support SimpleX Chat").foregroundColor(theme.colors.secondary)) {
|
||||
settingsRow("keyboard", color: theme.colors.secondary) { Text("[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)") }
|
||||
settingsRow("star", color: theme.colors.secondary) {
|
||||
Button("Rate the app") {
|
||||
if let scene = sceneDelegate.windowScene {
|
||||
SKStoreReviewController.requestReview(in: scene)
|
||||
}
|
||||
Section(header: Text("Support SimpleX Chat").foregroundColor(theme.colors.secondary)) {
|
||||
settingsRow("keyboard", color: theme.colors.secondary) { Text("[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)") }
|
||||
settingsRow("star", color: theme.colors.secondary) {
|
||||
Button("Rate the app") {
|
||||
if let scene = sceneDelegate.windowScene {
|
||||
SKStoreReviewController.requestReview(in: scene)
|
||||
}
|
||||
}
|
||||
ZStack(alignment: .leading) {
|
||||
Image(colorScheme == .dark ? "github_light" : "github")
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.opacity(0.5)
|
||||
.colorMultiply(theme.colors.secondary)
|
||||
Text("[Star on GitHub](https://github.com/simplex-chat/simplex-chat)")
|
||||
.padding(.leading, indent)
|
||||
}
|
||||
}
|
||||
ZStack(alignment: .leading) {
|
||||
Image(colorScheme == .dark ? "github_light" : "github")
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.opacity(0.5)
|
||||
.colorMultiply(theme.colors.secondary)
|
||||
Text("[Star on GitHub](https://github.com/simplex-chat/simplex-chat)")
|
||||
.padding(.leading, indent)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Develop").foregroundColor(theme.colors.secondary)) {
|
||||
NavigationLink {
|
||||
DeveloperView()
|
||||
.navigationTitle("Developer tools")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) { Text("Developer tools") }
|
||||
}
|
||||
NavigationLink {
|
||||
VersionView()
|
||||
.navigationBarTitle("App version")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
|
||||
}
|
||||
Section(header: Text("Develop").foregroundColor(theme.colors.secondary)) {
|
||||
NavigationLink {
|
||||
DeveloperView()
|
||||
.navigationTitle("Developer tools")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) { Text("Developer tools") }
|
||||
}
|
||||
NavigationLink {
|
||||
VersionView()
|
||||
.navigationBarTitle("App version")
|
||||
.modifier(ThemedBackground())
|
||||
} label: {
|
||||
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Your settings")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.onDisappear {
|
||||
chatModel.showingTerminal = false
|
||||
chatModel.terminalItems = []
|
||||
}
|
||||
}
|
||||
.navigationTitle("Your settings")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.onDisappear {
|
||||
chatModel.showingTerminal = false
|
||||
chatModel.terminalItems = []
|
||||
}
|
||||
}
|
||||
|
||||
private func chatDatabaseRow() -> some View {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue