mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +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 {
|
ZStack {
|
||||||
let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted
|
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.
|
// 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 {
|
HStack {
|
||||||
Text(call.contact.displayName).font(.body).foregroundColor(.white)
|
Text(call.contact.displayName).font(.body).foregroundColor(.white)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
|
@ -467,7 +467,7 @@ struct ActiveCallOverlay: View {
|
||||||
.disabled(call.initialCallType == .audio && client.activeCall?.peerHasOldVersion == true)
|
.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) {
|
controlButton(call, "arrow.triangle.2.circlepath", padding: 12) {
|
||||||
Task {
|
Task {
|
||||||
if await WebRTCClient.isAuthorized(for: .video) {
|
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)
|
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()
|
AudioDevicePicker()
|
||||||
.opacity(0.8)
|
.opacity(0.8)
|
||||||
.scaleEffect(2)
|
.scaleEffect(2)
|
||||||
|
|
|
@ -50,7 +50,7 @@ struct CICallItemView: View {
|
||||||
Image(systemName: "phone.connection").foregroundColor(.green)
|
Image(systemName: "phone.connection").foregroundColor(.green)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func endedCallIcon(_ sent: Bool) -> some View {
|
private func endedCallIcon(_ sent: Bool) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "phone.down")
|
Image(systemName: "phone.down")
|
||||||
Text(durationText(duration)).foregroundColor(theme.colors.secondary)
|
Text(durationText(duration)).foregroundColor(theme.colors.secondary)
|
||||||
|
|
|
@ -46,7 +46,7 @@ struct CILinkView: View {
|
||||||
|
|
||||||
func openBrowserAlert(uri: URL) {
|
func openBrowserAlert(uri: URL) {
|
||||||
showAlert(
|
showAlert(
|
||||||
NSLocalizedString("Open in browser?", comment: "alert title"),
|
NSLocalizedString("Open link?", comment: "alert title"),
|
||||||
message: uri.absoluteString,
|
message: uri.absoluteString,
|
||||||
actions: {[
|
actions: {[
|
||||||
UIAlertAction(
|
UIAlertAction(
|
||||||
|
|
|
@ -68,7 +68,7 @@ struct CIRcvDecryptionError: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func viewBody() -> some View {
|
private func viewBody() -> some View {
|
||||||
Group {
|
Group {
|
||||||
if case let .direct(contact) = chat.chatInfo,
|
if case let .direct(contact) = chat.chatInfo,
|
||||||
let contactStats = contact.activeConn?.connectionStats {
|
let contactStats = contact.activeConn?.connectionStats {
|
||||||
|
|
|
@ -87,7 +87,7 @@ struct FramedItemView: View {
|
||||||
.overlay(DetermineWidth())
|
.overlay(DetermineWidth())
|
||||||
.accessibilityLabel("")
|
.accessibilityLabel("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) }
|
.background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) }
|
||||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||||
|
|
||||||
|
@ -201,6 +201,7 @@ struct FramedItemView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func ciQuoteView(_ qi: CIQuote) -> some View {
|
@ViewBuilder private func ciQuoteView(_ qi: CIQuote) -> some View {
|
||||||
|
let backgroundColor = chatItemFrameContextColor(chatItem, theme)
|
||||||
let v = ZStack(alignment: .topTrailing) {
|
let v = ZStack(alignment: .topTrailing) {
|
||||||
switch (qi.content) {
|
switch (qi.content) {
|
||||||
case let .image(_, image):
|
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
|
// 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() } }
|
.overlay { if case .voice = chatItem.content.msgContent {} else { DetermineWidth() } }
|
||||||
.frame(minWidth: msgWidth, alignment: .leading)
|
.frame(minWidth: msgWidth, alignment: .leading)
|
||||||
.background(chatItemFrameContextColor(chatItem, theme))
|
.background(backgroundColor)
|
||||||
|
.environment(\.containerBackground, UIColor(backgroundColor))
|
||||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||||
} else {
|
} else {
|
||||||
|
@ -308,6 +310,7 @@ struct FramedItemView: View {
|
||||||
rightToLeft: rtl,
|
rightToLeft: rtl,
|
||||||
prefix: txtPrefix
|
prefix: txtPrefix
|
||||||
)
|
)
|
||||||
|
.environment(\.containerBackground, UIColor(chatItemFrameColor(ci, theme)))
|
||||||
.multilineTextAlignment(rtl ? .trailing : .leading)
|
.multilineTextAlignment(rtl ? .trailing : .leading)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
|
|
|
@ -26,6 +26,7 @@ private func typing(_ theme: AppTheme, _ descr: UIFontDescriptor, _ ws: [UIFont.
|
||||||
struct MsgContentView: View {
|
struct MsgContentView: View {
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
@Environment(\.showTimestamp) var showTimestamp: Bool
|
@Environment(\.showTimestamp) var showTimestamp: Bool
|
||||||
|
@Environment(\.containerBackground) var containerBackground: UIColor
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
var text: String
|
var text: String
|
||||||
var formattedText: [FormattedText]? = nil
|
var formattedText: [FormattedText]? = nil
|
||||||
|
@ -92,7 +93,8 @@ struct MsgContentView: View {
|
||||||
|
|
||||||
@inline(__always)
|
@inline(__always)
|
||||||
private func msgContentView() -> some View {
|
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
|
let t: Text
|
||||||
if let mt = meta {
|
if let mt = meta {
|
||||||
if mt.isLive {
|
if mt.isLive {
|
||||||
|
@ -102,7 +104,7 @@ struct MsgContentView: View {
|
||||||
} else {
|
} else {
|
||||||
t = Text(AttributedString(s))
|
t = Text(AttributedString(s))
|
||||||
}
|
}
|
||||||
return t.overlay(handleTextLinks(s, showSecrets: $showSecrets))
|
return msgTextResultView(r, t, showSecrets: $showSecrets)
|
||||||
}
|
}
|
||||||
|
|
||||||
@inline(__always)
|
@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
|
return GeometryReader { g in
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(Color.clear)
|
.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 linkAttrKey = NSAttributedString.Key("chat.simplex.app.link")
|
||||||
|
|
||||||
private let webLinkAttrKey = NSAttributedString.Key("chat.simplex.app.webLink")
|
private let webLinkAttrKey = NSAttributedString.Key("chat.simplex.app.webLink")
|
||||||
|
|
||||||
private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret")
|
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 res = NSMutableAttributedString()
|
||||||
let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
|
let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)
|
||||||
let font = UIFont.preferredFont(forTextStyle: textStyle)
|
let font = UIFont.preferredFont(forTextStyle: textStyle)
|
||||||
|
@ -188,7 +226,10 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: U
|
||||||
.font: font,
|
.font: font,
|
||||||
.foregroundColor: UIColor.label
|
.foregroundColor: UIColor.label
|
||||||
]
|
]
|
||||||
|
let secretColor = backgroundColor.withAlphaComponent(1)
|
||||||
var link: [NSAttributedString.Key: Any]?
|
var link: [NSAttributedString.Key: Any]?
|
||||||
|
var hasSecrets = false
|
||||||
|
var handleTaps = false
|
||||||
|
|
||||||
if let sender {
|
if let sender {
|
||||||
if preview {
|
if preview {
|
||||||
|
@ -230,14 +271,16 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: U
|
||||||
if let showSecrets {
|
if let showSecrets {
|
||||||
if !showSecrets.contains(secretIdx) {
|
if !showSecrets.contains(secretIdx) {
|
||||||
attrs[.foregroundColor] = UIColor.clear
|
attrs[.foregroundColor] = UIColor.clear
|
||||||
attrs[.backgroundColor] = UIColor.secondarySystemFill // secretColor
|
attrs[.backgroundColor] = secretColor
|
||||||
}
|
}
|
||||||
attrs[secretAttrKey] = secretIdx
|
attrs[secretAttrKey] = secretIdx
|
||||||
secretIdx += 1
|
secretIdx += 1
|
||||||
|
handleTaps = true
|
||||||
} else {
|
} else {
|
||||||
attrs[.foregroundColor] = UIColor.clear
|
attrs[.foregroundColor] = UIColor.clear
|
||||||
attrs[.backgroundColor] = UIColor.secondarySystemFill
|
attrs[.backgroundColor] = secretColor
|
||||||
}
|
}
|
||||||
|
hasSecrets = true
|
||||||
case let .colored(color):
|
case let .colored(color):
|
||||||
if let c = color.uiColor {
|
if let c = color.uiColor {
|
||||||
attrs[.foregroundColor] = UIColor(c)
|
attrs[.foregroundColor] = UIColor(c)
|
||||||
|
@ -247,11 +290,13 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: U
|
||||||
if !preview {
|
if !preview {
|
||||||
attrs[linkAttrKey] = NSURL(string: ft.text)
|
attrs[linkAttrKey] = NSURL(string: ft.text)
|
||||||
attrs[webLinkAttrKey] = true
|
attrs[webLinkAttrKey] = true
|
||||||
|
handleTaps = true
|
||||||
}
|
}
|
||||||
case let .simplexLink(linkType, simplexUri, smpHosts):
|
case let .simplexLink(linkType, simplexUri, smpHosts):
|
||||||
attrs = linkAttrs()
|
attrs = linkAttrs()
|
||||||
if !preview {
|
if !preview {
|
||||||
attrs[linkAttrKey] = NSURL(string: simplexUri)
|
attrs[linkAttrKey] = NSURL(string: simplexUri)
|
||||||
|
handleTaps = true
|
||||||
}
|
}
|
||||||
if case .description = privacySimplexLinkModeDefault.get() {
|
if case .description = privacySimplexLinkModeDefault.get() {
|
||||||
t = simplexLinkText(linkType, smpHosts)
|
t = simplexLinkText(linkType, smpHosts)
|
||||||
|
@ -278,11 +323,13 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: U
|
||||||
attrs = linkAttrs()
|
attrs = linkAttrs()
|
||||||
if !preview {
|
if !preview {
|
||||||
attrs[linkAttrKey] = NSURL(string: "mailto:" + ft.text)
|
attrs[linkAttrKey] = NSURL(string: "mailto:" + ft.text)
|
||||||
|
handleTaps = true
|
||||||
}
|
}
|
||||||
case .phone:
|
case .phone:
|
||||||
attrs = linkAttrs()
|
attrs = linkAttrs()
|
||||||
if !preview {
|
if !preview {
|
||||||
attrs[linkAttrKey] = NSURL(string: "tel:" + t.replacingOccurrences(of: " ", with: ""))
|
attrs[linkAttrKey] = NSURL(string: "tel:" + t.replacingOccurrences(of: " ", with: ""))
|
||||||
|
handleTaps = true
|
||||||
}
|
}
|
||||||
case .none: ()
|
case .none: ()
|
||||||
}
|
}
|
||||||
|
@ -292,7 +339,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, textStyle: U
|
||||||
res.append(NSMutableAttributedString(string: text, attributes: plain))
|
res.append(NSMutableAttributedString(string: text, attributes: plain))
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return (string: res, hasSecrets: hasSecrets, handleTaps: handleTaps)
|
||||||
|
|
||||||
func linkAttrs() -> [NSAttributedString.Key: Any] {
|
func linkAttrs() -> [NSAttributedString.Key: Any] {
|
||||||
link = link ?? [
|
link = link ?? [
|
||||||
|
|
|
@ -41,7 +41,7 @@ struct ChatItemForwardingView: View {
|
||||||
.alert(item: $alert) { $0.alert }
|
.alert(item: $alert) { $0.alert }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func forwardListView() -> some View {
|
private func forwardListView() -> some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if !chatsToForwardTo.isEmpty {
|
if !chatsToForwardTo.isEmpty {
|
||||||
List {
|
List {
|
||||||
|
|
|
@ -131,9 +131,9 @@ struct ChatItemInfoView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func details() -> some View {
|
private func details() -> some View {
|
||||||
let meta = ci.meta
|
let meta = ci.meta
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
return VStack(alignment: .leading, spacing: 16) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
.bold()
|
.bold()
|
||||||
|
@ -197,7 +197,7 @@ struct ChatItemInfoView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func historyTab() -> some View {
|
private func historyTab() -> some View {
|
||||||
GeometryReader { g in
|
GeometryReader { g in
|
||||||
let maxWidth = (g.size.width - 32) * 0.84
|
let maxWidth = (g.size.width - 32) * 0.84
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
@ -227,12 +227,13 @@ struct ChatItemInfoView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View {
|
private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
let backgroundColor = chatItemFrameColor(ci, theme)
|
||||||
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil)
|
return VStack(alignment: .leading, spacing: 4) {
|
||||||
|
textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil, backgroundColor: UIColor(backgroundColor))
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.background(chatItemFrameColor(ci, theme))
|
.background(backgroundColor)
|
||||||
.modifier(ChatItemClipped())
|
.modifier(ChatItemClipped())
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
if itemVersion.msgContent.text != "" {
|
if itemVersion.msgContent.text != "" {
|
||||||
|
@ -257,9 +258,9 @@ struct ChatItemInfoView: View {
|
||||||
.frame(maxWidth: maxWidth, alignment: .leading)
|
.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 != "" {
|
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 {
|
} else {
|
||||||
Text("no text")
|
Text("no text")
|
||||||
.italic()
|
.italic()
|
||||||
|
@ -274,15 +275,16 @@ struct ChatItemInfoView: View {
|
||||||
var sender: String? = nil
|
var sender: String? = nil
|
||||||
var mentions: [String: CIMention]?
|
var mentions: [String: CIMention]?
|
||||||
var userMemberId: String?
|
var userMemberId: String?
|
||||||
|
var backgroundColor: UIColor
|
||||||
@State private var showSecrets: Set<Int> = []
|
@State private var showSecrets: Set<Int> = []
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let s = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, secondaryColor: theme.colors.secondary)
|
let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: backgroundColor)
|
||||||
Text(AttributedString(s)).overlay(handleTextLinks(s, showSecrets: $showSecrets))
|
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
|
GeometryReader { g in
|
||||||
let maxWidth = (g.size.width - 32) * 0.84
|
let maxWidth = (g.size.width - 32) * 0.84
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
@ -300,9 +302,10 @@ struct ChatItemInfoView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View {
|
private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
let backgroundColor = quotedMsgFrameColor(qi, theme)
|
||||||
textBubble(qi.text, qi.formattedText, qi.getSender(nil))
|
return VStack(alignment: .leading, spacing: 4) {
|
||||||
|
textBubble(qi.text, qi.formattedText, qi.getSender(nil), backgroundColor: UIColor(backgroundColor))
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.background(quotedMsgFrameColor(qi, theme))
|
.background(quotedMsgFrameColor(qi, theme))
|
||||||
|
@ -335,7 +338,7 @@ struct ChatItemInfoView: View {
|
||||||
: theme.appColors.receivedMessage
|
: theme.appColors.receivedMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
|
private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
details()
|
details()
|
||||||
|
@ -373,7 +376,7 @@ struct ChatItemInfoView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View {
|
private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
ChatInfoImage(chat: Chat(chatInfo: forwardedFromItem.chatInfo), size: 48)
|
ChatInfoImage(chat: Chat(chatInfo: forwardedFromItem.chatInfo), size: 48)
|
||||||
.padding(.trailing, 6)
|
.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 {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
details()
|
details()
|
||||||
|
@ -419,7 +422,7 @@ struct ChatItemInfoView: View {
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
.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) {
|
LazyVStack(alignment: .leading, spacing: 12) {
|
||||||
let mss = membersStatuses(memberDeliveryStatuses)
|
let mss = membersStatuses(memberDeliveryStatuses)
|
||||||
if !mss.isEmpty {
|
if !mss.isEmpty {
|
||||||
|
|
|
@ -18,6 +18,10 @@ extension EnvironmentValues {
|
||||||
static let defaultValue: Bool = true
|
static let defaultValue: Bool = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ContainerBackground: EnvironmentKey {
|
||||||
|
static let defaultValue: UIColor = .clear
|
||||||
|
}
|
||||||
|
|
||||||
var showTimestamp: Bool {
|
var showTimestamp: Bool {
|
||||||
get { self[ShowTimestamp.self] }
|
get { self[ShowTimestamp.self] }
|
||||||
set { self[ShowTimestamp.self] = newValue }
|
set { self[ShowTimestamp.self] = newValue }
|
||||||
|
@ -27,6 +31,11 @@ extension EnvironmentValues {
|
||||||
get { self[Revealed.self] }
|
get { self[Revealed.self] }
|
||||||
set { self[Revealed.self] = newValue }
|
set { self[Revealed.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var containerBackground: UIColor {
|
||||||
|
get { self[ContainerBackground.self] }
|
||||||
|
set { self[ContainerBackground.self] = newValue }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatItemView: View {
|
struct ChatItemView: View {
|
||||||
|
|
|
@ -71,10 +71,9 @@ struct ChatView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var viewBody: some View {
|
private var viewBody: some View {
|
||||||
let cInfo = chat.chatInfo
|
let cInfo = chat.chatInfo
|
||||||
ZStack {
|
return ZStack {
|
||||||
let wallpaperImage = theme.wallpaper.type.image
|
let wallpaperImage = theme.wallpaper.type.image
|
||||||
let wallpaperType = theme.wallpaper.type
|
let wallpaperType = theme.wallpaper.type
|
||||||
let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background)
|
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
|
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
||||||
VStack(alignment: alignment.horizontal, spacing: 3) {
|
return VStack(alignment: alignment.horizontal, spacing: 3) {
|
||||||
HStack {
|
HStack {
|
||||||
if ci.chatDir.sent {
|
if ci.chatDir.sent {
|
||||||
goToItemButton(true)
|
goToItemButton(true)
|
||||||
|
|
|
@ -70,8 +70,10 @@ struct ContextItemView: View {
|
||||||
.lineLimit(lines)
|
.lineLimit(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func contextMsgPreview(_ contextItem: ChatItem) -> Text {
|
private func contextMsgPreview(_ contextItem: ChatItem) -> some View {
|
||||||
return attachment() + Text(AttributedString(messageText(contextItem.text, contextItem.formattedText, sender: nil, preview: true, mentions: contextItem.mentions, userMemberId: nil, showSecrets: nil, secondaryColor: theme.colors.secondary)))
|
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 {
|
func attachment() -> Text {
|
||||||
let isFileLoaded = if let fileSource = getLoadedFileSource(contextItem.file) {
|
let isFileLoaded = if let fileSource = getLoadedFileSource(contextItem.file) {
|
||||||
|
|
|
@ -145,9 +145,9 @@ struct AddGroupMembersViewCommon: View {
|
||||||
return dummy
|
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"
|
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Invite to group" : "Invite to chat"
|
||||||
Button {
|
return Button {
|
||||||
inviteMembers()
|
inviteMembers()
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
|
@ -292,9 +292,9 @@ struct GroupChatInfoView: View {
|
||||||
.disabled(!groupInfo.ready || chat.chatItems.isEmpty)
|
.disabled(!groupInfo.ready || chat.chatItems.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func addMembersActionButton(width: CGFloat) -> some View {
|
private func addMembersActionButton(width: CGFloat) -> some View {
|
||||||
if chat.chatInfo.incognito {
|
ZStack {
|
||||||
ZStack {
|
if chat.chatInfo.incognito {
|
||||||
InfoViewButton(image: "link.badge.plus", title: "invite", width: width) {
|
InfoViewButton(image: "link.badge.plus", title: "invite", width: width) {
|
||||||
groupLinkNavLinkActive = true
|
groupLinkNavLinkActive = true
|
||||||
}
|
}
|
||||||
|
@ -306,10 +306,7 @@ struct GroupChatInfoView: View {
|
||||||
}
|
}
|
||||||
.frame(width: 1, height: 1)
|
.frame(width: 1, height: 1)
|
||||||
.hidden()
|
.hidden()
|
||||||
}
|
} else {
|
||||||
.disabled(!groupInfo.ready)
|
|
||||||
} else {
|
|
||||||
ZStack {
|
|
||||||
InfoViewButton(image: "person.fill.badge.plus", title: "invite", width: width) {
|
InfoViewButton(image: "person.fill.badge.plus", title: "invite", width: width) {
|
||||||
addMembersNavLinkActive = true
|
addMembersNavLinkActive = true
|
||||||
}
|
}
|
||||||
|
@ -322,8 +319,8 @@ struct GroupChatInfoView: View {
|
||||||
.frame(width: 1, height: 1)
|
.frame(width: 1, height: 1)
|
||||||
.hidden()
|
.hidden()
|
||||||
}
|
}
|
||||||
.disabled(!groupInfo.ready)
|
|
||||||
}
|
}
|
||||||
|
.disabled(!groupInfo.ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func muteButton(width: CGFloat, nextNtfMode: MsgFilter) -> some View {
|
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"
|
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group" : "Leave chat"
|
||||||
Button(role: .destructive) {
|
return Button(role: .destructive) {
|
||||||
alert = .leaveGroupAlert
|
alert = .leaveGroupAlert
|
||||||
} label: {
|
} label: {
|
||||||
Label(label, systemImage: "rectangle.portrait.and.arrow.right")
|
Label(label, systemImage: "rectangle.portrait.and.arrow.right")
|
||||||
|
|
|
@ -18,6 +18,7 @@ struct GroupWelcomeView: View {
|
||||||
@State private var editMode = true
|
@State private var editMode = true
|
||||||
@FocusState private var keyboardVisible: Bool
|
@FocusState private var keyboardVisible: Bool
|
||||||
@State private var showSaveDialog = false
|
@State private var showSaveDialog = false
|
||||||
|
@State private var showSecrets: Set<Int> = []
|
||||||
|
|
||||||
let maxByteCount = 1200
|
let maxByteCount = 1200
|
||||||
|
|
||||||
|
@ -58,9 +59,8 @@ struct GroupWelcomeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func textPreview() -> some View {
|
private func textPreview() -> some View {
|
||||||
let s = messageText(welcomeText, parseSimpleXMarkdown(welcomeText), sender: nil, mentions: nil, userMemberId: nil, showSecrets: nil, secondaryColor: theme.colors.secondary)
|
let r = messageText(welcomeText, parseSimpleXMarkdown(welcomeText), sender: nil, mentions: nil, userMemberId: nil, showSecrets: showSecrets, backgroundColor: UIColor(theme.colors.background))
|
||||||
return Text(AttributedString(s))
|
return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets)
|
||||||
.overlay(handleTextLinks(s))
|
|
||||||
.frame(minHeight: 130, alignment: .topLeading)
|
.frame(minHeight: 130, alignment: .topLeading)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ struct ChatListNavLink: View {
|
||||||
.actionSheet(item: $actionSheet) { $0.actionSheet }
|
.actionSheet(item: $actionSheet) { $0.actionSheet }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func contactNavLink(_ contact: Contact) -> some View {
|
private func contactNavLink(_ contact: Contact) -> some View {
|
||||||
Group {
|
Group {
|
||||||
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
|
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
|
||||||
ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false))
|
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(
|
NavLinkPlain(
|
||||||
chatId: chat.chatInfo.id,
|
chatId: chat.chatInfo.id,
|
||||||
selection: $chatModel.chatId,
|
selection: $chatModel.chatId,
|
||||||
|
|
|
@ -335,9 +335,9 @@ struct ChatListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private var chatList: some View {
|
private var chatList: some View {
|
||||||
let cs = filteredChats()
|
let cs = filteredChats()
|
||||||
ZStack {
|
return ZStack {
|
||||||
ScrollViewReader { scrollProxy in
|
ScrollViewReader { scrollProxy in
|
||||||
List {
|
List {
|
||||||
if !chatModel.chats.isEmpty {
|
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
|
ForEach(PresetTag.allCases, id: \.id) { tag in
|
||||||
if (chatTagsModel.presetTags[tag] ?? 0) > 0 {
|
if (chatTagsModel.presetTags[tag] ?? 0) > 0 {
|
||||||
expandedTagFilterView(tag)
|
expandedTagFilterView(tag)
|
||||||
|
|
|
@ -187,13 +187,14 @@ struct ChatPreviewView: View {
|
||||||
.kerning(-2)
|
.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) {
|
ZStack(alignment: .topTrailing) {
|
||||||
let s = chat.chatStats
|
let s = chat.chatStats
|
||||||
let mentionWidth: CGFloat = if s.unreadMentions > 0 && s.unreadCount > 1 { dynamicSize(userFont).unreadCorner } else { 0 }
|
let mentionWidth: CGFloat = if s.unreadMentions > 0 && s.unreadCount > 1 { dynamicSize(userFont).unreadCorner } else { 0 }
|
||||||
let t = text
|
let t = text
|
||||||
.lineLimit(userFont <= .xxxLarge ? 2 : 1)
|
.lineLimit(userFont <= .xxxLarge ? 2 : 1)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
.if(hasSecrets, transform: hiddenSecretsView)
|
||||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||||
.padding(.leading, hasFilePreview ? 0 : 8)
|
.padding(.leading, hasFilePreview ? 0 : 8)
|
||||||
.padding(.trailing, mentionWidth + (hasFilePreview ? 38 : 36))
|
.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
|
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))
|
||||||
+ attachment()
|
return (image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary)
|
||||||
+ Text(AttributedString(messageText(msg, parseSimpleXMarkdown(msg), sender: nil, preview: true, mentions: draft.mentions, userMemberId: nil, showSecrets: nil, secondaryColor: theme.colors.secondary)))
|
+ attachment()
|
||||||
|
+ Text(AttributedString(r.string)),
|
||||||
|
r.hasSecrets)
|
||||||
|
|
||||||
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
|
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
|
||||||
Text(Image(systemName: s)).foregroundColor(color) + textSpace
|
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 itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
|
||||||
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
|
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;
|
// 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
|
// 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 {
|
@ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?, _ hasFilePreview: Bool = false) -> some View {
|
||||||
if chatModel.draftChatId == chat.id, let draft = chatModel.draft {
|
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 {
|
} 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 {
|
} else {
|
||||||
switch (chat.chatInfo) {
|
switch (chat.chatInfo) {
|
||||||
case let .direct(contact):
|
case let .direct(contact):
|
||||||
|
@ -399,7 +405,7 @@ struct ChatPreviewView: View {
|
||||||
: chatPreviewInfoText("you are invited to group")
|
: chatPreviewInfoText("you are invited to group")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View {
|
private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View {
|
||||||
Text(text)
|
Text(text)
|
||||||
.frame(maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
|
.frame(maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
|
||||||
.padding([.leading, .trailing], 8)
|
.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")
|
Image(systemName: "flag")
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
|
|
|
@ -245,7 +245,7 @@ struct ServersSummaryView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func smpServersListView(
|
private func smpServersListView(
|
||||||
_ servers: [SMPServerSummary],
|
_ servers: [SMPServerSummary],
|
||||||
_ statsStartedAt: Date,
|
_ statsStartedAt: Date,
|
||||||
_ header: LocalizedStringKey? = nil,
|
_ header: LocalizedStringKey? = nil,
|
||||||
|
@ -256,7 +256,7 @@ struct ServersSummaryView: View {
|
||||||
? serverAddress($0.smpServer) < serverAddress($1.smpServer)
|
? serverAddress($0.smpServer) < serverAddress($1.smpServer)
|
||||||
: $0.hasSubs && !$1.hasSubs
|
: $0.hasSubs && !$1.hasSubs
|
||||||
}
|
}
|
||||||
Section {
|
return Section {
|
||||||
ForEach(sortedServers) { server in
|
ForEach(sortedServers) { server in
|
||||||
smpServerView(server, statsStartedAt)
|
smpServerView(server, statsStartedAt)
|
||||||
}
|
}
|
||||||
|
@ -318,14 +318,14 @@ struct ServersSummaryView: View {
|
||||||
return onionHosts == .require ? .indigo : .accentColor
|
return onionHosts == .require ? .indigo : .accentColor
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func xftpServersListView(
|
private func xftpServersListView(
|
||||||
_ servers: [XFTPServerSummary],
|
_ servers: [XFTPServerSummary],
|
||||||
_ statsStartedAt: Date,
|
_ statsStartedAt: Date,
|
||||||
_ header: LocalizedStringKey? = nil,
|
_ header: LocalizedStringKey? = nil,
|
||||||
_ footer: LocalizedStringKey? = nil
|
_ footer: LocalizedStringKey? = nil
|
||||||
) -> some View {
|
) -> some View {
|
||||||
let sortedServers = servers.sorted { serverAddress($0.xftpServer) < serverAddress($1.xftpServer) }
|
let sortedServers = servers.sorted { serverAddress($0.xftpServer) < serverAddress($1.xftpServer) }
|
||||||
Section {
|
return Section {
|
||||||
ForEach(sortedServers) { server in
|
ForEach(sortedServers) { server in
|
||||||
xftpServerView(server, statsStartedAt)
|
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")
|
Image(systemName: selected ? "checkmark.circle.fill" : "circle")
|
||||||
.imageScale(.large)
|
.imageScale(.large)
|
||||||
.foregroundStyle(selected ? Color.accentColor : Color(.tertiaryLabel))
|
.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)
|
let t = Text(chat.chatInfo.chatViewName).foregroundColor(titleColor)
|
||||||
(
|
return (
|
||||||
contact.verified == true
|
contact.verified == true
|
||||||
? verifiedIcon + t
|
? verifiedIcon + t
|
||||||
: 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) {
|
VStack(alignment: .center, spacing: 20) {
|
||||||
switch status {
|
switch status {
|
||||||
case let .errorNotADatabase(dbFile):
|
case let .errorNotADatabase(dbFile):
|
||||||
|
|
|
@ -28,7 +28,7 @@ struct PasscodeEntry: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func passwordView() -> some View {
|
private func passwordView() -> some View {
|
||||||
Text(
|
Text(
|
||||||
password == ""
|
password == ""
|
||||||
? " "
|
? " "
|
||||||
|
|
|
@ -85,7 +85,7 @@ struct NewChatSheet: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func viewBody(_ showArchive: Bool) -> some View {
|
private func viewBody(_ showArchive: Bool) -> some View {
|
||||||
List {
|
List {
|
||||||
HStack {
|
HStack {
|
||||||
ContactsListSearchBar(
|
ContactsListSearchBar(
|
||||||
|
@ -258,7 +258,7 @@ struct ContactsList: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func noResultSection(text: String) -> some View {
|
private func noResultSection(text: String) -> some View {
|
||||||
Section {
|
Section {
|
||||||
Text(text)
|
Text(text)
|
||||||
.foregroundColor(theme.colors.secondary)
|
.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 {
|
Button {
|
||||||
if selectedProfile == user && incognitoEnabled {
|
if selectedProfile == user && incognitoEnabled {
|
||||||
incognitoEnabled = false
|
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 checked = selectedOperatorIds.contains(serverOperator.operatorId)
|
||||||
let icon = checked ? "checkmark.circle.fill" : "circle"
|
let icon = checked ? "checkmark.circle.fill" : "circle"
|
||||||
let iconColor = checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme)
|
let iconColor = checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme)
|
||||||
HStack(spacing: 10) {
|
return HStack(spacing: 10) {
|
||||||
Image(serverOperator.largeLogo(colorScheme))
|
Image(serverOperator.largeLogo(colorScheme))
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
|
|
|
@ -38,9 +38,9 @@ struct OperatorView: View {
|
||||||
.allowsHitTesting(!testing)
|
.allowsHitTesting(!testing)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func operatorView() -> some View {
|
private func operatorView() -> some View {
|
||||||
let duplicateHosts = findDuplicateHosts(serverErrors)
|
let duplicateHosts = findDuplicateHosts(serverErrors)
|
||||||
VStack {
|
return VStack {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
infoViewLink()
|
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
|
let operatorIds = ChatModel.shared.conditions.serverOperators
|
||||||
.filter {
|
.filter {
|
||||||
$0.operatorId == userServers[operatorIndex].operator_.operatorId || // Opened operator
|
$0.operatorId == userServers[operatorIndex].operator_.operatorId || // Opened operator
|
||||||
($0.enabled && !$0.conditionsAcceptance.conditionsAccepted) // Other enabled operators with conditions not accepted
|
($0.enabled && !$0.conditionsAcceptance.conditionsAccepted) // Other enabled operators with conditions not accepted
|
||||||
}
|
}
|
||||||
.map { $0.operatorId }
|
.map { $0.operatorId }
|
||||||
Button {
|
return Button {
|
||||||
acceptForOperators(operatorIds, operatorIndex)
|
acceptForOperators(operatorIds, operatorIndex)
|
||||||
} label: {
|
} label: {
|
||||||
Text("Accept conditions")
|
Text("Accept conditions")
|
||||||
|
|
|
@ -38,9 +38,9 @@ struct YourServersView: View {
|
||||||
.allowsHitTesting(!testing)
|
.allowsHitTesting(!testing)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func yourServersView() -> some View {
|
private func yourServersView() -> some View {
|
||||||
let duplicateHosts = findDuplicateHosts(serverErrors)
|
let duplicateHosts = findDuplicateHosts(serverErrors)
|
||||||
List {
|
return List {
|
||||||
if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty {
|
if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty {
|
||||||
Section {
|
Section {
|
||||||
ForEach($userServers[operatorIndex].smpServers) { srv in
|
ForEach($userServers[operatorIndex].smpServers) { srv in
|
||||||
|
|
|
@ -280,159 +280,159 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func settingsView() -> some View {
|
func settingsView() -> some View {
|
||||||
let user = chatModel.currentUser
|
List {
|
||||||
List {
|
let user = chatModel.currentUser
|
||||||
Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) {
|
Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
NotificationsView()
|
NotificationsView()
|
||||||
.navigationTitle("Notifications")
|
.navigationTitle("Notifications")
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.modifier(ThemedBackground(grouped: true))
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
notificationsIcon()
|
notificationsIcon()
|
||||||
Text("Notifications")
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.disabled(chatModel.chatRunning != true)
|
||||||
|
|
||||||
Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) {
|
NavigationLink {
|
||||||
chatDatabaseRow()
|
NetworkAndServers()
|
||||||
NavigationLink {
|
.navigationTitle("Network & servers")
|
||||||
MigrateFromDevice(showProgressOnSettings: $showProgress)
|
.modifier(ThemedBackground(grouped: true))
|
||||||
.toolbar {
|
} label: {
|
||||||
// Redaction broken for `.navigationTitle` - using a toolbar item instead.
|
settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") }
|
||||||
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") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.disabled(chatModel.chatRunning != true)
|
||||||
Section(header: Text("Help").foregroundColor(theme.colors.secondary)) {
|
|
||||||
if let user = user {
|
NavigationLink {
|
||||||
NavigationLink {
|
CallSettings()
|
||||||
ChatHelp(dismissSettingsSheet: dismiss)
|
.navigationTitle("Your calls")
|
||||||
.navigationTitle("Welcome \(user.displayName)!")
|
.modifier(ThemedBackground(grouped: true))
|
||||||
.modifier(ThemedBackground())
|
} label: {
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") }
|
||||||
} label: {
|
}
|
||||||
settingsRow("questionmark", color: theme.colors.secondary) { Text("How to use it") }
|
.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 {
|
NavigationLink {
|
||||||
WhatsNewView(viaSettings: true, updatedConditions: false)
|
AppearanceSettings()
|
||||||
.modifier(ThemedBackground())
|
.navigationTitle("Appearance")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.modifier(ThemedBackground(grouped: true))
|
||||||
} label: {
|
} 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 {
|
NavigationLink {
|
||||||
SimpleXInfo(onboarding: false)
|
ChatHelp(dismissSettingsSheet: dismiss)
|
||||||
.navigationBarTitle("", displayMode: .inline)
|
.navigationTitle("Welcome \(user.displayName)!")
|
||||||
.modifier(ThemedBackground())
|
.modifier(ThemedBackground())
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
} label: {
|
} 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") {
|
NavigationLink {
|
||||||
dismiss()
|
WhatsNewView(viaSettings: true, updatedConditions: false)
|
||||||
DispatchQueue.main.async {
|
.modifier(ThemedBackground())
|
||||||
UIApplication.shared.open(simplexTeamURL)
|
.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)) {
|
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("keyboard", color: theme.colors.secondary) { Text("[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)") }
|
||||||
settingsRow("star", color: theme.colors.secondary) {
|
settingsRow("star", color: theme.colors.secondary) {
|
||||||
Button("Rate the app") {
|
Button("Rate the app") {
|
||||||
if let scene = sceneDelegate.windowScene {
|
if let scene = sceneDelegate.windowScene {
|
||||||
SKStoreReviewController.requestReview(in: scene)
|
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)) {
|
Section(header: Text("Develop").foregroundColor(theme.colors.secondary)) {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
DeveloperView()
|
DeveloperView()
|
||||||
.navigationTitle("Developer tools")
|
.navigationTitle("Developer tools")
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.modifier(ThemedBackground(grouped: true))
|
||||||
} label: {
|
} label: {
|
||||||
settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) { Text("Developer tools") }
|
settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) { Text("Developer tools") }
|
||||||
}
|
}
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
VersionView()
|
VersionView()
|
||||||
.navigationBarTitle("App version")
|
.navigationBarTitle("App version")
|
||||||
.modifier(ThemedBackground())
|
.modifier(ThemedBackground())
|
||||||
} label: {
|
} label: {
|
||||||
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
|
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Your settings")
|
}
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.navigationTitle("Your settings")
|
||||||
.onDisappear {
|
.modifier(ThemedBackground(grouped: true))
|
||||||
chatModel.showingTerminal = false
|
.onDisappear {
|
||||||
chatModel.terminalItems = []
|
chatModel.showingTerminal = false
|
||||||
}
|
chatModel.terminalItems = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func chatDatabaseRow() -> some View {
|
private func chatDatabaseRow() -> some View {
|
||||||
|
|
|
@ -133,7 +133,6 @@ struct UserProfile: View {
|
||||||
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
|
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func overlayButton(
|
private func overlayButton(
|
||||||
_ systemName: String,
|
_ systemName: String,
|
||||||
edge: Edge.Set,
|
edge: Edge.Set,
|
||||||
|
|
|
@ -221,11 +221,11 @@ struct UserProfilesView: View {
|
||||||
!user.hidden ? nil : trimmedSearchTextOrPassword
|
!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 passwordValid = actionPassword == actionPassword.trimmingCharacters(in: .whitespaces)
|
||||||
let passwordField = PassphraseField(key: $actionPassword, placeholder: "Profile password", valid: passwordValid)
|
let passwordField = PassphraseField(key: $actionPassword, placeholder: "Profile password", valid: passwordValid)
|
||||||
let actionEnabled: (User) -> Bool = { user in actionPassword != "" && passwordValid && correctPassword(user, actionPassword) }
|
let actionEnabled: (User) -> Bool = { user in actionPassword != "" && passwordValid && correctPassword(user, actionPassword) }
|
||||||
List {
|
return List {
|
||||||
switch action {
|
switch action {
|
||||||
case let .deleteUser(user, delSMPQueues):
|
case let .deleteUser(user, delSMPQueues):
|
||||||
actionHeader("Delete profile", user)
|
actionHeader("Delete profile", user)
|
||||||
|
|
|
@ -5290,8 +5290,8 @@ Requires compatible VPN.</source>
|
||||||
<target>Отвори група</target>
|
<target>Отвори група</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<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>
|
<source>Open group</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<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>
|
<target>Gruppe öffnen</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||||
|
|
|
@ -5573,9 +5573,9 @@ Requires compatible VPN.</target>
|
||||||
<target>Open group</target>
|
<target>Open group</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<target>Open in browser?</target>
|
<target>Open link?</target>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<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>
|
<target>Grupo abierto</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||||
|
|
|
@ -5069,8 +5069,8 @@ Edellyttää VPN:n sallimista.</target>
|
||||||
<source>Open group</source>
|
<source>Open group</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<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>
|
<target>Ouvrir le groupe</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<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>
|
<target>Csoport megnyitása</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<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>
|
<target>Apri gruppo</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||||
|
|
|
@ -5146,8 +5146,8 @@ VPN を有効にする必要があります。</target>
|
||||||
<source>Open group</source>
|
<source>Open group</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<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>
|
<target>Open groep</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||||
|
|
|
@ -5449,8 +5449,8 @@ Wymaga włączenia VPN.</target>
|
||||||
<target>Grupa otwarta</target>
|
<target>Grupa otwarta</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||||
|
|
|
@ -5538,8 +5538,8 @@ Requires compatible VPN.</source>
|
||||||
<target>Открыть группу</target>
|
<target>Открыть группу</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||||
|
|
|
@ -5048,8 +5048,8 @@ Requires compatible VPN.</source>
|
||||||
<source>Open group</source>
|
<source>Open group</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<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>
|
<target>Grubu aç</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||||
|
|
|
@ -5479,8 +5479,8 @@ Requires compatible VPN.</source>
|
||||||
<target>Відкрита група</target>
|
<target>Відкрита група</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||||
|
|
|
@ -5373,8 +5373,8 @@ Requires compatible VPN.</source>
|
||||||
<target>打开群</target>
|
<target>打开群</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open in browser?" xml:space="preserve">
|
<trans-unit id="Open link?" xml:space="preserve">
|
||||||
<source>Open in browser?</source>
|
<source>Open link?</source>
|
||||||
<note>alert title</note>
|
<note>alert title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
<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 {
|
previewArea {
|
||||||
HStack(alignment: .center, spacing: 8) {
|
HStack(alignment: .center, spacing: 8) {
|
||||||
if let uiImage = imageFromBase64(linkPreview.image) {
|
if let uiImage = imageFromBase64(linkPreview.image) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue