+ @State private var sheetItem: ChooseServerOperatorsSheet? = nil
+
+ var body: some View {
+ GeometryReader { g in
+ ScrollView {
+ VStack(alignment: .leading, spacing: 20) {
+ Text("Server operators")
+ .font(.largeTitle)
+ .bold()
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.top, 25)
+
+ infoText()
+ .frame(maxWidth: .infinity, alignment: .center)
+
+ Spacer()
+
+ ForEach(serverOperators) { srvOperator in
+ operatorCheckView(srvOperator)
+ }
+ VStack {
+ Text("SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.").padding(.bottom, 8)
+ Text("You can configure servers via settings.")
+ }
+ .font(.footnote)
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.horizontal, 16)
+
+ Spacer()
+
+ VStack(spacing: 8) {
+ setOperatorsButton()
+ onboardingButtonPlaceholder()
+ }
+ }
+ .frame(minHeight: g.size.height)
+ }
+ .sheet(item: $sheetItem) { item in
+ switch item {
+ case .showInfo:
+ ChooseServerOperatorsInfoView()
+ }
+ }
+ .frame(maxHeight: .infinity, alignment: .top)
+ }
+ .frame(maxHeight: .infinity, alignment: .top)
+ .padding(25)
+ .interactiveDismissDisabled(selectedOperatorIds.isEmpty)
+ }
+
+ private func infoText() -> some View {
+ Button {
+ sheetItem = .showInfo
+ } label: {
+ Label("How it helps privacy", systemImage: "info.circle")
+ .font(.headline)
+ }
+ }
+
+ 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)
+ return HStack(spacing: 10) {
+ Image(serverOperator.largeLogo(colorScheme))
+ .resizable()
+ .scaledToFit()
+ .frame(height: 48)
+ Spacer()
+ Image(systemName: icon)
+ .resizable()
+ .scaledToFit()
+ .frame(width: 26, height: 26)
+ .foregroundColor(iconColor)
+ }
+ .background(theme.colors.background)
+ .padding()
+ .clipShape(RoundedRectangle(cornerRadius: 18))
+ .overlay(
+ RoundedRectangle(cornerRadius: 18)
+ .stroke(Color(uiColor: .secondarySystemFill), lineWidth: 2)
+ )
+ .padding(.horizontal, 2)
+ .onTapGesture {
+ if checked {
+ selectedOperatorIds.remove(serverOperator.operatorId)
+ } else {
+ selectedOperatorIds.insert(serverOperator.operatorId)
+ }
+ }
+ }
+
+ private func setOperatorsButton() -> some View {
+ Button {
+ dismiss()
+ } label: {
+ Text("OK")
+ }
+ .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
+ .disabled(selectedOperatorIds.isEmpty)
+ }
+}
+
let operatorsPostLink = URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html")!
struct ChooseServerOperatorsInfoView: View {
@@ -447,5 +408,5 @@ struct ChooseServerOperatorsInfoView: View {
}
#Preview {
- ChooseServerOperators(onboarding: true)
+ OnboardingConditionsView()
}
diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift
index 14ad9dfb08..ae72cb1be5 100644
--- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift
+++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift
@@ -62,8 +62,7 @@ struct CreateProfile: View {
.frame(height: 20)
} footer: {
VStack(alignment: .leading, spacing: 8) {
- Text("Your profile, contacts and delivered messages are stored on your device.")
- Text("The profile is only shared with your contacts.")
+ Text("Your profile is stored on your device and only shared with your contacts.")
}
.foregroundColor(theme.colors.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -118,25 +117,22 @@ struct CreateFirstProfile: View {
@State private var nextStepNavLinkActive = false
var body: some View {
- VStack(alignment: .leading, spacing: 20) {
- VStack(alignment: .center, spacing: 20) {
- Text("Create your profile")
+ let v = VStack(alignment: .leading, spacing: 16) {
+ VStack(alignment: .center, spacing: 16) {
+ Text("Create profile")
.font(.largeTitle)
.bold()
.multilineTextAlignment(.center)
-
- Text("Your profile, contacts and delivered messages are stored on your device.")
- .font(.callout)
- .foregroundColor(theme.colors.secondary)
- .multilineTextAlignment(.center)
-
- Text("The profile is only shared with your contacts.")
+
+ Text("Your profile is stored on your device and only shared with your contacts.")
.font(.callout)
.foregroundColor(theme.colors.secondary)
.multilineTextAlignment(.center)
}
+ .fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity) // Ensures it takes up the full width
.padding(.horizontal, 10)
+ .onTapGesture { focusDisplayName = false }
HStack {
let name = displayName.trimmingCharacters(in: .whitespaces)
@@ -145,6 +141,7 @@ struct CreateFirstProfile: View {
TextField("Enter your name…", text: $displayName)
.focused($focusDisplayName)
.padding(.horizontal)
+ .padding(.trailing, 20)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
@@ -173,13 +170,23 @@ struct CreateFirstProfile: View {
}
}
.onAppear() {
- focusDisplayName = true
- setLastVersionDefault()
+ if #available(iOS 16, *) {
+ focusDisplayName = true
+ } else {
+ // it does not work before animation completes on iOS 15
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ focusDisplayName = true
+ }
+ }
}
.padding(.horizontal, 25)
- .padding(.top, 10)
.padding(.bottom, 25)
.frame(maxWidth: .infinity, alignment: .leading)
+ if #available(iOS 16, *) {
+ return v.padding(.top, 10)
+ } else {
+ return v.padding(.top, 75).ignoresSafeArea(.all, edges: .top)
+ }
}
func createProfileButton() -> some View {
@@ -207,7 +214,7 @@ struct CreateFirstProfile: View {
}
private func nextStepDestinationView() -> some View {
- ChooseServerOperators(onboarding: true)
+ OnboardingConditionsView()
.navigationBarBackButtonHidden(true)
.modifier(ThemedBackground())
}
@@ -236,15 +243,15 @@ private func showCreateProfileAlert(
_ error: Error
) {
let m = ChatModel.shared
- switch error as? ChatResponse {
- case .chatCmdError(_, .errorStore(.duplicateName)),
- .chatCmdError(_, .error(.userExists)):
+ switch error as? ChatError {
+ case .errorStore(.duplicateName),
+ .error(.userExists):
if m.currentUser == nil {
AlertManager.shared.showAlert(duplicateUserAlert)
} else {
showAlert(.duplicateUserError)
}
- case .chatCmdError(_, .error(.invalidDisplayName)):
+ case .error(.invalidDisplayName):
if m.currentUser == nil {
AlertManager.shared.showAlert(invalidDisplayNameAlert)
} else {
diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift
index befb34b318..a2f5db7f03 100644
--- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift
+++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift
@@ -31,7 +31,7 @@ struct CreateSimpleXAddress: View {
Spacer()
if let userAddress = m.userAddress {
- SimpleXLinkQRCode(uri: userAddress.connReqContact)
+ SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: Binding.constant(false))
.frame(maxHeight: g.size.width)
shareQRCodeButton(userAddress)
.frame(maxWidth: .infinity)
@@ -77,9 +77,9 @@ struct CreateSimpleXAddress: View {
progressIndicator = true
Task {
do {
- let connReqContact = try await apiCreateUserAddress()
+ let connLinkContact = try await apiCreateUserAddress(short: false)
DispatchQueue.main.async {
- m.userAddress = UserContactLink(connReqContact: connReqContact)
+ m.userAddress = UserContactLink(connLinkContact: connLinkContact)
}
await MainActor.run { progressIndicator = false }
} catch let error {
@@ -121,7 +121,7 @@ struct CreateSimpleXAddress: View {
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
Button {
- showShareSheet(items: [simplexChatLink(userAddress.connReqContact)])
+ showShareSheet(items: [simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: false))])
} label: {
Label("Share", systemImage: "square.and.arrow.up")
}
@@ -189,7 +189,7 @@ struct SendAddressMailView: View {
let messageBody = String(format: NSLocalizedString("""
Hi!
Connect to me via SimpleX Chat
- """, comment: "email text"), simplexChatLink(userAddress.connReqContact))
+ """, comment: "email text"), simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: false)))
MailView(
isShowing: self.$showMailView,
result: $mailViewResult,
diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift
index b2b1b8fa68..8f448dc508 100644
--- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift
+++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift
@@ -23,7 +23,7 @@ struct OnboardingView: View {
case .step3_CreateSimpleXAddress: // deprecated
CreateSimpleXAddress()
case .step3_ChooseServerOperators:
- ChooseServerOperators(onboarding: true)
+ OnboardingConditionsView()
.navigationBarBackButtonHidden(true)
.modifier(ThemedBackground())
case .step4_SetNotificationsMode:
@@ -44,7 +44,7 @@ enum OnboardingStage: String, Identifiable {
case step1_SimpleXInfo
case step2_CreateProfile // deprecated
case step3_CreateSimpleXAddress // deprecated
- case step3_ChooseServerOperators
+ case step3_ChooseServerOperators // changed to simplified conditions
case step4_SetNotificationsMode
case onboardingComplete
diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift
index 97e1f49382..31865e7af9 100644
--- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift
+++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift
@@ -17,7 +17,7 @@ struct SetNotificationsMode: View {
var body: some View {
GeometryReader { g in
- ScrollView {
+ let v = ScrollView {
VStack(alignment: .center, spacing: 20) {
Text("Push notifications")
.font(.largeTitle)
@@ -57,11 +57,17 @@ struct SetNotificationsMode: View {
.padding(25)
.frame(minHeight: g.size.height)
}
+ if #available(iOS 16.4, *) {
+ v.scrollBounceBehavior(.basedOnSize)
+ } else {
+ v
+ }
}
.frame(maxHeight: .infinity)
.sheet(isPresented: $showInfo) {
NotificationsInfoView()
}
+ .navigationBarHidden(true) // necessary on iOS 15
}
private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) {
diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift
index a8704e964b..9f41a37b1d 100644
--- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift
+++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift
@@ -18,7 +18,7 @@ struct SimpleXInfo: View {
var body: some View {
GeometryReader { g in
- ScrollView {
+ let v = ScrollView {
VStack(alignment: .leading) {
VStack(alignment: .center, spacing: 10) {
Image(colorScheme == .light ? "logo" : "logo-light")
@@ -36,7 +36,7 @@ struct SimpleXInfo: View {
.font(.headline)
}
}
-
+
Spacer()
VStack(alignment: .leading) {
@@ -66,6 +66,9 @@ struct SimpleXInfo: View {
}
}
}
+ .padding(.horizontal, 25)
+ .padding(.top, 75)
+ .padding(.bottom, 25)
.frame(minHeight: g.size.height)
}
.sheet(isPresented: Binding(
@@ -88,11 +91,17 @@ struct SimpleXInfo: View {
createProfileNavLinkActive: $createProfileNavLinkActive
)
}
+ if #available(iOS 16.4, *) {
+ v.scrollBounceBehavior(.basedOnSize)
+ } else {
+ v
+ }
+ }
+ .onAppear() {
+ setLastVersionDefault()
}
.frame(maxHeight: .infinity)
- .padding(.horizontal, 25)
- .padding(.top, 75)
- .padding(.bottom, 25)
+ .navigationBarHidden(true) // necessary on iOS 15
}
private func onboardingInfoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View {
@@ -126,6 +135,7 @@ struct SimpleXInfo: View {
NavigationLink(isActive: $createProfileNavLinkActive) {
CreateFirstProfile()
+ .modifier(ThemedBackground())
} label: {
EmptyView()
}
@@ -137,6 +147,8 @@ struct SimpleXInfo: View {
let textSpace = Text(verbatim: " ")
+let textNewLine = Text(verbatim: "\n")
+
struct SimpleXInfo_Previews: PreviewProvider {
static var previews: some View {
SimpleXInfo(onboarding: true)
diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
index 182c5652d7..f65a21623a 100644
--- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
+++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
@@ -539,7 +539,46 @@ private let versionDescriptions: [VersionDescription] = [
description: "Delivered even when Apple drops them."
)),
]
- )
+ ),
+ VersionDescription(
+ version: "v6.3",
+ post: URL(string: "https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html"),
+ features: [
+ .feature(Description(
+ icon: "at",
+ title: "Mention members 👋",
+ description: "Get notified when mentioned."
+ )),
+ .feature(Description(
+ icon: "flag",
+ title: "Send private reports",
+ description: "Help admins moderating their groups."
+ )),
+ .feature(Description(
+ icon: "list.bullet",
+ title: "Organize chats into lists",
+ description: "Don't miss important messages."
+ )),
+ .feature(Description(
+ icon: nil,
+ title: "Better privacy and security",
+ description: nil,
+ subfeatures: [
+ ("eye.slash", "Private media file names."),
+ ("trash", "Set message expiration in chats.")
+ ]
+ )),
+ .feature(Description(
+ icon: nil,
+ title: "Better groups performance",
+ description: nil,
+ subfeatures: [
+ ("bolt", "Faster sending messages."),
+ ("person.2.slash", "Faster deletion of groups.")
+ ]
+ )),
+ ]
+ ),
]
private let lastVersion = versionDescriptions.last!.version
@@ -555,8 +594,6 @@ func shouldShowWhatsNew() -> Bool {
}
fileprivate struct NewOperatorsView: View {
- @State private var showOperatorsSheet = false
-
var body: some View {
VStack(alignment: .leading) {
Image((operatorsInfo[.flux] ?? ServerOperator.dummyOperatorInfo).largeLogo)
@@ -567,16 +604,7 @@ fileprivate struct NewOperatorsView: View {
.multilineTextAlignment(.leading)
.lineLimit(10)
HStack {
- Button("Enable Flux") {
- showOperatorsSheet = true
- }
- Text("for better metadata privacy.")
- }
- }
- .sheet(isPresented: $showOperatorsSheet) {
- NavigationView {
- ChooseServerOperators(onboarding: false)
- .modifier(ThemedBackground())
+ Text("Enable Flux in Network & servers settings for better metadata privacy.")
}
}
}
diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift
index 67020e09e7..01b25baed8 100644
--- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift
+++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift
@@ -456,12 +456,12 @@ struct ConnectDesktopView: View {
}
} catch let e {
await MainActor.run {
- switch e as? ChatResponse {
- case .chatCmdError(_, .errorRemoteCtrl(.badInvitation)): alert = .badInvitationError
- case .chatCmdError(_, .error(.commandError)): alert = .badInvitationError
- case let .chatCmdError(_, .errorRemoteCtrl(.badVersion(v))): alert = .badVersionError(version: v)
- case .chatCmdError(_, .errorAgent(.RCP(.version))): alert = .badVersionError(version: nil)
- case .chatCmdError(_, .errorAgent(.RCP(.ctrlAuth))): alert = .desktopDisconnectedError
+ switch e as? ChatError {
+ case .errorRemoteCtrl(.badInvitation): alert = .badInvitationError
+ case .error(.commandError): alert = .badInvitationError
+ case let .errorRemoteCtrl(.badVersion(v)): alert = .badVersionError(version: v)
+ case .errorAgent(.RCP(.version)): alert = .badVersionError(version: nil)
+ case .errorAgent(.RCP(.ctrlAuth)): alert = .desktopDisconnectedError
default: errorAlert(e)
}
}
diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift
index 23e1f783f7..554219eb69 100644
--- a/apps/ios/Shared/Views/TerminalView.swift
+++ b/apps/ios/Shared/Views/TerminalView.swift
@@ -18,7 +18,9 @@ struct TerminalView: View {
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State var composeState: ComposeState = ComposeState()
+ @State var selectedRange = NSRange()
@State private var keyboardVisible = false
+ @State private var keyboardHiddenDate = Date.now
@State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
@State private var terminalItem: TerminalItem?
@State private var scrolled = false
@@ -96,10 +98,12 @@ struct TerminalView: View {
SendMessageView(
composeState: $composeState,
+ selectedRange: $selectedRange,
sendMessage: { _ in consoleSendMessage() },
showVoiceMessageButton: false,
onMediaAdded: { _ in },
- keyboardVisible: $keyboardVisible
+ keyboardVisible: $keyboardVisible,
+ keyboardHiddenDate: $keyboardHiddenDate
)
.padding(.horizontal, 12)
}
@@ -141,18 +145,18 @@ struct TerminalView: View {
}
func consoleSendMessage() {
- let cmd = ChatCommand.string(composeState.message)
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
- let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
+ let resp: APIResult = APIResult.error(ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
Task {
- await TerminalItems.shared.addCommand(.now, cmd, resp)
+ await TerminalItems.shared.addCommand(.now, .string(composeState.message), resp)
}
} else {
+ let cmd = composeState.message
DispatchQueue.global().async {
Task {
- composeState.inProgress = true
- _ = await chatSendCmd(cmd)
- composeState.inProgress = false
+ await MainActor.run { composeState.inProgress = true }
+ await sendTerminalCmd(cmd)
+ await MainActor.run { composeState.inProgress = false }
}
}
}
@@ -160,12 +164,38 @@ struct TerminalView: View {
}
}
+func sendTerminalCmd(_ cmd: String) async {
+ let start: Date = .now
+ await withCheckedContinuation { (cont: CheckedContinuation) in
+ let d = sendSimpleXCmdStr(cmd)
+ Task {
+ guard let d else {
+ await TerminalItems.shared.addCommand(start, ChatCommand.string(cmd), APIResult.error(.invalidJSON(json: nil)))
+ return
+ }
+ let r0: APIResult = decodeAPIResult(d)
+ guard case .invalid = r0 else {
+ await TerminalItems.shared.addCommand(start, .string(cmd), r0)
+ return
+ }
+ let r1: APIResult = decodeAPIResult(d)
+ guard case .invalid = r1 else {
+ await TerminalItems.shared.addCommand(start, .string(cmd), r1)
+ return
+ }
+ let r2: APIResult = decodeAPIResult(d)
+ await TerminalItems.shared.addCommand(start, .string(cmd), r2)
+ }
+ cont.resume(returning: ())
+ }
+}
+
struct TerminalView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.terminalItems = [
- .resp(.now, ChatResponse.response(type: "contactSubscribed", json: "{}")),
- .resp(.now, ChatResponse.response(type: "newChatItems", json: "{}"))
+ .err(.now, APIResult.invalid(type: "contactSubscribed", json: "{}".data(using: .utf8)!).unexpected),
+ .err(.now, APIResult.invalid(type: "newChatItems", json: "{}".data(using: .utf8)!).unexpected)
]
return NavigationView {
TerminalView()
diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift
index 4c61d592ac..c6d0e27289 100644
--- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift
@@ -367,13 +367,13 @@ struct ChatThemePreview: View {
let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview"))
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
HStack {
- ChatItemView(chat: Chat.sampleData, chatItem: alice)
+ ChatItemView(chat: Chat.sampleData, chatItem: alice, scrollToItemId: { _ in })
.modifier(ChatItemClipped(alice, tailVisible: true))
Spacer()
}
HStack {
Spacer()
- ChatItemView(chat: Chat.sampleData, chatItem: bob)
+ ChatItemView(chat: Chat.sampleData, chatItem: bob, scrollToItemId: { _ in })
.modifier(ChatItemClipped(bob, tailVisible: true))
.frame(alignment: .trailing)
}
diff --git a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift
index cf9cada592..71c284e9ab 100644
--- a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift
+++ b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift
@@ -19,7 +19,7 @@ struct MarkdownHelp: View {
mdFormat("_italic_", Text("italic").italic())
mdFormat("~strike~", Text("strike").strikethrough())
mdFormat("`a + b`", Text("`a + b`").font(.body.monospaced()))
- mdFormat("!1 colored!", Text("colored").foregroundColor(.red) + Text(" (") + color("1", .red) + color("2", .green) + color("3", .blue) + color("4", .yellow) + color("5", .cyan) + Text("6").foregroundColor(.purple) + Text(")"))
+ mdFormat("!1 colored!", Text("colored").foregroundColor(.red) + Text(verbatim: " (") + color("1", .red) + color("2", .green) + color("3", .blue) + color("4", .yellow) + color("5", .cyan) + Text("6").foregroundColor(.purple) + Text(verbatim: ")"))
(
mdFormat("#secret#", Text("secret")
.foregroundColor(.clear)
@@ -39,7 +39,7 @@ private func mdFormat(_ format: LocalizedStringKey, _ example: Text) -> some Vie
}
private func color(_ s: String, _ c: Color) -> Text {
- Text(s).foregroundColor(c) + Text(", ")
+ Text(s).foregroundColor(c) + Text(verbatim: ", ")
}
struct MarkdownHelp_Previews: PreviewProvider {
diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift
index 754ca3cf6b..fa698f8b7c 100644
--- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift
@@ -208,6 +208,19 @@ struct AdvancedNetworkSettings: View {
}
}
+ Section {
+ Picker("Use web port", selection: $netCfg.smpWebPortServers) {
+ ForEach(SMPWebPortServers.allCases, id: \.self) { Text($0.text) }
+ }
+ .frame(height: 36)
+ } header: {
+ Text("TCP port for messaging")
+ } footer: {
+ netCfg.smpWebPortServers == .preset
+ ? Text("Use TCP port 443 for preset servers only.")
+ : Text("Use TCP port \(netCfg.smpWebPortServers == .all ? "443" : "5223") when no port is specified.")
+ }
+
Section("TCP connection") {
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
@@ -360,8 +373,8 @@ struct AdvancedNetworkSettings: View {
let userMode = Text("A separate TCP connection will be used **for each chat profile you have in the app**.")
return switch mode {
case .user: userMode
- case .session: userMode + Text("\n") + Text("New SOCKS credentials will be used every time you start the app.")
- case .server: userMode + Text("\n") + Text("New SOCKS credentials will be used for each server.")
+ case .session: userMode + textNewLine + Text("New SOCKS credentials will be used every time you start the app.")
+ case .server: userMode + textNewLine + Text("New SOCKS credentials will be used for each server.")
case .entity: Text("A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.")
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift
index 16aa98bc5f..6f4710396a 100644
--- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift
+++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift
@@ -238,22 +238,23 @@ struct UsageConditionsView: View {
var body: some View {
VStack(alignment: .leading, spacing: 20) {
- HStack {
- Text("Conditions of use").font(.largeTitle).bold()
- Spacer()
- conditionsLinkButton()
- }
- .padding(.top)
- .padding(.top)
-
switch ChatModel.shared.conditions.conditionsAction {
case .none:
+ regularConditionsHeader()
+ .padding(.top)
+ .padding(.top)
ConditionsTextView()
.padding(.bottom)
.padding(.bottom)
case let .review(operators, deadline, _):
+ HStack {
+ Text("Updated conditions").font(.largeTitle).bold()
+ }
+ .padding(.top)
+ .padding(.top)
+
Text("Conditions will be accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.")
ConditionsTextView()
VStack(spacing: 8) {
@@ -265,6 +266,10 @@ struct UsageConditionsView: View {
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.horizontal, 32)
+ conditionsDiffButton(.footnote)
+ } else {
+ conditionsDiffButton()
+ .padding(.top)
}
}
.padding(.bottom)
@@ -272,6 +277,9 @@ struct UsageConditionsView: View {
case let .accepted(operators):
+ regularConditionsHeader()
+ .padding(.top)
+ .padding(.top)
Text("Conditions are accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.")
ConditionsTextView()
.padding(.bottom)
@@ -312,6 +320,43 @@ struct UsageConditionsView: View {
}
}
}
+
+ @ViewBuilder private func conditionsDiffButton(_ font: Font? = nil) -> some View {
+ let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit
+ if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") {
+ Link(destination: commitUrl) {
+ HStack {
+ Text("Open changes")
+ Image(systemName: "arrow.up.right.circle")
+ }
+ .font(font)
+ }
+ }
+ }
+}
+
+private func regularConditionsHeader() -> some View {
+ HStack {
+ Text("Conditions of use").font(.largeTitle).bold()
+ Spacer()
+ conditionsLinkButton()
+ }
+}
+
+struct SimpleConditionsView: View {
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 20) {
+ regularConditionsHeader()
+ .padding(.top)
+ .padding(.top)
+ ConditionsTextView()
+ .padding(.bottom)
+ .padding(.bottom)
+ }
+ .padding(.horizontal, 25)
+ .frame(maxHeight: .infinity)
+ }
}
func validateServers_(_ userServers: Binding<[UserOperatorServers]>, _ serverErrors: Binding<[UserServersError]>) {
diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift
index 24da6a94a8..afbccc109c 100644
--- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift
+++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift
@@ -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")
diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift
index ed3c5c773c..b9737914ec 100644
--- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift
+++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift
@@ -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
diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift
index 4e7f826f4f..c4d0588987 100644
--- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift
@@ -13,7 +13,7 @@ struct NotificationsView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@State private var notificationMode: NotificationsMode = ChatModel.shared.notificationMode
- @State private var showAlert: NotificationAlert?
+ @State private var ntfAlert: NotificationAlert?
@State private var legacyDatabase = dbContainerGroupDefault.get() == .documents
@State private var testing = false
@State private var testedSuccess: Bool? = nil
@@ -25,7 +25,7 @@ struct NotificationsView: View {
ProgressView().scaleEffect(2)
}
}
- .alert(item: $showAlert) { alert in
+ .alert(item: $ntfAlert) { alert in
if let token = m.deviceToken {
return notificationAlert(alert, token)
} else {
@@ -41,7 +41,7 @@ struct NotificationsView: View {
List {
Section {
SelectionListView(list: NotificationsMode.values, selection: $notificationMode) { mode in
- showAlert = .setMode(mode: mode)
+ ntfAlert = .setMode(mode: mode)
}
} footer: {
VStack(alignment: .leading) {
@@ -95,7 +95,7 @@ struct NotificationsView: View {
if let server = m.notificationServer {
smpServers("Push server", [server], theme.colors.secondary)
- testServerButton(server)
+ testTokenButton(server)
}
} header: {
Text("Push notifications")
@@ -163,7 +163,7 @@ struct NotificationsView: View {
await MainActor.run {
let err = responseError(error)
logger.error("apiDeleteToken error: \(err)")
- showAlert = .error(title: "Error deleting token", error: err)
+ ntfAlert = .error(title: "Error deleting token", error: err)
}
}
default:
@@ -181,19 +181,19 @@ struct NotificationsView: View {
await MainActor.run {
let err = responseError(error)
logger.error("apiRegisterToken error: \(err)")
- showAlert = .error(title: "Error enabling notifications", error: err)
+ ntfAlert = .error(title: "Error enabling notifications", error: err)
}
}
}
}
}
- private func testServerButton(_ server: String) -> some View {
+ private func testTokenButton(_ server: String) -> some View {
HStack {
- Button("Test server") {
+ Button("Test notifications") {
testing = true
Task {
- await testServer(server)
+ await testServerAndToken(server)
await MainActor.run { testing = false }
}
}
@@ -215,22 +215,61 @@ struct NotificationsView: View {
}
}
- private func testServer(_ server: String) async {
+ private func testServerAndToken(_ server: String) async {
do {
let r = try await testProtoServer(server: server)
switch r {
case .success:
- await MainActor.run {
- testedSuccess = true
+ if let token = m.deviceToken {
+ do {
+ let status = try await apiCheckToken(token: token)
+ await MainActor.run {
+ m.tokenStatus = status
+ testedSuccess = status.workingToken
+ if status.workingToken {
+ showAlert(
+ NSLocalizedString("Notifications status", comment: "alert title"),
+ message: tokenStatusInfo(status, register: false)
+ )
+ } else {
+ showAlert(
+ title: NSLocalizedString("Notifications error", comment: "alert title"),
+ message: tokenStatusInfo(status, register: true),
+ buttonTitle: "Register",
+ buttonAction: {
+ reRegisterToken(token: token)
+ testedSuccess = nil
+ },
+ cancelButton: true
+ )
+ }
+ }
+ } catch let error {
+ await MainActor.run {
+ let err = responseError(error)
+ logger.error("apiCheckToken \(err)")
+ ntfAlert = .error(title: "Error checking token status", error: err)
+ }
+ }
+ } else {
+ await MainActor.run {
+ showAlert(
+ NSLocalizedString("No token!", comment: "alert title")
+ )
+ }
}
case let .failure(f):
await MainActor.run {
- showAlert = .testFailure(testFailure: f)
+ ntfAlert = .testFailure(testFailure: f)
testedSuccess = false
}
}
} catch let error {
- logger.error("testServerConnection \(responseError(error))")
+ await MainActor.run {
+ let err = responseError(error)
+ logger.error("testServerConnection \(err)")
+ ntfAlert = .error(title: "Error testing server connection", error: err)
+ }
}
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
index 62aad348a7..eba7f8066a 100644
--- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift
@@ -19,6 +19,8 @@ struct PrivacySettings: View {
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
+ @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
+ @AppStorage(DEFAULT_PRIVACY_SHORT_LINKS) private var shortSimplexLinks = false
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@State private var currentLAMode = privacyLocalAuthModeDefault.get()
@@ -99,6 +101,11 @@ struct PrivacySettings: View {
.onChange(of: simplexLinkMode) { mode in
privacySimplexLinkModeDefault.set(mode)
}
+ if developerTools {
+ settingsRow("link.badge.plus", color: theme.colors.secondary) {
+ Toggle("Use short links (BETA)", isOn: $shortSimplexLinks)
+ }
+ }
} header: {
Text("Chats")
.foregroundColor(theme.colors.secondary)
diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
index 8a4ccce91b..e06b1c4dd3 100644
--- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
@@ -32,6 +32,7 @@ let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // deprecated, moved t
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft"
+let DEFAULT_PRIVACY_SHORT_LINKS = "privacyShortLinks"
let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen"
let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet"
let DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS = "privacyMediaBlurRadius"
@@ -98,6 +99,7 @@ let appDefaults: [String: Any] = [
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue,
DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true,
DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true,
+ DEFAULT_PRIVACY_SHORT_LINKS: false,
DEFAULT_PRIVACY_PROTECT_SCREEN: false,
DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false,
DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS: 0,
@@ -278,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 {
@@ -474,7 +476,11 @@ struct SettingsView: View {
case .registered:
icon = "bolt.fill"
color = theme.colors.secondary
- case .invalid:
+ case .invalid: fallthrough
+ case .invalidBad: fallthrough
+ case .invalidTopic: fallthrough
+ case .invalidExpired: fallthrough
+ case .invalidUnregistered:
icon = "bolt.slash"
color = theme.colors.secondary
case .confirmed:
@@ -521,7 +527,7 @@ struct ProfilePreview: View {
func profileName(_ profileOf: NamedChat) -> Text {
var t = Text(profileOf.displayName).fontWeight(.semibold).font(.title2)
if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName {
- t = t + Text(" (" + profileOf.fullName + ")")
+ t = t + Text(verbatim: " (" + profileOf.fullName + ")")
// .font(.callout)
}
return t
diff --git a/apps/ios/Shared/Views/UserSettings/StorageView.swift b/apps/ios/Shared/Views/UserSettings/StorageView.swift
index 2cf63692a7..094c1cb3d6 100644
--- a/apps/ios/Shared/Views/UserSettings/StorageView.swift
+++ b/apps/ios/Shared/Views/UserSettings/StorageView.swift
@@ -33,7 +33,7 @@ struct StorageView: View {
private func directoryView(_ name: LocalizedStringKey, _ contents: [String: Int64]) -> some View {
Text(name).font(.headline)
ForEach(Array(contents), id: \.key) { (key, value) in
- Text(key).bold() + Text(" ") + Text("\(ByteCountFormatter.string(fromByteCount: value, countStyle: .binary))")
+ Text(key).bold() + Text(verbatim: " ") + Text((ByteCountFormatter.string(fromByteCount: value, countStyle: .binary)))
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
index 7965215b49..4813edf96c 100644
--- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
@@ -8,7 +8,7 @@
import SwiftUI
import MessageUI
-import SimpleXChat
+@preconcurrency import SimpleXChat
struct UserAddressView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@@ -16,6 +16,7 @@ struct UserAddressView: View {
@EnvironmentObject var theme: AppTheme
@State var shareViaProfile = false
@State var autoCreate = false
+ @State private var showShortLink = true
@State private var aas = AutoAcceptState()
@State private var savedAAS = AutoAcceptState()
@State private var showMailView = false
@@ -135,8 +136,8 @@ struct UserAddressView: View {
@ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View {
Section {
- SimpleXLinkQRCode(uri: userAddress.connReqContact)
- .id("simplex-contact-address-qrcode-\(userAddress.connReqContact)")
+ SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: $showShortLink)
+ .id("simplex-contact-address-qrcode-\(userAddress.connLinkContact.simplexChatUri(short: showShortLink))")
shareQRCodeButton(userAddress)
// if MFMailComposeViewController.canSendMail() {
// shareViaEmailButton(userAddress)
@@ -153,8 +154,7 @@ struct UserAddressView: View {
}
addressSettingsButton(userAddress)
} header: {
- Text("For social media")
- .foregroundColor(theme.colors.secondary)
+ ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink)
} footer: {
if aas.business {
Text("Add your team members to the conversations.")
@@ -193,9 +193,10 @@ struct UserAddressView: View {
progressIndicator = true
Task {
do {
- let connReqContact = try await apiCreateUserAddress()
+ let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)
+ let connLinkContact = try await apiCreateUserAddress(short: short)
DispatchQueue.main.async {
- chatModel.userAddress = UserContactLink(connReqContact: connReqContact)
+ chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact)
alert = .shareOnCreate
progressIndicator = false
}
@@ -231,7 +232,7 @@ struct UserAddressView: View {
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
Button {
- showShareSheet(items: [simplexChatLink(userAddress.connReqContact)])
+ showShareSheet(items: [simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: showShortLink))])
} label: {
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
Text("Share address")
@@ -294,6 +295,28 @@ struct UserAddressView: View {
}
}
+struct ToggleShortLinkHeader: View {
+ @EnvironmentObject var theme: AppTheme
+ let text: Text
+ var link: CreatedConnLink
+ @Binding var short: Bool
+
+ var body: some View {
+ if link.connShortLink == nil {
+ text.foregroundColor(theme.colors.secondary)
+ } else {
+ HStack {
+ text.foregroundColor(theme.colors.secondary)
+ Spacer()
+ Text(short ? "Full link" : "Short link")
+ .textCase(.none)
+ .foregroundColor(theme.colors.primary)
+ .onTapGesture { short.toggle() }
+ }
+ }
+ }
+}
+
private struct AutoAcceptState: Equatable {
var enable = false
var incognito = false
@@ -542,7 +565,7 @@ private func saveAAS(_ aas: Binding, _ savedAAS: Binding 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)
diff --git a/apps/ios/SimpleX (iOS).entitlements b/apps/ios/SimpleX (iOS).entitlements
index c78a7cb941..2ec32def0a 100644
--- a/apps/ios/SimpleX (iOS).entitlements
+++ b/apps/ios/SimpleX (iOS).entitlements
@@ -9,6 +9,10 @@
applinks:simplex.chat
applinks:www.simplex.chat
applinks:simplex.chat?mode=developer
+ applinks:*.simplex.im
+ applinks:*.simplex.im?mode=developer
+ applinks:*.simplexonflux.com
+ applinks:*.simplexonflux.com?mode=developer
com.apple.security.application-groups
diff --git a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff
index ef91bb30fd..e965e5a1a5 100644
--- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff
+++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff
@@ -49,7 +49,7 @@
%@
- %@
+ %@
No comment provided by engineer.
@@ -379,292 +379,356 @@
أضف إلى جهاز آخر
No comment provided by engineer.
-
+
Admins can create the links to join groups.
+ يمكن للمُدراء إنشاء روابط للانضمام إلى المجموعات.
No comment provided by engineer.
-
+
Advanced network settings
+ إعدادات الشبكة المتقدمة
No comment provided by engineer.
-
+
All chats and messages will be deleted - this cannot be undone!
+ سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا!
No comment provided by engineer.
-
+
All group members will remain connected.
+ سيبقى جميع أعضاء المجموعة على اتصال.
No comment provided by engineer.
-
+
All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.
+ سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.
No comment provided by engineer.
All your contacts will remain connected
No comment provided by engineer.
-
+
Allow
+ سماح
No comment provided by engineer.
-
+
Allow disappearing messages only if your contact allows it to you.
+ السماح بالرسائل المختفية فقط إذا سمحت لك جهة الاتصال بذلك.
No comment provided by engineer.
Allow irreversible message deletion only if your contact allows it to you.
No comment provided by engineer.
-
+
Allow sending direct messages to members.
+ السماح بإرسال رسائل مباشرة إلى الأعضاء.
No comment provided by engineer.
-
+
Allow sending disappearing messages.
+ السماح بإرسال الرسائل التي تختفي.
No comment provided by engineer.
Allow to irreversibly delete sent messages.
No comment provided by engineer.
-
+
Allow to send voice messages.
+ السماح بإرسال رسائل صوتية.
No comment provided by engineer.
-
+
Allow voice messages only if your contact allows them.
+ اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك.
No comment provided by engineer.
-
+
Allow voice messages?
+ السماح بالرسائل الصوتية؟
No comment provided by engineer.
Allow your contacts to irreversibly delete sent messages.
No comment provided by engineer.
-
+
Allow your contacts to send disappearing messages.
+ السماح لجهات اتصالك بإرسال رسائل تختفي.
No comment provided by engineer.
-
+
Allow your contacts to send voice messages.
+ اسمح لجهات اتصالك بإرسال رسائل صوتية.
No comment provided by engineer.
-
+
Already connected?
+ متصل بالفعل؟
No comment provided by engineer.
-
+
Answer call
+ أجب الاتصال
No comment provided by engineer.
-
+
App build: %@
+ إصدار التطبيق: %@
No comment provided by engineer.
-
+
App icon
+ رمز التطبيق
No comment provided by engineer.
-
+
App version
+ نسخة التطبيق
No comment provided by engineer.
-
+
App version: v%@
+ نسخة التطبيق: v%@
No comment provided by engineer.
-
+
Appearance
+ المظهر
No comment provided by engineer.
-
+
Attach
+ إرفاق
No comment provided by engineer.
-
+
Audio & video calls
+ مكالمات الصوت والفيديو
No comment provided by engineer.
-
+
Authentication failed
+ فشلت المصادقة
No comment provided by engineer.
-
+
Authentication unavailable
+ المصادقة غير متاحة
No comment provided by engineer.
-
+
Auto-accept contact requests
+ قبول طلبات الاتصال تلقائيًا
No comment provided by engineer.
-
+
Auto-accept images
+ قبول تلقائي للصور
No comment provided by engineer.
Automatically
No comment provided by engineer.
-
+
Back
+ رجوع
No comment provided by engineer.
Both you and your contact can irreversibly delete sent messages.
No comment provided by engineer.
-
+
Both you and your contact can send disappearing messages.
+ يمكنك أنت وجهة اتصالك إرسال رسائل تختفي.
No comment provided by engineer.
-
+
Both you and your contact can send voice messages.
+ يمكنك أنت وجهة اتصالك إرسال رسائل صوتية.
No comment provided by engineer.
-
+
By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).
+ حسب ملف تعريف الدردشة (افتراضي) أو [حسب الاتصال] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).
No comment provided by engineer.
-
+
Call already ended!
+ انتهت المكالمة بالفعل!
No comment provided by engineer.
-
+
Calls
+ المكالمات
No comment provided by engineer.
-
+
Can't invite contact!
+ لا يمكن دعوة جهة اتصال!
No comment provided by engineer.
-
+
Can't invite contacts!
+ لا يمكن دعوة جهات الاتصال!
No comment provided by engineer.
-
+
Cancel
+ إلغاء
No comment provided by engineer.
-
+
Cannot access keychain to save database password
+ لا يمكن الوصول إلى سلسلة المفاتيح لحفظ كلمة مرور قاعدة البيانات
No comment provided by engineer.
-
+
Cannot receive file
+ لا يمكن استلام الملف
No comment provided by engineer.
-
+
Change
+ تغير
No comment provided by engineer.
-
+
Change database passphrase?
+ تغيير عبارة مرور قاعدة البيانات؟
No comment provided by engineer.
-
+
Change member role?
+ تغيير دور العضو؟
No comment provided by engineer.
-
+
Change receiving address
+ تغيير عنوان الاستلام
No comment provided by engineer.
-
+
Change receiving address?
+ تغيير عنوان الاستلام؟
No comment provided by engineer.
-
+
Change role
+ تغيير الدور
No comment provided by engineer.
Chat archive
No comment provided by engineer.
-
+
Chat console
+ وحدة تحكم الدردشة
No comment provided by engineer.
-
+
Chat database
+ قاعدة بيانات الدردشة
No comment provided by engineer.
-
+
Chat database deleted
+ حُذفت قاعدة بيانات الدردشة
No comment provided by engineer.
-
+
Chat database imported
+ استُوردت قاعدة بيانات الدردشة
No comment provided by engineer.
-
+
Chat is running
+ الدردشة قيد التشغيل
No comment provided by engineer.
-
+
Chat is stopped
+ توقفت الدردشة
No comment provided by engineer.
-
+
Chat preferences
+ تفضيلات الدردشة
No comment provided by engineer.
-
+
Chats
+ الدردشات
No comment provided by engineer.
-
+
Check server address and try again.
+ تحقق من عنوان الخادم وحاول مرة أخرى.
No comment provided by engineer.
-
+