diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c3ef9fa088..b89f6ccce0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,17 +10,25 @@ on:
- "!*-fdroid"
- "!*-armv7a"
pull_request:
- paths-ignore:
- - "apps/ios"
- - "apps/multiplatform"
- - "blog"
- - "docs"
- - "fastlane"
- - "images"
- - "packages"
- - "website"
- - "README.md"
- - "PRIVACY.md"
+ paths:
+ - "src/**"
+ - "apps/simplex-chat/**"
+ - "apps/simplex-bot/**"
+ - "apps/simplex-bot-advanced/**"
+ - "apps/simplex-broadcast-bot/**"
+ - "apps/simplex-directory-service/**"
+ - "tests/**"
+ - "bots/src/**"
+ - "simplex-chat.cabal"
+ - "cabal.project"
+ - "Dockerfile*"
+ - "scripts/ci/**"
+ - "scripts/desktop/**"
+ - ".github/**"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/v') }}
# This workflow uses custom actions (prepare-build and prepare-release) defined in:
#
@@ -294,6 +302,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true
shell: docker exec -t builder sh -eu {0}
run: |
+ export ASSETS_DIR='../../assets'
scripts/desktop/make-deb-linux.sh
- name: Prepare Desktop
@@ -319,6 +328,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == '22.04' && matrix.should_run == true
shell: docker exec -t builder sh -eu {0}
run: |
+ export ASSETS_DIR='../../assets'
scripts/desktop/make-appimage-linux.sh
- name: Prepare AppImage
@@ -369,6 +379,100 @@ jobs:
exit 1
fi
+# =================================
+# Linux PostgreSQL Library Build
+# =================================
+
+ build-linux-postgres:
+ name: "ubuntu-22.04-x86_64 (Postgres lib), GHC: ${{ needs.variables.outputs.GHC_VER }}"
+ needs: [maybe-release, variables]
+ runs-on: ubuntu-22.04
+ if: startsWith(github.ref, 'refs/tags/v')
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v3
+
+ - name: Get UID and GID
+ id: ids
+ run: |
+ echo "uid=$(id -u)" >> $GITHUB_OUTPUT
+ echo "gid=$(id -g)" >> $GITHUB_OUTPUT
+
+ - name: Free disk space
+ shell: bash
+ run: ./scripts/ci/linux_util_free_space.sh
+
+ - name: Restore cached build
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cabal/store
+ dist-newstyle
+ key: ubuntu-22.04-x86_64-postgres-ghc${{ needs.variables.outputs.GHC_VER }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }}
+
+ - name: Set up Docker Buildx
+ uses: simplex-chat/docker-setup-buildx-action@v3
+
+ - name: Build and cache Docker image
+ uses: simplex-chat/docker-build-push-action@v6
+ with:
+ context: .
+ load: true
+ file: Dockerfile.build
+ tags: build/22.04:latest
+ build-args: |
+ TAG=22.04
+ HASH=sha256:5c8b2c0a6c745bc177669abfaa716b4bc57d58e2ea3882fb5da67f4d59e3dda5
+ GHC=${{ needs.variables.outputs.GHC_VER }}
+ USER_UID=${{ steps.ids.outputs.uid }}
+ USER_GID=${{ steps.ids.outputs.gid }}
+
+ - name: Start container
+ shell: bash
+ run: |
+ docker run -t -d \
+ --name builder \
+ -v ~/.cabal:/root/.cabal \
+ -v /home/runner/work/_temp:/home/runner/work/_temp \
+ -v ${{ github.workspace }}:/project \
+ build/22.04:latest
+
+ - name: Prepare cabal.project.local
+ shell: bash
+ run: |
+ echo "ignore-project: False" >> cabal.project.local
+ echo "package direct-sqlcipher" >> cabal.project.local
+ echo " flags: +openssl" >> cabal.project.local
+
+ - name: Build postgres library
+ shell: docker exec -t builder sh -eu {0}
+ run: |
+ cabal clean
+ cabal update
+ scripts/desktop/build-lib-linux.sh postgres
+
+ - name: Copy libs from container
+ shell: bash
+ run: |
+ ARCH=x86_64
+ GHC_VER=${{ needs.variables.outputs.GHC_VER }}
+ BUILD_DIR=$(echo dist-newstyle/build/${ARCH}-linux/ghc-${GHC_VER}/simplex-chat-*)
+ mkdir -p postgres-libs
+ cp ${BUILD_DIR}/build/libsimplex.so postgres-libs/
+ cp ${BUILD_DIR}/build/deps/* postgres-libs/
+
+ - name: Upload postgres libs artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: simplex-libs-linux-postgres-x86_64
+ path: postgres-libs/
+
+ - name: Fix permissions for cache
+ shell: bash
+ run: |
+ sudo chmod -R 777 dist-newstyle ~/.cabal
+ sudo chown -R $(id -u):$(id -g) dist-newstyle ~/.cabal
+
# =========================
# MacOS Build
# =========================
@@ -447,6 +551,7 @@ jobs:
APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }}
APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }}
run: |
+ export ASSETS_DIR='../../assets'
scripts/ci/build-desktop-mac.sh
path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg)
echo "package_path=$path" >> $GITHUB_OUTPUT
@@ -573,7 +678,7 @@ jobs:
export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo)
scripts/desktop/build-lib-windows.sh
cd apps/multiplatform
- ./gradlew packageMsi
+ ./gradlew -Psimplex.assets.dir=../../assets packageMsi
rm -rf dist-newstyle/src/direct-sq*
path=$(echo $PWD/release/main/msi/*imple*.msi | sed 's#/\([a-z]\)#\1:#' | sed 's#/#\\#g')
echo "package_path=$path" >> $GITHUB_OUTPUT
@@ -605,7 +710,7 @@ jobs:
release-nodejs-libs:
runs-on: ubuntu-latest
- needs: [build-linux, build-macos]
+ needs: [build-linux, build-linux-postgres, build-macos]
if: startsWith(github.ref, 'refs/tags/v') && (!cancelled())
steps:
- name: Checkout current repository
@@ -614,6 +719,13 @@ jobs:
- name: Install packages for archiving
run: sudo apt install -y msitools gcc-mingw-w64
+ - name: Download postgres libs artifact
+ if: needs.build-linux-postgres.result == 'success'
+ uses: actions/download-artifact@v4
+ with:
+ name: simplex-libs-linux-postgres-x86_64
+ path: ${{ runner.temp }}/postgres-libs
+
- name: Build archives
run: |
INIT_DIR='${{ runner.temp }}/artifacts'
@@ -670,6 +782,17 @@ jobs:
zip -r "${PREFIX}-windows-x86_64.zip" libs
mv "${PREFIX}-windows-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
+ # Linux PostgreSQL (only if postgres build succeeded)
+ # -------------------------------------------------
+ POSTGRES_LIBS='${{ runner.temp }}/postgres-libs'
+ if [ -d "$POSTGRES_LIBS" ]; then
+ mkdir -p linux-postgres/libs
+ cp "${POSTGRES_LIBS}"/*.so linux-postgres/libs/
+ cd linux-postgres
+ zip -r "${PREFIX}-linux-x86_64-postgres.zip" libs
+ mv "${PREFIX}-linux-x86_64-postgres.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
+ fi
+
- name: Create release in libs repo and upload artifacts
uses: softprops/action-gh-release@v2
with:
diff --git a/README.md b/README.md
index 818ed7142f..252fc95708 100644
--- a/README.md
+++ b/README.md
@@ -425,9 +425,9 @@ Please do NOT report security vulnerabilities via GitHub issues.
## License
-This software is licensed under the GNU Affero General Public License version 3 (AGPLv3). See the [LICENSE](./LICENSE) file for details. The SimpleX and SimpleX Chat name, logo, and associated branding materials are not covered by this license and are subject to the terms outlined in the [TRADEMARK](./docs/TRADEMARK.md) file.
+This software is licensed under the GNU Affero General Public License version 3 (AGPLv3). See the [LICENSE](./LICENSE) file for details. The SimpleX and SimpleX Chat name, logo, associated branding materials, and application and website graphic assets (illustrations, images, visual designs, etc.) are not covered by this license and are subject to the terms outlined in the [TRADEMARK](./docs/TRADEMARK.md) and [ASSETS_LICENSE](./assets/ASSETS_LICENSE.md) files respectively.
-Graphic designs, artworks and layouts are not licensed for re-use. If you want to use them in your publications, please ask for permission. Texts can be used as direct quotes, referencing the source.
+If you want to use any graphic assets in your publications, please ask for permission. Texts can be used as direct quotes, referencing the source.
[
](https://apps.apple.com/us/app/simplex-chat/id1605771084)
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 20653ab9db..85bb8a30b4 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -2183,7 +2183,7 @@ func startChat(refreshInvitations: Bool = true, onboarding: Bool = false) throws
withAnimation {
let savedOnboardingStage = onboardingStageDefault.get()
m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1
- ? .step3_ChooseServerOperators
+ ? .step4_NetworkCommitments
: savedOnboardingStage
if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() {
m.setDeliveryReceipts = true
diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift
index d0ff1934ba..1839651daa 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift
@@ -172,8 +172,8 @@ struct ChatItemContentView: View {
case .rcvBlocked: deletedItemView()
case let .sndDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo))
case let .rcvDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo))
- case .sndGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText())
- case .rcvGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText())
+ case let .sndGroupE2EEInfo(e2eeInfo): CIEventView(eventText: groupE2EEInfoText(e2eeInfo))
+ case let .rcvGroupE2EEInfo(e2eeInfo): CIEventView(eventText: groupE2EEInfoText(e2eeInfo))
case .chatBanner: EmptyView()
case let .invalidJSON(json): CIInvalidJSONView(json: json)
}
@@ -257,6 +257,12 @@ struct ChatItemContentView: View {
e2eeInfoText("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.")
}
+ private func groupE2EEInfoText(_ info: E2EEInfo) -> Text {
+ info.public == true
+ ? e2eeInfoText("Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages.")
+ : e2eeInfoNoPQText()
+ }
+
private func e2eeInfoText(_ s: LocalizedStringKey) -> Text {
Text(s)
.font(.caption)
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
index fd47ddfacb..334abd76ee 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
@@ -515,7 +515,7 @@ struct ComposeView: View {
sendMessageView(
disableSendButton,
placeholder: chat.chatInfo.groupInfo.map { gi in
- gi.useRelays && gi.membership.memberRole >= .owner
+ gi.useRelays && gi.membership.memberRole >= .owner && chat.chatInfo.groupChatScope() == nil
? NSLocalizedString("Broadcast", comment: "compose placeholder for channel owner")
: nil
} ?? nil
@@ -1659,7 +1659,7 @@ struct ComposeView: View {
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
scope: chat.chatInfo.groupChatScope(),
- sendAsGroup: chat.chatInfo.groupInfo.map { $0.useRelays && $0.membership.memberRole >= .owner } ?? false,
+ sendAsGroup: chat.chatInfo.sendAsGroup,
live: live,
ttl: ttl,
composedMessages: msgs
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift
index 9279c53c83..21685fccd1 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift
@@ -103,6 +103,10 @@ struct GroupChatInfoView: View {
}
}
+ let showUserSupportChat = groupInfo.membership.memberActive
+ && ((groupInfo.fullGroupPreferences.support.on && groupInfo.membership.memberRole < .moderator)
+ || groupInfo.membership.supportChat != nil)
+
if groupInfo.useRelays {
Section {
// TODO [relays] allow other owners to manage channel link (requires protocol changes to share link ownership)
@@ -124,6 +128,12 @@ struct GroupChatInfoView: View {
if groupInfo.isOwner || members.contains(where: { $0.wrapped.memberRole >= .owner }) {
channelMembersButton()
}
+ if groupInfo.membership.memberRole >= .moderator {
+ memberSupportButton()
+ }
+ if showUserSupportChat {
+ UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
+ }
} footer: {
if !groupInfo.isOwner && groupInfo.groupProfile.publicGroup?.groupLink != nil {
Text("You can share a link or a QR code - anybody will be able to join the channel.")
@@ -141,8 +151,7 @@ struct GroupChatInfoView: View {
if groupInfo.canModerate {
GroupReportsChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
}
- if groupInfo.membership.memberActive
- && (groupInfo.membership.memberRole < .moderator || groupInfo.membership.supportChat != nil) {
+ if showUserSupportChat {
UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
}
} header: {
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
index af7054db01..4dff86f7bb 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
@@ -121,13 +121,15 @@ struct GroupMemberInfoView: View {
}
if connectionLoaded {
+ let showMemberSupportChat = !openedFromSupportChat
+ && groupInfo.membership.memberRole >= .moderator
+ && member.memberRole != .relay
+ && ((groupInfo.fullGroupPreferences.support.on && member.memberRole < .moderator)
+ || member.supportChat != nil)
if member.memberActive {
Section {
- if !openedFromSupportChat
- && groupInfo.membership.memberRole >= .moderator
- && member.memberRole != .relay
- && (member.memberRole < .moderator || member.supportChat != nil) {
+ if showMemberSupportChat {
MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId)
}
if let code = connectionCode,
@@ -142,6 +144,10 @@ struct GroupMemberInfoView: View {
// synchronizeConnectionButtonForce()
// }
}
+ } else if groupInfo.useRelays && member.memberCurrent && showMemberSupportChat {
+ Section {
+ MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId)
+ }
}
if let contactLink = member.contactLink {
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
index 84e852f5a3..cc2feef706 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
@@ -46,13 +46,30 @@ struct GroupPreferencesView: View {
featureSection(.voice, $preferences.voice.enable, $preferences.voice.role)
featureSection(.files, $preferences.files.enable, $preferences.files.role)
featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
- featureSection(.reports, $preferences.reports.enable)
+ featureSection(.reports, $preferences.reports.enable, disabled: true) // enable reports in 7.0 once directory support added
featureSection(.history, $preferences.history.enable)
+ featureSection(.support, $preferences.support.enable, disabled: true)
} else {
featureSection(.timedMessages, $preferences.timedMessages.enable)
featureSection(.fullDelete, $preferences.fullDelete.enable)
featureSection(.reactions, $preferences.reactions.enable)
featureSection(.history, $preferences.history.enable)
+ let supportNotice = NSLocalizedString("Chats with admins in public channels have no E2E encryption - use only with trusted chat relays.", comment: "alert message")
+ featureSection(.support, $preferences.support.enable, notice: supportNotice)
+ .onChange(of: preferences.support.enable) { enable in
+ if enable == .on {
+ showAlert(
+ NSLocalizedString("Enable chats with admins?", comment: "alert title"),
+ message: supportNotice,
+ actions: {[
+ UIAlertAction(title: NSLocalizedString("Enable", comment: "alert button"), style: .destructive) { _ in },
+ UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel) { _ in
+ preferences.support.enable = .off
+ }
+ ]}
+ )
+ }
+ }
}
if groupInfo.isOwner {
@@ -92,7 +109,7 @@ struct GroupPreferencesView: View {
}
}
- private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding, _ enableForRole: Binding? = nil) -> some View {
+ private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding, _ enableForRole: Binding? = nil, disabled: Bool = false, notice: String? = nil) -> some View {
Section {
let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary
let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon
@@ -103,9 +120,9 @@ struct GroupPreferencesView: View {
set: { on, _ in enableFeature.wrappedValue = on ? .on : .off }
)
settingsRow(icon, color: color) {
- Toggle(feature.text, isOn: enable)
+ Toggle(feature.text(isChannel: groupInfo.isChannel), isOn: enable)
}
- .disabled(feature == .reports) // remove in 6.4
+ .disabled(disabled)
if timedOn {
DropdownCustomTimePicker(
selection: $preferences.timedMessages.ttl,
@@ -126,7 +143,7 @@ struct GroupPreferencesView: View {
}
} else {
settingsRow(icon, color: color) {
- infoRow(Text(feature.text), enableFeature.wrappedValue.text)
+ infoRow(Text(feature.text(isChannel: groupInfo.isChannel)), enableFeature.wrappedValue.text)
}
if timedOn {
infoRow("Delete after", timeText(preferences.timedMessages.ttl))
@@ -144,8 +161,11 @@ struct GroupPreferencesView: View {
}
}
} footer: {
- Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner))
- .foregroundColor(theme.colors.secondary)
+ VStack(alignment: .leading) {
+ Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner, isChannel: groupInfo.isChannel))
+ if let notice { Text(notice) }
+ }
+ .foregroundColor(theme.colors.secondary)
}
.onChange(of: enableFeature.wrappedValue) { enabled in
if case .off = enabled {
diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift
index 3dc27c08f6..880933985c 100644
--- a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift
@@ -45,7 +45,7 @@ struct MemberSupportView: View {
: membersWithChats.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
if membersWithChats.isEmpty {
- Text("No chats with members")
+ Text(groupInfo.fullGroupPreferences.support.on ? "No chats with members" : "Chats with members are disabled")
.foregroundColor(.secondary)
} else {
List {
diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift
index 967fedf293..dc4971aafa 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift
@@ -401,6 +401,13 @@ struct ChatListView: View {
.padding(.top, oneHandUI ? 8 : 0)
.id("searchBar")
}
+ if !oneHandUICardShown {
+ OneHandUICard()
+ .padding(.vertical, 6)
+ .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
+ .listRowSeparator(.hidden)
+ .listRowBackground(Color.clear)
+ }
if #available(iOS 16.0, *) {
ForEach(cs, id: \.viewId) { chat in
ChatListNavLink(chat: chat, parentSheet: $sheet)
@@ -420,13 +427,6 @@ struct ChatListView: View {
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
}
}
- if !oneHandUICardShown {
- OneHandUICard()
- .padding(.vertical, 6)
- .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
- .listRowSeparator(.hidden)
- .listRowBackground(Color.clear)
- }
if !addressCreationCardShown && hasConversations {
ConnectBannerCard()
.padding(.vertical, 6)
@@ -839,11 +839,11 @@ struct TagsView: View {
nil
}
let active = tag == selectedPresetTag
- let (icon, text) = presetTagLabel(tag: tag, active: active)
+ let (icon, menuIcon, text) = presetTagLabel(tag: tag, active: active)
let color: Color = active ? .accentColor : .secondary
HStack(spacing: 4) {
- Image(systemName: icon)
+ Image(systemName: menuIcon ?? icon)
.foregroundColor(color)
ZStack {
Text(text).fontWeight(.semibold).foregroundColor(.clear)
@@ -886,9 +886,9 @@ struct TagsView: View {
Button {
setActiveFilter(filter: .presetTag(tag))
} label: {
- let (systemName, text) = presetTagLabel(tag: tag, active: tag == selectedPresetTag)
+ let (icon, _, text) = presetTagLabel(tag: tag, active: tag == selectedPresetTag)
HStack {
- Image(systemName: systemName)
+ Image(systemName: icon)
Text(text)
}
}
@@ -896,8 +896,8 @@ struct TagsView: View {
}
} label: {
if let tag = selectedPresetTag, tag.сollapse {
- let (systemName, _) = presetTagLabel(tag: tag, active: true)
- Image(systemName: systemName)
+ let (icon, menuIcon, _) = presetTagLabel(tag: tag, active: true)
+ Image(systemName: menuIcon ?? icon)
.foregroundColor(.accentColor)
} else {
Image(systemName: "list.bullet")
@@ -907,15 +907,15 @@ struct TagsView: View {
.frame(minWidth: 28)
}
- private func presetTagLabel(tag: PresetTag, active: Bool) -> (String, LocalizedStringKey) {
+ private func presetTagLabel(tag: PresetTag, active: Bool) -> (item: String, menu: String?, label: LocalizedStringKey) {
switch tag {
- case .groupReports: (active ? "flag.fill" : "flag", "Reports")
- case .favorites: (active ? "star.fill" : "star", "Favorites")
- case .contacts: (active ? "person.fill" : "person", "Contacts")
- case .groups: (active ? "person.2.fill" : "person.2", "Groups")
- case .channels: (active ? "antenna.radiowaves.left.and.right.circle.fill" : "antenna.radiowaves.left.and.right.circle", "Channels")
- case .business: (active ? "briefcase.fill" : "briefcase", "Businesses")
- case .notes: (active ? "folder.fill" : "folder", "Notes")
+ case .groupReports: (item: active ? "flag.fill" : "flag", menu: nil, label: "Reports")
+ case .favorites: (item: active ? "star.fill" : "star", menu: nil, label: "Favorites")
+ case .contacts: (item: active ? "person.fill" : "person", menu: nil, label: "Contacts")
+ case .groups: (item: active ? "person.2.fill" : "person.2", menu: nil, label: "Groups")
+ case .channels: (item: active ? "antenna.radiowaves.left.and.right.circle.fill" : "antenna.radiowaves.left.and.right", menu: "antenna.radiowaves.left.and.right", label: "Channels")
+ case .business: (item: active ? "briefcase.fill" : "briefcase", menu: nil, label: "Businesses")
+ case .notes: (item: active ? "folder.fill" : "folder", menu: nil, label: "Notes")
}
}
diff --git a/apps/ios/Shared/Views/ChatList/OneHandUICard.swift b/apps/ios/Shared/Views/ChatList/OneHandUICard.swift
index 059f24cc82..132a19d7e7 100644
--- a/apps/ios/Shared/Views/ChatList/OneHandUICard.swift
+++ b/apps/ios/Shared/Views/ChatList/OneHandUICard.swift
@@ -11,27 +11,46 @@ import SimpleXChat
struct OneHandUICard: View {
@EnvironmentObject var theme: AppTheme
- @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
@AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false
+ @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
@State private var showOneHandUIAlert = false
var body: some View {
- ZStack(alignment: .topTrailing) {
- VStack(alignment: .leading, spacing: 8) {
- Text("Toggle chat list:").font(.title3)
- Toggle("Reachable chat toolbar", isOn: $oneHandUI)
+ HStack(spacing: 2) {
+ segment(
+ icon: "platter.filled.bottom.and.arrow.down.iphone",
+ text: "Bottom bar",
+ isSelected: oneHandUI
+ ) {
+ withAnimation { oneHandUI = true }
}
- Image(systemName: "multiply")
- .foregroundColor(theme.colors.secondary)
- .onTapGesture {
- showOneHandUIAlert = true
+ .background { if oneHandUI { Color(uiColor: .systemGray5) } }
+ .background(ToolbarMaterial.material(toolbarMaterial))
+ ZStack(alignment: .trailing) {
+ segment(
+ icon: "platter.filled.top.and.arrow.up.iphone",
+ text: "Top bar",
+ isSelected: !oneHandUI
+ ) {
+ withAnimation { oneHandUI = false }
}
+ Image(systemName: "multiply")
+ .foregroundColor(theme.colors.secondary)
+ .frame(width: 12, height: 12)
+ .padding(.vertical, 4)
+ .padding(.trailing, 16)
+ .padding(.leading, 4)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ showOneHandUIAlert = true
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background { if !oneHandUI { Color(uiColor: .systemGray5) } }
+ .background(ToolbarMaterial.material(toolbarMaterial))
}
- .padding()
- .background(theme.appColors.sentMessage)
- .cornerRadius(12)
- .frame(height: dynamicSize(userFont).rowHeight)
+ .clipShape(Capsule())
.alert(isPresented: $showOneHandUIAlert) {
Alert(
title: Text("Reachable chat toolbar"),
@@ -44,6 +63,22 @@ struct OneHandUICard: View {
)
}
}
+
+ private func segment(icon: String, text: LocalizedStringKey, isSelected: Bool, action: @escaping () -> Void) -> some View {
+ HStack(spacing: 8) {
+ Image(systemName: icon)
+ .font(.body)
+ .foregroundColor(isSelected ? theme.colors.secondary : theme.colors.primary)
+ Text(text)
+ .font(.subheadline)
+ .foregroundColor(isSelected ? theme.colors.secondary : theme.colors.onBackground)
+ }
+ .padding(.leading, 16)
+ .padding(.vertical, 4)
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
+ .contentShape(Rectangle())
+ .onTapGesture { action() }
+ }
}
#Preview {
diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift
index 76bdc898d5..56e343588d 100644
--- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift
+++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift
@@ -110,8 +110,8 @@ struct MigrateToAppGroupView: View {
do {
resetChatCtrl()
try initializeChat(start: true)
- onboardingStageDefault.set(.step4_SetNotificationsMode)
- chatModel.onboardingStage = .step4_SetNotificationsMode
+ onboardingStageDefault.set(.step4_NetworkCommitments)
+ chatModel.onboardingStage = .step4_NetworkCommitments
setV3DBMigration(.ready)
} catch let error {
dbContainerGroupDefault.set(.documents)
diff --git a/apps/ios/Shared/Views/Helpers/DetermineWidth.swift b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift
index b05ab17089..54e9fe0e80 100644
--- a/apps/ios/Shared/Views/Helpers/DetermineWidth.swift
+++ b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift
@@ -21,6 +21,19 @@ struct DetermineWidth: View {
}
}
+struct DetermineHeight: View {
+ typealias Key = MaximumHeightPreferenceKey
+ var body: some View {
+ GeometryReader { proxy in
+ Color.clear
+ .preference(
+ key: MaximumHeightPreferenceKey.self,
+ value: proxy.size.height
+ )
+ }
+ }
+}
+
struct DetermineWidthImageVideoItem: View {
typealias Key = MaximumWidthImageVideoPreferenceKey
var body: some View {
@@ -41,6 +54,13 @@ struct MaximumWidthPreferenceKey: PreferenceKey {
}
}
+struct MaximumHeightPreferenceKey: PreferenceKey {
+ static var defaultValue: CGFloat = 0
+ static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
+ value = max(value, nextValue())
+ }
+}
+
struct MaximumWidthImageVideoPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift
index 8b982ec0b7..9f2fc833ba 100644
--- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift
+++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift
@@ -86,6 +86,40 @@ func showSheet(
}
}
+func openExternalLink(_ url: URL) {
+ let s = url.absoluteString
+ if s.starts(with: "https://simplex.chat/contact#") || (s.starts(with: "https://smp") && s.contains(".simplex.im/a#")) {
+ ChatModel.shared.appOpenUrl = url
+ } else {
+ showAlert(
+ title: NSLocalizedString("Open external link?", comment: "alert title"),
+ message: s,
+ buttonTitle: NSLocalizedString("Open", comment: "alert button"),
+ buttonAction: { UIApplication.shared.open(url) },
+ cancelButton: true
+ )
+ }
+}
+
+struct ExternalLink: View {
+ let destination: URL
+ let label: Label
+
+ init(destination: URL, @ViewBuilder label: () -> Label) {
+ self.destination = destination
+ self.label = label()
+ }
+
+ init(_ titleKey: LocalizedStringKey, destination: URL) where Label == Text {
+ self.destination = destination
+ self.label = Text(titleKey)
+ }
+
+ var body: some View {
+ Button { openExternalLink(destination) } label: { label }
+ }
+}
+
let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default)
let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel)
diff --git a/apps/ios/Shared/Views/NewChat/AddChannelView.swift b/apps/ios/Shared/Views/NewChat/AddChannelView.swift
index 4e9a42971c..477f7eef8e 100644
--- a/apps/ios/Shared/Views/NewChat/AddChannelView.swift
+++ b/apps/ios/Shared/Views/NewChat/AddChannelView.swift
@@ -10,6 +10,7 @@ import SwiftUI
import SimpleXChat
struct AddChannelView: View {
+ @Environment(\.colorScheme) var colorScheme
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@StateObject private var channelRelaysModel = ChannelRelaysModel.shared
@@ -45,28 +46,39 @@ struct AddChannelView: View {
private func profileStepView() -> some View {
List {
Group {
- ZStack(alignment: .center) {
- ZStack(alignment: .topTrailing) {
- ProfileImage(imageStr: profile.image, iconName: "antenna.radiowaves.left.and.right.circle.fill", size: 128)
- if profile.image != nil {
- Button {
- profile.image = nil
- } label: {
- Image(systemName: "multiply")
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(width: 12)
+ HStack(spacing: 0) {
+ Spacer(minLength: 0)
+ ZStack(alignment: .center) {
+ ZStack(alignment: .topTrailing) {
+ ProfileImage(imageStr: profile.image, iconName: "antenna.radiowaves.left.and.right.circle.fill", size: 128)
+ if profile.image != nil {
+ Button {
+ profile.image = nil
+ } label: {
+ Image(systemName: "multiply")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 12)
+ }
}
}
+ editImageButton { showChooseSource = true }
+ .buttonStyle(BorderlessButtonStyle())
}
- editImageButton { showChooseSource = true }
- .buttonStyle(BorderlessButtonStyle())
+ .padding(.horizontal, 10) // Offsets transparent space built into 3D asset
+ #if SIMPLEX_ASSETS
+ Spacer(minLength: 0)
+ Image(colorScheme == .light ? "create-channel" : "create-channel-light")
+ .resizable()
+ .scaledToFit()
+ .frame(height: 140)
+ #endif
+ Spacer(minLength: 0)
}
- .frame(maxWidth: .infinity, alignment: .center)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
- .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
+ .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0))
Section {
channelNameTextField()
@@ -161,7 +173,10 @@ struct AddChannelView: View {
private func createChannel() {
focusDisplayName = false
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
- profile.groupPreferences = GroupPreferences(history: GroupPreference(enable: .on))
+ profile.groupPreferences = GroupPreferences(
+ history: GroupPreference(enable: .on),
+ support: GroupPreference(enable: .off)
+ )
creationInProgress = true
Task {
do {
@@ -323,24 +338,24 @@ struct AddChannelView: View {
.compactSectionSpacing()
Section {
- Button("Channel link") {
+ Button("Continue") {
if activeCount >= total {
showLinkStep = true
} else if activeCount > 0 {
let actions: [UIAlertAction] = if activeCount + failedCount < total {
[
- UIAlertAction(title: NSLocalizedString("Proceed", comment: "alert action"), style: .default) { _ in showLinkStep = true },
+ UIAlertAction(title: NSLocalizedString("Continue", comment: "alert action"), style: .default) { _ in showLinkStep = true },
UIAlertAction(title: NSLocalizedString("Wait", comment: "alert action"), style: .cancel) { _ in }
]
} else {
[
- UIAlertAction(title: NSLocalizedString("Proceed", comment: "alert action"), style: .default) { _ in showLinkStep = true },
+ UIAlertAction(title: NSLocalizedString("Continue", comment: "alert action"), style: .default) { _ in showLinkStep = true },
cancelAlertAction
]
}
showAlert(
NSLocalizedString("Not all relays connected", comment: "alert title"),
- message: String.localizedStringWithFormat(NSLocalizedString("Channel will start working with %d of %d relays. Proceed?", comment: "alert message"), activeCount, total),
+ message: String.localizedStringWithFormat(NSLocalizedString("Channel will start working with %d of %d relays. Continue?", comment: "alert message"), activeCount, total),
actions: { actions }
)
}
@@ -352,7 +367,12 @@ struct AddChannelView: View {
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
- Button("Cancel") { cancelChannelCreation(gInfo) }
+ Button("Delete channel") { showCancelChannelAlert(gInfo) }
+ }
+ }
+ .onDisappear {
+ if !showLinkStep && m.creatingChannelId == gInfo.id {
+ showCancelChannelAlert(gInfo)
}
}
.onChange(of: channelRelaysModel.groupRelays) { relays in
@@ -414,6 +434,24 @@ struct AddChannelView: View {
}
}
+ private func showCancelChannelAlert(_ gInfo: GroupInfo) {
+ let activeCount = groupRelays.filter { $0.relayStatus == .rsActive && relayMemberConnFailed($0) == nil }.count
+ let total = groupRelays.count
+ showAlert(
+ NSLocalizedString("Cancel creating channel?", comment: "alert title"),
+ message: String.localizedStringWithFormat(
+ NSLocalizedString("Your new channel %@ is connected to %d of %d relays.\nIf you cancel, the channel will be deleted - you can create it again.", comment: "alert message"),
+ gInfo.groupProfile.displayName, activeCount, total
+ ),
+ actions: {[
+ UIAlertAction(title: NSLocalizedString("Wait", comment: "alert action"), style: .cancel) { _ in },
+ UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert action"), style: .destructive) { _ in
+ cancelChannelCreation(gInfo)
+ }
+ ]}
+ )
+ }
+
// MARK: - Helpers
private func showInvalidChannelNameAlert() {
diff --git a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift
index 3a64a955c5..6add190b88 100644
--- a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift
+++ b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift
@@ -26,7 +26,7 @@ struct AddContactLearnMore: View {
VStack(alignment: .leading, spacing: 18) {
Text("To connect, your contact can scan QR code or use the link in the app.")
Text("If you can't meet in person, show QR code in a video call, or share the link.")
- Text("Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).")
+ ExternalLink("Read more in User Guide.", destination: URL(string: "https://simplex.chat/docs/guide/readme.html#connect-to-friends")!)
}
.frame(maxWidth: .infinity, alignment: .leading)
.listRowBackground(Color.clear)
diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift
index 3ce4d1fa40..47afee5f06 100644
--- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift
+++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift
@@ -68,6 +68,7 @@ struct AddGroupView: View {
List {
Group {
HStack(spacing: 0) {
+ Spacer(minLength: 0)
ZStack(alignment: .center) {
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: profile.image, iconName: "person.2.circle.fill", size: 128)
@@ -86,16 +87,16 @@ struct AddGroupView: View {
editImageButton { showChooseSource = true }
.buttonStyle(BorderlessButtonStyle()) // otherwise whole "list row" is clickable
}
- .frame(maxWidth: .infinity)
+ .padding(.horizontal, 10) // Offsets transparent space built into 3D asset
#if SIMPLEX_ASSETS
+ Spacer(minLength: 0)
Image(colorScheme == .light ? "create-group" : "create-group-light")
.resizable()
.scaledToFit()
.frame(height: 140)
- .frame(maxWidth: .infinity)
#endif
+ Spacer(minLength: 0)
}
- .frame(maxWidth: .infinity)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift
index fab8f8a143..9bcc326a66 100644
--- a/apps/ios/Shared/Views/NewChat/NewChatView.swift
+++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift
@@ -338,14 +338,6 @@ private struct InviteView: View {
HStack(spacing: 8) {
let link = connLinkInvitation.simplexChatUri(short: showShortLink)
linkTextView(link)
- Button {
- UIPasteboard.general.string = link
- setInvitationUsed()
- } label: {
- Image(systemName: "doc.on.doc")
- .padding(.top, -7)
- .padding(.horizontal, 8)
- }
Button {
showShareSheet(items: [link])
setInvitationUsed()
diff --git a/apps/ios/Shared/Views/NewChat/OnboardingCards.swift b/apps/ios/Shared/Views/NewChat/OnboardingCards.swift
index 913fdf5577..0a0b3c143d 100644
--- a/apps/ios/Shared/Views/NewChat/OnboardingCards.swift
+++ b/apps/ios/Shared/Views/NewChat/OnboardingCards.swift
@@ -23,17 +23,19 @@ struct OnboardingCardView: View {
let action: () -> Void
static let lightStops: [Gradient.Stop] = [
- .init(color: Color(red: 0.824, green: 0.910, blue: 1.0), location: 0.0),
- .init(color: Color(red: 0.800, green: 0.914, blue: 1.0), location: 0.5),
- .init(color: Color(red: 0.875, green: 1.0, blue: 1.0), location: 0.9),
- .init(color: Color(red: 1.0, green: 0.988, blue: 0.918), location: 1.0)
+ .init(color: oklch(0.9219, 0.0431, 249.4), location: 0.0),
+ .init(color: oklch(0.9198, 0.0471, 240.7), location: 0.5),
+ .init(color: oklch(0.9772, 0.0358, 196.6), location: 0.9),
+ .init(color: oklch(0.9829, 0.0104, 70.0), location: 0.95),
+ .init(color: oklch(0.9886, 0.0272, 99.1), location: 1.0)
]
static let darkStops: [Gradient.Stop] = [
- .init(color: Color(red: 0.016, green: 0.039, blue: 0.141), location: 0.4),
- .init(color: Color(red: 0.220, green: 0.329, blue: 0.671), location: 0.72),
- .init(color: Color(red: 0.659, green: 0.929, blue: 0.953), location: 0.9),
- .init(color: Color(red: 1.0, green: 0.965, blue: 0.878), location: 1.0)
+ .init(color: oklch(0.1578, 0.0609, 267.3), location: 0.4),
+ .init(color: oklch(0.4729, 0.1574, 267.3), location: 0.72),
+ .init(color: oklch(0.9024, 0.0760, 202.8), location: 0.9),
+ .init(color: oklch(0.9384, 0.0354, 65.0), location: 0.95),
+ .init(color: oklch(0.9744, 0.0370, 88.4), location: 1.0)
]
static let gradientAngle: Double = 80.0 * .pi / 180.0
@@ -206,7 +208,7 @@ struct ConnectOnboardingView: View {
.font(.largeTitle)
.bold()
.lineLimit(1)
- .minimumScaleFactor(0.75)
+ .minimumScaleFactor(0.67)
.frame(maxWidth: .infinity, alignment: .center)
if isLandscape {
ZStack(alignment: .leading) {
@@ -277,7 +279,7 @@ struct ConnectOnboardingView: View {
private var connectWithSomeonePage: some View {
GeometryReader { geo in
VStack(spacing: 0) {
- pageHeader("Connect with someone", showBack: true)
+ pageHeader("Create your link", showBack: true)
Spacer(minLength: 16)
diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift
index b5598c1f85..b61b81a46b 100644
--- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift
+++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift
@@ -44,160 +44,147 @@ struct OnboardingButtonStyle: ButtonStyle {
}
}
-private enum OnboardingConditionsViewSheet: Identifiable {
- case showConditions
- case configureOperators
-
- var id: String {
- switch self {
- case .showConditions: return "showConditions"
- case .configureOperators: return "configureOperators"
- }
- }
-}
-
struct OnboardingConditionsView: View {
@EnvironmentObject var theme: AppTheme
- @State private var serverOperators: [ServerOperator] = []
- @State private var selectedOperatorIds = Set()
- @State private var sheetItem: OnboardingConditionsViewSheet? = nil
- @State private var notificationsModeNavLinkActive = false
- @State private var justOpened = true
+ @Environment(\.colorScheme) var colorScheme: ColorScheme
+ @State private var showConditionsSheet = false
+ var selectedOperatorIds: Set
var body: some View {
GeometryReader { g in
- let v = ScrollView {
- VStack(alignment: .leading, spacing: 20) {
- Text("Conditions of use")
- .font(.largeTitle)
- .bold()
- .frame(maxWidth: .infinity, alignment: .center)
- .padding(.top, 25)
+ VStack(alignment: .leading, spacing: 10) {
+ Spacer(minLength: 0)
- Spacer()
+ heroImage().frame(maxWidth: .infinity, minHeight: 80)
- VStack(alignment: .leading, spacing: 20) {
- Text("Private chats, groups and your contacts are not accessible to server operators.")
- .lineSpacing(2)
- .frame(maxWidth: .infinity, alignment: .leading)
- Text("""
- By using SimpleX Chat you agree to:
- - send only legal content in public groups.
- - respect other users – no spam.
- """)
- .lineSpacing(2)
- .frame(maxWidth: .infinity, alignment: .leading)
+ Text("Network commitments")
+ .font(.largeTitle)
+ .bold()
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .fixedSize(horizontal: false, vertical: true)
- Button("Privacy policy and conditions of use.") {
- sheetItem = .showConditions
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- }
- .padding(.horizontal, 4)
+ Text("Operators commit to:\n- Be independent\n- Minimize metadata usage\n- Run verified open-source code")
+ .font(.callout)
+ .lineSpacing(2)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.leading, 4)
+ .padding(.top, 10)
+ .fixedSize(horizontal: false, vertical: true)
- Spacer()
+ Text("You commit to:\n- Only legal content in public groups\n- Respect other users - no spam")
+ .font(.callout)
+ .lineSpacing(2)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.leading, 4)
+ .padding(.top, 10)
+ .fixedSize(horizontal: false, vertical: true)
- VStack(spacing: 12) {
- acceptConditionsButton()
-
- Button("Configure server operators") {
- sheetItem = .configureOperators
- }
- .frame(minHeight: 40)
- }
+ Button {
+ showConditionsSheet = true
+ } label: {
+ Text("Privacy policy and conditions of use.")
+ .fontWeight(.medium)
+ .fixedSize(horizontal: false, vertical: true)
}
- .padding(25)
- .frame(minHeight: g.size.height)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.leading, 4)
+ .padding(.top, 10)
+ .padding(.bottom, 15)
+
+ Spacer(minLength: 0)
+
+ acceptButton()
+ .padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
}
- .onAppear {
- if justOpened {
- serverOperators = ChatModel.shared.conditions.serverOperators
- selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId })
- justOpened = false
+ .padding(.horizontal, 25)
+ .padding(.top, 25)
+ .padding(.bottom, 25)
+ .frame(minHeight: g.size.height)
+ }
+ .frame(maxHeight: .infinity)
+ .navigationBarHidden(true)
+ .sheet(isPresented: $showConditionsSheet) {
+ NavigationView {
+ VStack {
+ ConditionsTextView()
+ .padding()
+ acceptButton()
+ .padding(.horizontal, 25)
+ .padding(.bottom, 20)
}
- }
- .sheet(item: $sheetItem) { item in
- switch item {
- case .showConditions:
- SimpleConditionsView()
- .modifier(ThemedBackground(grouped: true))
- case .configureOperators:
- ChooseServerOperators(serverOperators: serverOperators, selectedOperatorIds: $selectedOperatorIds)
- .modifier(ThemedBackground())
- }
- }
- .frame(maxHeight: .infinity, alignment: .top)
- if #available(iOS 16.4, *) {
- v.scrollBounceBehavior(.basedOnSize)
- } else {
- v
+ .navigationTitle("Conditions of use")
+ .navigationBarTitleDisplayMode(.large)
+ .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) }
+ .modifier(ThemedBackground(grouped: true))
}
}
- .frame(maxHeight: .infinity, alignment: .top)
- .navigationBarHidden(true) // necessary on iOS 15
}
- private func continueToNextStep() {
- onboardingStageDefault.set(.step4_SetNotificationsMode)
- notificationsModeNavLinkActive = true
- }
-
- func notificationsModeNavLinkButton(_ button: @escaping (() -> some View)) -> some View {
+ @ViewBuilder
+ private func heroImage() -> some View {
+ #if SIMPLEX_ASSETS
+ Image(colorScheme == .light ? "network-commitments" : "network-commitments-light")
+ .resizable()
+ .scaledToFit()
+ #else
ZStack {
- button()
-
- NavigationLink(isActive: $notificationsModeNavLinkActive) {
- notificationsModeDestinationView()
- } label: {
- EmptyView()
- }
- .frame(width: 1, height: 1)
- .hidden()
+ let gp = OnboardingCardView.gradientPoints(aspectRatio: 1.5, scale: colorScheme == .light ? 1.2 : 1.5)
+ LinearGradient(
+ stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops,
+ startPoint: gp.start,
+ endPoint: gp.end
+ )
+ Image(systemName: "checkmark.shield")
+ .font(.system(size: 72))
+ .foregroundColor(theme.colors.primary)
}
+ .aspectRatio(1.5, contentMode: .fit)
+ .clipShape(RoundedRectangle(cornerRadius: 24))
+ .padding(.horizontal, 25)
+ #endif
}
- private func notificationsModeDestinationView() -> some View {
- SetNotificationsMode()
- .navigationBarBackButtonHidden(true)
- .modifier(ThemedBackground())
- }
-
- private func acceptConditionsButton() -> some View {
- notificationsModeNavLinkButton {
- Button {
- Task {
- do {
- let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId
- let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: Array(selectedOperatorIds))
+ private func acceptButton() -> some View {
+ Button {
+ Task {
+ do {
+ let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId
+ let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: Array(selectedOperatorIds))
+ await MainActor.run {
+ ChatModel.shared.conditions = r
+ }
+ if let enabledOps = enabledOperators(r.serverOperators) {
+ let r2 = try await setServerOperators(operators: enabledOps)
await MainActor.run {
- ChatModel.shared.conditions = r
+ ChatModel.shared.conditions = r2
+ completeOnboarding()
}
- if let enabledOperators = enabledOperators(r.serverOperators) {
- let r2 = try await setServerOperators(operators: enabledOperators)
- await MainActor.run {
- ChatModel.shared.conditions = r2
- continueToNextStep()
- }
- } else {
- await MainActor.run {
- continueToNextStep()
- }
- }
- } catch let error {
+ } else {
await MainActor.run {
- showAlert(
- NSLocalizedString("Error accepting conditions", comment: "alert title"),
- message: responseError(error)
- )
+ completeOnboarding()
}
}
+ } catch let error {
+ await MainActor.run {
+ showAlert(
+ NSLocalizedString("Error accepting conditions", comment: "alert title"),
+ message: responseError(error)
+ )
+ }
}
- } label: {
- Text("Accept")
}
- .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
- .disabled(selectedOperatorIds.isEmpty)
+ } label: {
+ Text("Accept")
}
+ .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
+ .disabled(selectedOperatorIds.isEmpty)
+ }
+
+ private func completeOnboarding() {
+ let m = ChatModel.shared
+ onboardingStageDefault.set(.onboardingComplete)
+ m.onboardingStage = .onboardingComplete
}
private func enabledOperators(_ operators: [ServerOperator]) -> [ServerOperator]? {
@@ -222,7 +209,7 @@ struct OnboardingConditionsView: View {
if !haveXFTPProxy { op.xftpRoles.proxy = true }
ops[firstEnabledIndex] = op
return ops
- } else { // Shouldn't happen - view doesn't let to proceed if no operators are enabled
+ } else {
return nil
}
} else {
@@ -405,5 +392,5 @@ struct ChooseServerOperatorsInfoView: View {
}
#Preview {
- OnboardingConditionsView()
+ OnboardingConditionsView(selectedOperatorIds: [])
}
diff --git a/apps/ios/Shared/Views/Onboarding/ConnectBannerCard.swift b/apps/ios/Shared/Views/Onboarding/ConnectBannerCard.swift
index 460ab9b141..87f66a72bb 100644
--- a/apps/ios/Shared/Views/Onboarding/ConnectBannerCard.swift
+++ b/apps/ios/Shared/Views/Onboarding/ConnectBannerCard.swift
@@ -83,6 +83,7 @@ struct ConnectBannerCard: View {
.lineLimit(1)
.minimumScaleFactor(0.75)
}
+ .frame(height: 20)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
.background(ToolbarMaterial.material(toolbarMaterial))
diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift
index 7301c0421d..3c33546436 100644
--- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift
+++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift
@@ -29,45 +29,82 @@ enum UserProfileAlert: Identifiable {
let MAX_BIO_LENGTH_BYTES = 160
struct CreateProfile: View {
+ @Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
@EnvironmentObject var theme: AppTheme
@State private var displayName: String = ""
@State private var profileBio: String = ""
@FocusState private var focusDisplayName
@State private var alert: UserProfileAlert?
+ @State private var showChooseSource = false
+ @State private var showImagePicker = false
+ @State private var showTakePhoto = false
+ @State private var chosenImage: UIImage? = nil
+ @State private var profileImage: String? = nil
var body: some View {
List {
+ Group {
+ HStack(spacing: 0) {
+ Spacer(minLength: 0)
+ ZStack(alignment: .center) {
+ ZStack(alignment: .topTrailing) {
+ ProfileImage(imageStr: profileImage, size: 128)
+ if profileImage != nil {
+ Button {
+ profileImage = nil
+ } label: {
+ Image(systemName: "multiply")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 12)
+ }
+ }
+ }
+
+ editImageButton { showChooseSource = true }
+ .buttonStyle(BorderlessButtonStyle())
+ }
+ .padding(.horizontal, 10) // Offsets transparent space built into 3D asset
+ Spacer(minLength: 0)
+ #if SIMPLEX_ASSETS
+ Image(colorScheme == .light ? "create-profile" : "create-profile-light")
+ .resizable()
+ .scaledToFit()
+ .frame(height: 140)
+ // No trailing spacer — asset image has empty space on the right
+ #endif
+ }
+ }
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0))
+
Section {
- TextField("Enter your name…", text: $displayName)
- .focused($focusDisplayName)
- TextField("Bio", text: $profileBio)
- Button {
- createProfile()
- } label: {
- Label("Create profile", systemImage: "checkmark")
+ ZStack(alignment: .leading) {
+ let name = displayName.trimmingCharacters(in: .whitespaces)
+ if name != mkValidName(name) {
+ Button {
+ alert = .invalidNameError(validName: mkValidName(name))
+ } label: {
+ Image(systemName: "exclamationmark.circle").foregroundColor(.red)
+ }
+ } else {
+ Image(systemName: "pencil").foregroundColor(theme.colors.secondary)
+ }
+ TextField("Enter your name…", text: $displayName)
+ .padding(.leading, 36)
+ .focused($focusDisplayName)
+ }
+ ZStack(alignment: .leading) {
+ Image(systemName: "pencil").foregroundColor(theme.colors.secondary)
+ TextField("Bio", text: $profileBio)
+ .padding(.leading, 36)
+ }
+ Button(action: createProfile) {
+ settingsRow("checkmark", color: theme.colors.primary) { Text("Create profile") }
}
.disabled(!canCreateProfile(displayName) || !bioFitsLimit())
- } header: {
- HStack {
- Text("Your profile")
- .foregroundColor(theme.colors.secondary)
-
- let name = displayName.trimmingCharacters(in: .whitespaces)
- let validName = mkValidName(name)
- if name != validName {
- Spacer()
- validationErrorIndicator {
- alert = .invalidNameError(validName: validName)
- }
- } else if !bioFitsLimit() {
- Spacer()
- validationErrorIndicator {
- showAlert(NSLocalizedString("Bio too large", comment: "alert title"))
- }
- }
- }
- .frame(height: 20)
} footer: {
VStack(alignment: .leading, spacing: 8) {
Text("Your profile is stored on your device and only shared with your contacts.")
@@ -75,10 +112,42 @@ struct CreateProfile: View {
.foregroundColor(theme.colors.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
+ .compactSectionSpacing()
}
.navigationTitle("Create your profile")
.modifier(ThemedBackground(grouped: true))
.alert(item: $alert) { a in userProfileAlert(a, $displayName) }
+ .confirmationDialog("Profile image", isPresented: $showChooseSource, titleVisibility: .visible) {
+ Button("Take picture") {
+ showTakePhoto = true
+ }
+ Button("Choose from library") {
+ showImagePicker = true
+ }
+ }
+ .fullScreenCover(isPresented: $showTakePhoto) {
+ ZStack {
+ Color.black.edgesIgnoringSafeArea(.all)
+ CameraImagePicker(image: $chosenImage)
+ }
+ }
+ .sheet(isPresented: $showImagePicker) {
+ LibraryImagePicker(image: $chosenImage) { _ in
+ await MainActor.run {
+ showImagePicker = false
+ }
+ }
+ }
+ .onChange(of: chosenImage) { image in
+ Task {
+ let resized: String? = if let image {
+ await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
+ } else {
+ nil
+ }
+ await MainActor.run { profileImage = resized }
+ }
+ }
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
focusDisplayName = true
@@ -86,14 +155,6 @@ struct CreateProfile: View {
}
}
- private func validationErrorIndicator(_ onTap: @escaping () -> Void) -> some View {
- Image(systemName: "exclamationmark.circle")
- .foregroundColor(.red)
- .onTapGesture {
- onTap()
- }
- }
-
private func bioFitsLimit() -> Bool {
chatJsonLength(profileBio) <= MAX_BIO_LENGTH_BYTES
}
@@ -104,7 +165,8 @@ struct CreateProfile: View {
let profile = Profile(
displayName: displayName.trimmingCharacters(in: .whitespaces),
fullName: "",
- shortDescr: shortDescr
+ shortDescr: shortDescr,
+ image: profileImage
)
let m = ChatModel.shared
do {
@@ -133,61 +195,103 @@ struct CreateProfile: View {
struct CreateFirstProfile: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
- @Environment(\.dismiss) var dismiss
+ @Environment(\.colorScheme) var colorScheme: ColorScheme
@State private var displayName: String = ""
@FocusState private var focusDisplayName
@State private var nextStepNavLinkActive = false
-
+ @State private var showMigrateSheet = false
var body: some View {
- let v = VStack(alignment: .leading, spacing: 16) {
- VStack(alignment: .center, spacing: 16) {
- Text("Create profile")
- .font(.largeTitle)
- .bold()
- .multilineTextAlignment(.center)
-
- 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)
- let validName = mkValidName(name)
- ZStack(alignment: .trailing) {
- TextField("Enter your name…", text: $displayName)
- .focused($focusDisplayName)
- .padding(.horizontal)
- .padding(.trailing, 20)
- .padding(.vertical, 10)
- .background(
- RoundedRectangle(cornerRadius: 10, style: .continuous)
- .fill(Color(uiColor: .tertiarySystemFill))
+ let spacing: CGFloat = 10
+ let topPadding: CGFloat = 8
+ let padding: CGFloat = 25
+ GeometryReader { g in
+ let v = ScrollView {
+ VStack(alignment: .center, spacing: spacing) {
+ #if SIMPLEX_ASSETS
+ Image(colorScheme == .light ? "your-profile" : "your-profile-light")
+ .resizable()
+ .scaledToFit()
+ .frame(maxWidth: .infinity)
+ #else
+ ZStack {
+ let gp = OnboardingCardView.gradientPoints(aspectRatio: 1.0, scale: colorScheme == .light ? 1.2 : 1.5)
+ LinearGradient(
+ stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops,
+ startPoint: gp.start,
+ endPoint: gp.end
)
- if name != validName {
- Button {
- showAlert(.invalidNameError(validName: validName))
- } label: {
- Image(systemName: "exclamationmark.circle")
- .foregroundColor(.red)
- .padding(.horizontal, 10)
- }
+ Image(systemName: "person.crop.rectangle")
+ .font(.system(size: 72))
+ .foregroundColor(theme.colors.primary)
}
+ .aspectRatio(1.0, contentMode: .fit)
+ .clipShape(RoundedRectangle(cornerRadius: 24))
+ .padding(.horizontal, 25)
+ .frame(maxWidth: .infinity)
+ #endif
+
+ Text("Your profile")
+ .font(.largeTitle)
+ .bold()
+ .multilineTextAlignment(.center)
+ .fixedSize(horizontal: false, vertical: true)
+
+ Text("On your phone, not on servers.")
+ .font(.title3)
+ .fontWeight(.medium)
+ .foregroundColor(theme.colors.secondary)
+ .multilineTextAlignment(.center)
+ .fixedSize(horizontal: false, vertical: true)
+
+ Text("No account. No phone. No email. No ID.\nThe most secure encryption.")
+ .font(.footnote)
+ .foregroundColor(theme.colors.secondary)
+ .multilineTextAlignment(.center)
+ .fixedSize(horizontal: false, vertical: true)
+
+ profileNameField()
+ .padding(.top)
+ .padding(.bottom, 5)
+
+ Spacer(minLength: 0)
+
+ createProfileButton()
+ .padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
+ }
+ .padding(.horizontal, padding)
+ .padding(.top, topPadding)
+ .padding(.bottom, padding)
+ .frame(minHeight: g.size.height)
+ }
+ .onTapGesture { focusDisplayName = false }
+ .sheet(isPresented: $showMigrateSheet, onDismiss: { m.migrationState = nil }) {
+ NavigationView {
+ MigrateToDevice(migrationState: $m.migrationState)
+ .navigationTitle("Migrate here")
+ .modifier(ThemedBackground(grouped: true))
}
}
- .padding(.top)
-
- Spacer()
-
- VStack(spacing: 10) {
- createProfileButton()
- if !focusDisplayName {
- onboardingButtonPlaceholder()
+ if #available(iOS 17, *) {
+ v.scrollBounceBehavior(.basedOnSize).defaultScrollAnchor(.bottom)
+ } else if #available(iOS 16.4, *) {
+ v.scrollBounceBehavior(.basedOnSize)
+ } else {
+ v
+ }
+ }
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button {
+ if m.migrationState == nil {
+ m.migrationState = .pasteOrScanLink
+ }
+ showMigrateSheet = true
+ } label: {
+ HStack(spacing: 4) {
+ Image(systemName: "tray.and.arrow.down")
+ Text("Migrate")
+ .fontWeight(.medium)
+ }
}
}
}
@@ -195,23 +299,40 @@ struct CreateFirstProfile: View {
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(.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)
+ .frame(maxHeight: .infinity)
+ }
+
+ private func profileNameField() -> some View {
+ let name = displayName.trimmingCharacters(in: .whitespaces)
+ let validName = mkValidName(name)
+ return ZStack(alignment: .trailing) {
+ TextField("Enter profile name...", text: $displayName)
+ .focused($focusDisplayName)
+ .padding(.horizontal)
+ .padding(.trailing, name != validName ? 20 : 0)
+ .padding(.vertical, 10)
+ .background(
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
+ .fill(Color(uiColor: .tertiarySystemFill))
+ )
+ if name != validName {
+ Button {
+ showAlert(.invalidNameError(validName: validName))
+ } label: {
+ Image(systemName: "exclamationmark.circle")
+ .foregroundColor(.red)
+ .padding(.horizontal, 10)
+ }
+ }
}
}
- func createProfileButton() -> some View {
+ private func createProfileButton() -> some View {
ZStack {
Button {
createProfile()
@@ -236,7 +357,7 @@ struct CreateFirstProfile: View {
}
private func nextStepDestinationView() -> some View {
- OnboardingConditionsView()
+ YourNetworkView()
.navigationBarBackButtonHidden(true)
.modifier(ThemedBackground())
}
diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift
index 263b55a42d..e9b9c6b970 100644
--- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift
+++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift
@@ -9,7 +9,7 @@
import SwiftUI
-struct HowItWorks: View {
+struct OldHowItWorks: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var m: ChatModel
var onboarding: Bool
@@ -28,7 +28,7 @@ struct HowItWorks: View {
Text("Only client devices store user profiles, contacts, groups, and messages.")
Text("All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.")
if !onboarding {
- Text("Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).")
+ ExternalLink("Read more in our GitHub repository.", destination: URL(string: "https://github.com/simplex-chat/simplex-chat#readme")!)
}
}
.padding(.bottom)
@@ -61,9 +61,57 @@ struct HowItWorks: View {
}
}
-struct HowItWorks_Previews: PreviewProvider {
+struct WhySimpleX: View {
+ @Environment(\.dismiss) var dismiss: DismissAction
+ @EnvironmentObject var m: ChatModel
+ var onboarding: Bool
+ @Binding var createProfileNavLinkActive: Bool
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 16) {
+ Text("You were born without an account")
+ .font(.title)
+ .bold()
+ .padding(.top)
+ Text("Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life.")
+ Text("Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible.")
+ Text("There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected.")
+ Text("Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign.")
+ Text("Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public.")
+ Text("The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it.")
+ Text("Because we destroyed the power to know who you are. So that your power can never be taken.")
+ Text("Be free in your network.")
+ }
+ }
+ .padding(.bottom, 16)
+
+ Spacer()
+
+ if onboarding {
+ createFirstProfileButton()
+ }
+ }
+ .padding(onboarding ? 25 : 16)
+ .frame(maxHeight: .infinity, alignment: .top)
+ .modifier(ThemedBackground())
+ }
+
+ private func createFirstProfileButton() -> some View {
+ Button {
+ dismiss()
+ createProfileNavLinkActive = true
+ } label: {
+ Text("Get started")
+ }
+ .buttonStyle(OnboardingButtonStyle(isDisabled: false))
+ }
+}
+
+struct WhySimpleX_Previews: PreviewProvider {
static var previews: some View {
- HowItWorks(
+ WhySimpleX(
onboarding: true,
createProfileNavLinkActive: Binding.constant(false)
)
diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift
index daef95fbc6..39ccabce04 100644
--- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift
+++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift
@@ -19,17 +19,18 @@ struct OnboardingView: View {
case .step1_SimpleXInfo:
SimpleXInfo(onboarding: true)
.modifier(ThemedBackground())
- case .step2_CreateProfile: // deprecated
+ case .step2_CreateProfile:
CreateFirstProfile()
.modifier(ThemedBackground())
case .step3_CreateSimpleXAddress: // deprecated
CreateSimpleXAddress()
- case .step3_ChooseServerOperators:
- OnboardingConditionsView()
+ case .step3_ChooseServerOperators,
+ .step4_SetNotificationsMode: // deprecated
+ YourNetworkView()
.navigationBarBackButtonHidden(true)
.modifier(ThemedBackground())
- case .step4_SetNotificationsMode:
- SetNotificationsMode()
+ case .step4_NetworkCommitments:
+ OnboardingConditionsView(selectedOperatorIds: Set(ChatModel.shared.conditions.serverOperators.filter { $0.enabled }.map { $0.operatorId }))
.navigationBarBackButtonHidden(true)
.modifier(ThemedBackground())
case .onboardingComplete: EmptyView()
@@ -45,10 +46,11 @@ func onboardingButtonPlaceholder() -> some View {
// Spec: spec/client/navigation.md#onboardingStage
enum OnboardingStage: String, Identifiable {
case step1_SimpleXInfo
- case step2_CreateProfile // deprecated
+ case step2_CreateProfile
case step3_CreateSimpleXAddress // deprecated
- case step3_ChooseServerOperators // changed to simplified conditions
- case step4_SetNotificationsMode
+ case step3_ChooseServerOperators
+ case step4_SetNotificationsMode // deprecated
+ case step4_NetworkCommitments
case onboardingComplete
public var id: Self { self }
diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift
index 717405b03b..1a1f1bb68c 100644
--- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift
+++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift
@@ -11,45 +11,39 @@ import SwiftUI
import SimpleXChat
struct SetNotificationsMode: View {
- @EnvironmentObject var m: ChatModel
- @State private var notificationMode = NotificationsMode.instant
- @State private var showAlert: NotificationAlert?
- @State private var showInfo: Bool = false
+ @Environment(\.dismiss) var dismiss
+ @Binding var notificationMode: NotificationsMode
+ @State private var showInfo = false
var body: some View {
GeometryReader { g in
- let v = ScrollView {
+ ScrollView {
VStack(alignment: .center, spacing: 20) {
Text("Push notifications")
.font(.largeTitle)
.bold()
.padding(.top, 25)
-
- infoText()
-
+
+ Button {
+ showInfo = true
+ } label: {
+ Label("How it affects privacy", systemImage: "info.circle")
+ .font(.headline)
+ }
+
Spacer()
ForEach(NotificationsMode.values) { mode in
NtfModeSelector(mode: mode, selection: $notificationMode)
}
-
+
Spacer()
-
+
VStack(spacing: 10) {
Button {
- if let token = m.deviceToken {
- setNotificationsMode(token, notificationMode)
- } else {
- AlertManager.shared.showAlertMsg(title: "No device token!")
- }
- onboardingStageDefault.set(.onboardingComplete)
- m.onboardingStage = .onboardingComplete
+ dismiss()
} label: {
- if case .off = notificationMode {
- Text("Use chat")
- } else {
- Text("Enable notifications")
- }
+ Text("OK")
}
.buttonStyle(OnboardingButtonStyle())
onboardingButtonPlaceholder()
@@ -58,50 +52,11 @@ 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) {
- switch mode {
- case .off:
- m.tokenStatus = .new
- m.notificationMode = .off
- default:
- Task {
- do {
- let status = try await apiRegisterToken(token: token, notificationMode: mode)
- await MainActor.run {
- m.tokenStatus = status
- m.notificationMode = mode
- }
- } catch let error {
- let a = getErrorAlert(error, "Error enabling notifications")
- AlertManager.shared.showAlertMsg(
- title: a.title,
- message: a.message
- )
- }
- }
- }
- }
-
- private func infoText() -> some View {
- Button {
- showInfo = true
- } label: {
- Label("How it affects privacy", systemImage: "info.circle")
- .font(.headline)
- }
}
}
@@ -180,6 +135,6 @@ struct NotificationsInfoView: View {
struct NotificationsModeView_Previews: PreviewProvider {
static var previews: some View {
- SetNotificationsMode()
+ SetNotificationsMode(notificationMode: .constant(.instant))
}
}
diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift
index 80f35c1190..15b8e05b5e 100644
--- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift
+++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift
@@ -12,68 +12,84 @@ import SimpleXChat
struct SimpleXInfo: View {
@EnvironmentObject var m: ChatModel
+ @EnvironmentObject var theme: AppTheme
@Environment(\.colorScheme) var colorScheme: ColorScheme
- @State private var showHowItWorks = false
+ @State private var showWhyBuilt = false
@State private var createProfileNavLinkActive = false
var onboarding: Bool
var body: some View {
GeometryReader { g in
- let v = ScrollView {
- VStack(alignment: .leading) {
- VStack(alignment: .center, spacing: 10) {
- Image(colorScheme == .light ? "logo" : "logo-light")
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(width: g.size.width * 0.67)
- .padding(.bottom, 8)
- .padding(.leading, 4)
- .frame(maxWidth: .infinity, minHeight: 48, alignment: .top)
-
- Button {
- showHowItWorks = true
- } label: {
- Label("The future of messaging", systemImage: "info.circle")
- .font(.headline)
- }
- }
+ VStack(alignment: .center, spacing: 10) {
+ Image(colorScheme == .light ? "logo" : "logo-light")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: (g.size.width - 50) * 0.55)
+ .padding(.leading, 4)
+ .frame(maxWidth: .infinity, minHeight: 48, alignment: .top)
- Spacer()
+ #if SIMPLEX_ASSETS
+ Image(colorScheme == .light ? "intro" : "intro-light")
+ .resizable()
+ .scaledToFit()
+ .frame(maxWidth: .infinity)
+ #else
+ ZStack {
+ let gp = OnboardingCardView.gradientPoints(aspectRatio: 1.0, scale: colorScheme == .light ? 1.2 : 1.5)
+ LinearGradient(
+ stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops,
+ startPoint: gp.start,
+ endPoint: gp.end
+ )
+ Image(systemName: "bubble.left.and.bubble.right")
+ .font(.system(size: 72))
+ .foregroundColor(theme.colors.primary)
+ }
+ .aspectRatio(1.0, contentMode: .fit)
+ .clipShape(RoundedRectangle(cornerRadius: 24))
+ .padding(.horizontal, 25)
+ .frame(maxWidth: .infinity)
+ #endif
- VStack(alignment: .leading) {
- onboardingInfoRow("privacy", "Privacy redefined",
- "No user identifiers.", width: 48)
- onboardingInfoRow("shield", "Immune to spam",
- "You decide who can connect.", width: 46)
- onboardingInfoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized",
- "Anybody can host servers.", width: 46)
- }
- .padding(.leading, 16)
+ Text("Be free\nin your network")
+ .font(.largeTitle)
+ .bold()
+ .multilineTextAlignment(.center)
+ .fixedSize(horizontal: false, vertical: true)
- Spacer()
+ Text("Private and secure messaging.")
+ .font(.title3)
+ .fontWeight(.medium)
+ .foregroundColor(theme.colors.secondary)
+ .multilineTextAlignment(.center)
+ .fixedSize(horizontal: false, vertical: true)
- if onboarding {
- VStack(spacing: 10) {
- createFirstProfileButton()
+ Text("The first network where you own\nyour contacts and groups.")
+ .font(.footnote)
+ .foregroundColor(theme.colors.secondary)
+ .multilineTextAlignment(.center)
+ .fixedSize(horizontal: false, vertical: true)
- Button {
- m.migrationState = .pasteOrScanLink
- } label: {
- Label("Migrate from another device", systemImage: "tray.and.arrow.down")
- .font(.system(size: 17, weight: .semibold))
- .frame(minHeight: 40)
- }
- .frame(maxWidth: .infinity)
- }
+ if onboarding {
+ Spacer(minLength: 0)
+
+ createFirstProfileButton()
+ .padding(.vertical, 10)
+
+ Button {
+ showWhyBuilt = true
+ } label: {
+ Label("Why SimpleX is built.", systemImage: "info.circle")
+ .font(.headline)
}
}
- .padding(.horizontal, 25)
- .padding(.top, 75)
- .padding(.bottom, 25)
- .frame(minHeight: g.size.height)
}
+ .padding(.horizontal, 25)
+ .padding(.top, 28)
+ .padding(.bottom, 20)
+ .frame(minHeight: g.size.height)
.sheet(isPresented: Binding(
- get: { m.migrationState != nil },
+ get: { m.migrationState != nil && !createProfileNavLinkActive },
set: { _ in
m.migrationState = nil
MigrationToDeviceState.save(nil) }
@@ -86,17 +102,12 @@ struct SimpleXInfo: View {
.modifier(ThemedBackground(grouped: true))
}
}
- .sheet(isPresented: $showHowItWorks) {
- HowItWorks(
+ .sheet(isPresented: $showWhyBuilt) {
+ WhySimpleX(
onboarding: onboarding,
createProfileNavLinkActive: $createProfileNavLinkActive
)
}
- if #available(iOS 16.4, *) {
- v.scrollBounceBehavior(.basedOnSize)
- } else {
- v
- }
}
.onAppear() {
setLastVersionDefault()
@@ -105,32 +116,12 @@ struct SimpleXInfo: View {
.navigationBarHidden(true) // necessary on iOS 15
}
- private func onboardingInfoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View {
- HStack(alignment: .top) {
- Image(image)
- .resizable()
- .scaledToFit()
- .frame(width: width, height: 54)
- .frame(width: 54)
- .padding(.trailing, 10)
- VStack(alignment: .leading, spacing: 4) {
- Text(title).font(.headline)
- Text(text).frame(minHeight: 40, alignment: .top)
- .font(.callout)
- .lineLimit(3)
- .fixedSize(horizontal: false, vertical: true)
- }
- .padding(.top, 4)
- }
- .padding(.bottom, 12)
- }
-
private func createFirstProfileButton() -> some View {
ZStack {
Button {
createProfileNavLinkActive = true
} label: {
- Text("Create your profile")
+ Text("Get started")
}
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
index b7249f42ea..41a342d7c8 100644
--- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
+++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift
@@ -634,7 +634,7 @@ private let versionDescriptions: [VersionDescription] = [
),
VersionDescription(
version: "v6.5",
- post: URL(string: "https://simplex.chat/blog/20260428-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html"),
+ post: URL(string: "https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html"),
features: [
.feature(Description(
icon: nil,
@@ -791,7 +791,7 @@ struct WhatsNewView: View {
}
}
if let post = v.post {
- Link(destination: post) {
+ ExternalLink(destination: post) {
HStack {
Text("Read more")
Image(systemName: "arrow.up.right.circle")
diff --git a/apps/ios/Shared/Views/Onboarding/YourNetwork.swift b/apps/ios/Shared/Views/Onboarding/YourNetwork.swift
new file mode 100644
index 0000000000..d3727e196e
--- /dev/null
+++ b/apps/ios/Shared/Views/Onboarding/YourNetwork.swift
@@ -0,0 +1,193 @@
+//
+// YourNetwork.swift
+// SimpleX (iOS)
+//
+// Created by Evgeny on 22/04/2026.
+// Copyright © 2026 SimpleX Chat. All rights reserved.
+//
+
+import SwiftUI
+import SimpleXChat
+
+private enum YourNetworkSheet: Identifiable {
+ case configureOperators
+ case configureNotifications
+
+ var id: String {
+ switch self {
+ case .configureOperators: return "configureOperators"
+ case .configureNotifications: return "configureNotifications"
+ }
+ }
+}
+
+struct YourNetworkView: View {
+ @EnvironmentObject var theme: AppTheme
+ @Environment(\.colorScheme) var colorScheme: ColorScheme
+ @State private var serverOperators: [ServerOperator] = []
+ @State private var selectedOperatorIds = Set()
+ @State private var notificationMode: NotificationsMode = .instant
+ @State private var sheetItem: YourNetworkSheet? = nil
+ @State private var nextStepNavLinkActive = false
+ @State private var justOpened = true
+
+ var body: some View {
+ GeometryReader { g in
+ VStack(alignment: .center, spacing: 10) {
+ Spacer(minLength: 0)
+
+ #if SIMPLEX_ASSETS
+ Image(colorScheme == .light ? "your-network" : "your-network-light")
+ .resizable()
+ .scaledToFit()
+ .frame(maxWidth: .infinity)
+ #else
+ ZStack {
+ let gp = OnboardingCardView.gradientPoints(aspectRatio: 1.0, scale: colorScheme == .light ? 1.2 : 1.5)
+ LinearGradient(
+ stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops,
+ startPoint: gp.start,
+ endPoint: gp.end
+ )
+ Image(systemName: "network")
+ .font(.system(size: 72))
+ .foregroundColor(theme.colors.primary)
+ }
+ .aspectRatio(1.0, contentMode: .fit)
+ .clipShape(RoundedRectangle(cornerRadius: 24))
+ .padding(.horizontal, 25)
+ .frame(maxWidth: .infinity)
+ #endif
+
+ Text("Your network")
+ .font(.largeTitle)
+ .bold()
+ .multilineTextAlignment(.center)
+ .fixedSize(horizontal: false, vertical: true)
+ .padding(.top, 15)
+
+ Text("Network routers cannot know\nwho talks to whom")
+ .font(.title3)
+ .fontWeight(.medium)
+ .foregroundColor(theme.colors.secondary)
+ .multilineTextAlignment(.center)
+ .fixedSize(horizontal: false, vertical: true)
+
+ VStack(alignment: .leading, spacing: 20) {
+ configureRoutersButton()
+ configureNotificationsButton()
+ }
+ .padding(.top, 15)
+ .padding(.bottom, 15)
+
+ Spacer(minLength: 0)
+
+ continueButton()
+ .padding(.bottom, g.safeAreaInsets.bottom == 0 ? 20 : 0)
+ }
+ .padding(.horizontal, 25)
+ .padding(.top, 8)
+ .padding(.bottom, 20)
+ .frame(minHeight: g.size.height)
+ }
+ .onAppear {
+ if justOpened {
+ serverOperators = ChatModel.shared.conditions.serverOperators
+ selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId })
+ justOpened = false
+ }
+ }
+ .sheet(item: $sheetItem) { item in
+ switch item {
+ case .configureOperators:
+ ChooseServerOperators(serverOperators: serverOperators, selectedOperatorIds: $selectedOperatorIds)
+ .modifier(ThemedBackground())
+ case .configureNotifications:
+ SetNotificationsMode(notificationMode: $notificationMode)
+ .modifier(ThemedBackground())
+ }
+ }
+ .frame(maxHeight: .infinity)
+ .navigationBarHidden(true)
+ }
+
+ private func configureRoutersButton() -> some View {
+ Button {
+ sheetItem = .configureOperators
+ } label: {
+ HStack(spacing: 6) {
+ Text("Setup routers")
+ .fontWeight(.medium)
+ ForEach(serverOperators.reversed()) { op in
+ Image(op.logo(colorScheme))
+ .resizable()
+ .scaledToFit()
+ .frame(width: 22, height: 22)
+ .grayscale(selectedOperatorIds.contains(op.operatorId) ? 0.0 : 1.0)
+ }
+ }
+ }
+ }
+
+ private func configureNotificationsButton() -> some View {
+ Button {
+ sheetItem = .configureNotifications
+ } label: {
+ HStack(spacing: 4) {
+ Text("Setup notifications")
+ .fontWeight(.medium)
+ Image(systemName: notificationMode.icon)
+ }
+ }
+ }
+
+ private func continueButton() -> some View {
+ ZStack {
+ Button {
+ applyNotificationMode()
+ onboardingStageDefault.set(.step4_NetworkCommitments)
+ nextStepNavLinkActive = true
+ } label: {
+ Text("Continue")
+ }
+ .buttonStyle(OnboardingButtonStyle())
+
+ NavigationLink(isActive: $nextStepNavLinkActive) {
+ OnboardingConditionsView(selectedOperatorIds: selectedOperatorIds)
+ .navigationBarBackButtonHidden(true)
+ .modifier(ThemedBackground())
+ } label: {
+ EmptyView()
+ }
+ .frame(width: 1, height: 1)
+ .hidden()
+ }
+ }
+
+ private func applyNotificationMode() {
+ let m = ChatModel.shared
+ if let token = m.deviceToken {
+ switch notificationMode {
+ case .off:
+ m.tokenStatus = .new
+ m.notificationMode = .off
+ default:
+ Task {
+ do {
+ let status = try await apiRegisterToken(token: token, notificationMode: notificationMode)
+ await MainActor.run {
+ m.tokenStatus = status
+ m.notificationMode = notificationMode
+ }
+ } catch let error {
+ let a = getErrorAlert(error, "Error enabling notifications")
+ AlertManager.shared.showAlertMsg(
+ title: a.title,
+ message: a.message
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
index 184b03e679..a504b00116 100644
--- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
+++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
@@ -22,14 +22,16 @@ struct DeveloperView: View {
VStack {
List {
Section {
- ZStack(alignment: .leading) {
- Image(colorScheme == .dark ? "github_light" : "github")
- .resizable()
- .frame(width: 24, height: 24)
- .opacity(0.5)
- .colorMultiply(theme.colors.secondary)
- Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)")
- .padding(.leading, 36)
+ ExternalLink(destination: URL(string: "https://github.com/simplex-chat/simplex-chat")!) {
+ ZStack(alignment: .leading) {
+ Image(colorScheme == .dark ? "github_light" : "github")
+ .resizable()
+ .frame(width: 24, height: 24)
+ .opacity(0.5)
+ .colorMultiply(theme.colors.secondary)
+ Text("Install SimpleX Chat for terminal")
+ .padding(.leading, 36)
+ }
}
NavigationLink {
TerminalView()
diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift
index d9862aaac8..f74516c2c8 100644
--- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift
+++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift
@@ -23,7 +23,7 @@ struct IncognitoHelp: View {
Text("Incognito mode protects your privacy by using a new random profile for each contact.")
Text("It allows having many anonymous connections without any shared data between them in a single chat profile.")
Text("When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.")
- Text("Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).")
+ ExternalLink("Read more in User Guide.", destination: URL(string: "https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode")!)
}
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift
index 6f76e69182..5abbbf8d2e 100644
--- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift
+++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift
@@ -71,11 +71,7 @@ struct ConditionsWebView: UIViewRepresentable {
switch navigationAction.navigationType {
case .linkActivated:
decisionHandler(.cancel)
- if url.absoluteString.starts(with: "https://simplex.chat/contact#") {
- ChatModel.shared.appOpenUrl = url
- } else {
- UIApplication.shared.open(url)
- }
+ openExternalLink(url)
default:
decisionHandler(.allow)
}
diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift
index 74b7374654..f10b945dc0 100644
--- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift
+++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift
@@ -332,7 +332,7 @@ 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) {
+ ExternalLink(destination: commitUrl) {
HStack {
Text("Open changes")
Image(systemName: "arrow.up.right.circle")
@@ -351,21 +351,6 @@ private func regularConditionsHeader() -> some View {
}
}
-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]>,
diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift
index 9d068d3b26..26f24f2f0f 100644
--- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift
+++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift
@@ -364,11 +364,15 @@ struct OperatorInfoView: View {
Text(d)
}
}
- Link(serverOperator.info.website.absoluteString, destination: serverOperator.info.website)
+ ExternalLink(destination: serverOperator.info.website) {
+ Text(serverOperator.info.website.absoluteString)
+ }
}
if let selfhost = serverOperator.info.selfhost {
Section {
- Link(selfhost.text, destination: selfhost.link)
+ ExternalLink(destination: selfhost.link) {
+ Text(selfhost.text)
+ }
}
}
}
@@ -432,7 +436,7 @@ struct ConditionsTextView: View {
private func conditionsLinkView(_ conditionsLink: String) -> some View {
VStack(alignment: .leading, spacing: 20) {
Text("Current conditions text couldn't be loaded, you can review conditions via this link:")
- Link(destination: URL(string: conditionsLink)!) {
+ ExternalLink(destination: URL(string: conditionsLink)!) {
Text(conditionsLink)
.multilineTextAlignment(.leading)
}
@@ -591,11 +595,11 @@ func conditionsLinkButton() -> some View {
let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit
let mdUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/\(commit)/PRIVACY.md") ?? conditionsURL
return Menu {
- Link(destination: mdUrl) {
+ ExternalLink(destination: mdUrl) {
Label("Open conditions", systemImage: "doc")
}
if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") {
- Link(destination: commitUrl) {
+ ExternalLink(destination: commitUrl) {
Label("Open changes", systemImage: "ellipsis")
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift
index e57df4c5dc..b059be7cb0 100644
--- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift
+++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift
@@ -223,9 +223,7 @@ struct YourServersView: View {
func howToButton() -> some View {
Button {
- DispatchQueue.main.async {
- UIApplication.shared.open(howToUrl)
- }
+ openExternalLink(howToUrl)
} label: {
HStack {
Text("How to use your servers")
diff --git a/apps/ios/Shared/Views/UserSettings/RTCServers.swift b/apps/ios/Shared/Views/UserSettings/RTCServers.swift
index ef891738cc..b045a8ce55 100644
--- a/apps/ios/Shared/Views/UserSettings/RTCServers.swift
+++ b/apps/ios/Shared/Views/UserSettings/RTCServers.swift
@@ -139,9 +139,7 @@ struct RTCServers: View {
func howToButton() -> some View {
Button {
- DispatchQueue.main.async {
- UIApplication.shared.open(howToUrl)
- }
+ openExternalLink(howToUrl)
} label: {
HStack{
Text("How to")
diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
index 65e34a0ac5..a903329454 100644
--- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
@@ -11,7 +11,7 @@ import SwiftUI
import StoreKit
import SimpleXChat
-let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")!
+let simplexTeamURL = URL(string: "simplex:/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw?h=smp6.simplex.im")!
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
@@ -399,7 +399,9 @@ struct SettingsView: View {
}
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) {
+ ExternalLink("Contribute", destination: URL(string: "https://github.com/simplex-chat/simplex-chat#contribute")!)
+ }
settingsRow("star", color: theme.colors.secondary) {
Button("Rate the app") {
if let scene = sceneDelegate.windowScene {
@@ -407,14 +409,16 @@ struct SettingsView: View {
}
}
}
- 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)
+ ExternalLink(destination: URL(string: "https://github.com/simplex-chat/simplex-chat")!) {
+ 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")
+ .padding(.leading, indent)
+ }
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift
index 6c1ea8deb2..ac6ae05984 100644
--- a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift
@@ -31,7 +31,7 @@ struct UserAddressLearnMore: View {
.padding(.top)
Text("SimpleX address and 1-time links are safe to share via any messenger.")
Text("To protect against your link being replaced, you can compare contact security codes.")
- Text("Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).")
+ ExternalLink("Read more in User Guide.", destination: URL(string: "https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses")!)
.padding(.top)
}
diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
index 4df58f8b0c..e22042fa24 100644
--- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
@@ -215,11 +215,6 @@ struct UserAddressView: View {
HStack(spacing: 8) {
let link = userAddress.connLinkContact.simplexChatUri(short: showShortLink)
linkTextView(link)
- Button { UIPasteboard.general.string = link } label: {
- Image(systemName: "doc.on.doc")
- .padding(.top, -7)
- .padding(.horizontal, 8)
- }
Button { showShareSheet(items: [link]) } label: {
Image(systemName: "square.and.arrow.up")
.padding(.top, -7)
diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
index 3cf65c8b54..71a7a427be 100644
--- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
+++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
@@ -185,9 +185,20 @@
%d месеца
time interval
-
- %d relays
- channel relay bar
+
+ %d relays failed
+ channel relay bar
+channel subscriber relay bar
+
+
+ %d relays not active
+ channel relay bar
+channel subscriber relay bar
+
+
+ %d relays removed
+ channel relay bar
+channel subscriber relay bar
%d sec
@@ -222,10 +233,18 @@
channel creation progress
channel relay bar progress
+
+ %1$d/%2$d relays active, %3$d errors
+ channel relay bar
+
%1$d/%2$d relays active, %3$d failed
channel creation progress with errors
-channel relay bar progress with errors
+channel relay bar
+
+
+ %1$d/%2$d relays active, %3$d removed
+ channel relay bar
%1$d/%2$d relays connected
@@ -233,7 +252,15 @@ channel relay bar progress with errors
%1$d/%2$d relays connected, %3$d errors
- channel subscriber relay bar progress with errors
+ channel subscriber relay bar
+
+
+ %1$d/%2$d relays connected, %3$d failed
+ channel subscriber relay bar
+
+
+ %1$d/%2$d relays connected, %3$d removed
+ channel subscriber relay bar
%lld
@@ -349,11 +376,19 @@ channel relay bar progress with errors
%u пропуснати съобщения.
No comment provided by engineer.
+
+ (from owner)
+ chat link info line
+
(new)
(ново)
No comment provided by engineer.
+
+ (signed)
+ chat link info line
+
(this device v%@)
(това устройство v%@)
@@ -446,6 +481,12 @@ channel relay bar progress with errors
- и още!
No comment provided by engineer.
+
+ - opt-in to send link previews.
+- prevent hyperlink phishing.
+- remove link tracking.
+ No comment provided by engineer.
+
- optionally notify deleted contacts.
- profile names with spaces.
@@ -544,6 +585,10 @@ time interval
Още няколко неща
No comment provided by engineer.
+
+ A link for one person to connect
+ No comment provided by engineer.
+
A new contact
Нов контакт
@@ -670,9 +715,8 @@ swipe action
Активни връзки
No comment provided by engineer.
-
- Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.
- Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти.
+
+ Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts.
No comment provided by engineer.
@@ -740,6 +784,10 @@ swipe action
Добавени сървъри за съобщения
No comment provided by engineer.
+
+ Adding relays will be supported later.
+ No comment provided by engineer.
+
Additional accent
Допълнителен акцент
@@ -859,6 +907,14 @@ swipe action
Всички профили
profile dropdown
+
+ All relays failed
+ No comment provided by engineer.
+
+
+ All relays removed
+ No comment provided by engineer.
+
All reports will be archived for you.
Всички доклади за нарушения ще бъдат архивирани за вас.
@@ -919,6 +975,10 @@ swipe action
Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа)
No comment provided by engineer.
+
+ Allow members to chat with admins.
+ No comment provided by engineer.
+
Allow message reactions only if your contact allows them.
Позволи реакции на съобщения само ако вашият контакт ги разрешава.
@@ -934,6 +994,10 @@ swipe action
Позволи изпращането на лични съобщения до членовете.
No comment provided by engineer.
+
+ Allow sending direct messages to subscribers.
+ No comment provided by engineer.
+
Allow sending disappearing messages.
Разреши изпращането на изчезващи съобщения.
@@ -944,6 +1008,10 @@ swipe action
Позволи споделяне
No comment provided by engineer.
+
+ Allow subscribers to chat with admins.
+ No comment provided by engineer.
+
Allow to irreversibly delete sent messages. (24 hours)
Позволи необратимо изтриване на изпратените съобщения. (24 часа)
@@ -1049,11 +1117,6 @@ swipe action
Отговор на повикване
No comment provided by engineer.
-
- Anybody can host servers.
- Протокол и код с отворен код – всеки може да оперира собствени сървъри.
- No comment provided by engineer.
-
App build: %@
Компилация на приложението: %@
@@ -1258,6 +1321,19 @@ swipe action
Лош хеш на съобщението
No comment provided by engineer.
+
+ Be free
+in your network
+ No comment provided by engineer.
+
+
+ Be free in your network.
+ No comment provided by engineer.
+
+
+ Because we destroyed the power to know who you are. So that your power can never be taken.
+ No comment provided by engineer.
+
Better calls
По-добри обаждания
@@ -1407,6 +1483,10 @@ swipe action
И вие, и вашият контакт можете да изпращате гласови съобщения.
No comment provided by engineer.
+
+ Bottom bar
+ No comment provided by engineer.
+
Broadcast
compose placeholder for channel owner
@@ -1419,7 +1499,7 @@ swipe action
Business address
Бизнес адрес
- No comment provided by engineer.
+ chat link info line
Business chats
@@ -1441,15 +1521,6 @@ swipe action
Чрез чат профил (по подразбиране) или [чрез връзка](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА).
No comment provided by engineer.
-
- By using SimpleX Chat you agree to:
-- send only legal content in public groups.
-- respect other users – no spam.
- С използването на SimpleX Chat вие се съгласявате със:
-- изпращане само на легално съдържание в публични групи.
-- уважение към другите потребители – без спам.
- No comment provided by engineer.
-
Call already ended!
Разговорът вече приключи!
@@ -1610,12 +1681,21 @@ set passcode view
Channel full name (optional)
No comment provided by engineer.
+
+ Channel has no active relays. Please try to join later.
+ alert message
+alert subtitle
+
Channel image
No comment provided by engineer.
Channel link
+ chat link info line
+
+
+ Channel preferences
No comment provided by engineer.
@@ -1630,6 +1710,10 @@ set passcode view
Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers.
alert message
+
+ Channel temporarily unavailable
+ alert title
+
Channel will be deleted for all subscribers - this cannot be undone!
No comment provided by engineer.
@@ -1642,6 +1726,10 @@ set passcode view
Channel will start working with %1$d of %2$d relays. Proceed?
alert message
+
+ Channels
+ No comment provided by engineer.
+
Chat
Чат
@@ -1761,7 +1849,8 @@ set passcode view
Chat with admins
Чат с администраторите
- chat toolbar
+ chat feature
+chat toolbar
Chat with member
@@ -1778,11 +1867,23 @@ set passcode view
Чатове
No comment provided by engineer.
+
+ Chats with admins are prohibited.
+ No comment provided by engineer.
+
+
+ Chats with admins in public channels have no E2E encryption - use only with trusted chat relays.
+ alert message
+
Chats with members
Чатове с членовете
No comment provided by engineer.
+
+ Chats with members are disabled
+ No comment provided by engineer.
+
Check messages every 20 min.
Проверявай за съобщенията на всеки 20 минути.
@@ -1950,11 +2051,6 @@ set passcode view
Configure relays
No comment provided by engineer.
-
- Configure server operators
- Конфигуриране на сървърни оператори
- No comment provided by engineer.
-
Confirm
Потвърди
@@ -2060,6 +2156,10 @@ This is your own one-time link!
Свърване чрез линк
new chat sheet title
+
+ Connect via link or QR code
+ No comment provided by engineer.
+
Connect via one-time link
Свързване чрез еднократен линк за връзка
@@ -2138,7 +2238,7 @@ This is your own one-time link!
Connection error (AUTH)
Грешка при свързване (AUTH)
- No comment provided by engineer.
+ conn error description
Connection failed
@@ -2193,6 +2293,10 @@ This is your own one-time link!
Connections
No comment provided by engineer.
+
+ Contact address
+ chat link info line
+
Contact allows
Контактът позволява
@@ -2258,6 +2362,11 @@ This is your own one-time link!
Продължи
No comment provided by engineer.
+
+ Contribute
+ Допринеси
+ No comment provided by engineer.
+
Conversation deleted!
No comment provided by engineer.
@@ -2285,11 +2394,6 @@ This is your own one-time link!
Поправи име на %@?
alert message
-
- Create
- Създаване
- No comment provided by engineer.
-
Create 1-time link
Създаване на еднократна препратка
@@ -2356,11 +2460,19 @@ This is your own one-time link!
Create your address
No comment provided by engineer.
+
+ Create your link
+ No comment provided by engineer.
+
Create your profile
Създай своя профил
No comment provided by engineer.
+
+ Create your public address
+ No comment provided by engineer.
+
Created
No comment provided by engineer.
@@ -2536,11 +2648,6 @@ This is your own one-time link!
Debug delivery
No comment provided by engineer.
-
- Decentralized
- Децентрализиран
- No comment provided by engineer.
-
Decode link
relay test step
@@ -2914,6 +3021,14 @@ alert button
Личните съобщения между членовете са забранени в тази група.
No comment provided by engineer.
+
+ Direct messages between subscribers are prohibited.
+ No comment provided by engineer.
+
+
+ Disable
+ alert button
+
Disable (keep overrides)
Деактивиране (запазване на промените)
@@ -3014,6 +3129,10 @@ alert button
Не изпращай история на нови членове.
No comment provided by engineer.
+
+ Do not send history to new subscribers.
+ No comment provided by engineer.
+
Do not use credentials with proxy.
No comment provided by engineer.
@@ -3106,6 +3225,10 @@ chat item action
E2E encrypted notifications.
No comment provided by engineer.
+
+ Easier to invite your friends 👋
+ No comment provided by engineer.
+
Edit
Редактирай
@@ -3127,7 +3250,7 @@ chat item action
Enable
Активирай
- No comment provided by engineer.
+ alert button
Enable (keep overrides)
@@ -3162,6 +3285,10 @@ chat item action
Разреши достъпа до камерата
No comment provided by engineer.
+
+ Enable chats with admins?
+ alert title
+
Enable disappearing messages by default.
No comment provided by engineer.
@@ -3181,16 +3308,15 @@ chat item action
Активирай незабавни известия?
No comment provided by engineer.
+
+ Enable link previews?
+ alert title
+
Enable lock
Активирай заключване
No comment provided by engineer.
-
- Enable notifications
- Активирай известията
- No comment provided by engineer.
-
Enable periodic notifications?
Активирай периодични известия?
@@ -3323,6 +3449,10 @@ chat item action
Въведете парола по-горе, за да се покаже!
No comment provided by engineer.
+
+ Enter profile name...
+ No comment provided by engineer.
+
Enter relay name…
No comment provided by engineer.
@@ -3355,7 +3485,7 @@ chat item action
Error
Грешка при свързване със сървъра
- No comment provided by engineer.
+ conn error description
Error aborting address change
@@ -3674,6 +3804,10 @@ chat item action
Грешка при настройването на потвърждениeто за доставка!!
No comment provided by engineer.
+
+ Error sharing channel
+ alert title
+
Error starting chat
Грешка при стартиране на чата
@@ -4015,6 +4149,10 @@ server test error
For all moderators
No comment provided by engineer.
+
+ For anyone to reach you
+ No comment provided by engineer.
+
For chat profile %@:
servers error
@@ -4151,6 +4289,10 @@ Error: %2$@
Get notified when mentioned.
No comment provided by engineer.
+
+ Get started
+ No comment provided by engineer.
+
Good afternoon!
message preview
@@ -4207,7 +4349,7 @@ Error: %2$@
Group link
Групов линк
- No comment provided by engineer.
+ chat link info line
Group links
@@ -4316,6 +4458,10 @@ Error: %2$@
Историята не се изпраща на нови членове.
No comment provided by engineer.
+
+ History is not sent to new subscribers.
+ No comment provided by engineer.
+
How SimpleX works
Как работи SimpleX
@@ -4410,11 +4556,6 @@ Error: %2$@
Веднага
No comment provided by engineer.
-
- Immune to spam
- Защитен от спам и злоупотреби
- No comment provided by engineer.
-
Import
Импортиране
@@ -4552,9 +4693,9 @@ More improvements are coming soon!
Първоначална роля
No comment provided by engineer.
-
- Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)
- Инсталирайте [SimpleX Chat за терминал](https://github.com/simplex-chat/simplex-chat)
+
+ Install SimpleX Chat for terminal
+ Инсталирайте SimpleX Chat за терминал
No comment provided by engineer.
@@ -4606,7 +4747,7 @@ More improvements are coming soon!
Invalid connection link
Невалиден линк за връзка
- No comment provided by engineer.
+ conn error description
Invalid display name!
@@ -4670,6 +4811,10 @@ More improvements are coming soon!
Покани членове
No comment provided by engineer.
+
+ Invite someone privately
+ No comment provided by engineer.
+
Invite to chat
No comment provided by engineer.
@@ -4863,6 +5008,10 @@ This is your link for group %@!
Less traffic on mobile networks.
No comment provided by engineer.
+
+ Let someone connect to you
+ No comment provided by engineer.
+
Let's talk in SimpleX Chat
Нека да поговорим в SimpleX Chat
@@ -4883,6 +5032,10 @@ This is your link for group %@!
Свържете мобилни и настолни приложения! 🔗
No comment provided by engineer.
+
+ Link signature verified.
+ owner verification
+
Linked desktop options
Настройки на запомнени настолни устройства
@@ -5052,6 +5205,10 @@ This is your link for group %@!
Членовете на групата могат да добавят реакции към съобщенията.
No comment provided by engineer.
+
+ Members can chat with admins.
+ No comment provided by engineer.
+
Members can irreversibly delete sent messages. (24 hours)
Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа)
@@ -5202,6 +5359,14 @@ This is your link for group %@!
Съобщенията от %@ ще бъдат показани!
No comment provided by engineer.
+
+ Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages.
+ No comment provided by engineer.
+
+
+ Messages in this channel are not end-to-end encrypted. Chat relays can see these messages.
+ E2EE info chat item
+
Messages in this chat will never be deleted.
alert message
@@ -5228,16 +5393,15 @@ This is your link for group %@!
Съобщенията, файловете и разговорите са защитени чрез **квантово устойчиво e2e криптиране** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом.
No comment provided by engineer.
+
+ Migrate
+ No comment provided by engineer.
+
Migrate device
Мигрирай устройството
No comment provided by engineer.
-
- Migrate from another device
- Мигриране от друго устройство
- No comment provided by engineer.
-
Migrate here
Мигрирай тук
@@ -5355,6 +5519,10 @@ This is your link for group %@!
Мрежа и сървъри
No comment provided by engineer.
+
+ Network commitments
+ No comment provided by engineer.
+
Network connection
Мрежова връзка
@@ -5364,6 +5532,10 @@ This is your link for group %@!
Network decentralization
No comment provided by engineer.
+
+ Network error
+ conn error description
+
Network issues - message expired after many attempts to send it.
snd error text
@@ -5377,6 +5549,11 @@ This is your link for group %@!
Network operator
No comment provided by engineer.
+
+ Network routers cannot know
+who talks to whom
+ No comment provided by engineer.
+
Network settings
Мрежови настройки
@@ -5391,6 +5568,10 @@ This is your link for group %@!
New
token status text
+
+ New 1-time link
+ No comment provided by engineer.
+
New Passcode
Нов kод за достъп
@@ -5482,6 +5663,15 @@ This is your link for group %@!
Не
No comment provided by engineer.
+
+ No account. No phone. No email. No ID.
+The most secure encryption.
+ No comment provided by engineer.
+
+
+ No active relays
+ No comment provided by engineer.
+
No app password
Приложението няма kод за достъп
@@ -5622,9 +5812,16 @@ This is your link for group %@!
No unread chats
No comment provided by engineer.
-
- No user identifiers.
- Първата платформа без никакви потребителски идентификатори – поверителна по дизайн.
+
+ Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life.
+ No comment provided by engineer.
+
+
+ Non-profit governance
+ No comment provided by engineer.
+
+
+ Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign.
No comment provided by engineer.
@@ -5682,7 +5879,7 @@ This is your link for group %@!
OK
ОК
- No comment provided by engineer.
+ alert button
Off
@@ -5701,11 +5898,19 @@ new chat action
Стара база данни
No comment provided by engineer.
+
+ On your phone, not on servers.
+ No comment provided by engineer.
+
One-time invitation link
Линк за еднократна покана
No comment provided by engineer.
+
+ One-time link
+ chat link info line
+
Onion hosts will be **required** for connection.
Requires compatible VPN.
@@ -5725,6 +5930,10 @@ Requires compatible VPN.
Няма се използват Onion хостове.
No comment provided by engineer.
+
+ Only channel owners can change channel preferences.
+ No comment provided by engineer.
+
Only chat owners can change preferences.
No comment provided by engineer.
@@ -5822,7 +6031,8 @@ Requires compatible VPN.
Open
Отвори
- alert action
+ alert action
+alert button
Open Settings
@@ -5855,6 +6065,10 @@ Requires compatible VPN.
Open conditions
No comment provided by engineer.
+
+ Open external link?
+ alert title
+
Open full link
alert action
@@ -5914,6 +6128,13 @@ Requires compatible VPN.
Operator server
alert title
+
+ Operators commit to:
+- Be independent
+- Minimize metadata usage
+- Run verified open-source code
+ No comment provided by engineer.
+
Or import archive file
No comment provided by engineer.
@@ -5933,6 +6154,10 @@ Requires compatible VPN.
Или сигурно споделете този линк към файла
No comment provided by engineer.
+
+ Or show QR in person or via video call.
+ No comment provided by engineer.
+
Or show this code
Или покажи този код
@@ -5942,6 +6167,10 @@ Requires compatible VPN.
Or to share privately
No comment provided by engineer.
+
+ Or use this QR - print or show online.
+ No comment provided by engineer.
+
Organize chats into lists
No comment provided by engineer.
@@ -5964,6 +6193,10 @@ Requires compatible VPN.
Owners
No comment provided by engineer.
+
+ Ownership: you can run your own relays.
+ No comment provided by engineer.
+
PING count
PING бройка
@@ -6018,6 +6251,10 @@ Requires compatible VPN.
Постави изображение
No comment provided by engineer.
+
+ Paste link / Scan
+ No comment provided by engineer.
+
Paste link to connect!
Поставете линк, за да се свържете!
@@ -6201,13 +6438,12 @@ Error: %@
Privacy policy and conditions of use.
No comment provided by engineer.
-
- Privacy redefined
- Поверителността преосмислена
+
+ Privacy: for owners and subscribers.
No comment provided by engineer.
-
- Private chats, groups and your contacts are not accessible to server operators.
+
+ Private and secure messaging.
No comment provided by engineer.
@@ -6272,9 +6508,8 @@ Error: %@
Profile theme
No comment provided by engineer.
-
- Profile update will be sent to your contacts.
- Актуализацията на профила ще бъде изпратена до вашите контакти.
+
+ Profile update will be sent to your SimpleX contacts.
alert message
@@ -6282,6 +6517,10 @@ Error: %@
Забрани аудио/видео разговорите.
No comment provided by engineer.
+
+ Prohibit chats with admins.
+ No comment provided by engineer.
+
Prohibit irreversible message deletion.
Забрани необратимото изтриване на съобщения.
@@ -6311,6 +6550,10 @@ Error: %@
Забрани изпращането на лични съобщения до членовете.
No comment provided by engineer.
+
+ Prohibit sending direct messages to subscribers.
+ No comment provided by engineer.
+
Prohibit sending disappearing messages.
Забрани изпращането на изчезващи съобщения.
@@ -6371,6 +6614,10 @@ Enable in *Network & servers* settings.
Proxy requires password
No comment provided by engineer.
+
+ Public channels - speak freely 🚀
+ No comment provided by engineer.
+
Push notifications
Push известия
@@ -6410,24 +6657,14 @@ Enable in *Network & servers* settings.
Прочетете още
No comment provided by engineer.
-
- Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).
- Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).
+
+ Read more in User Guide.
+ Прочетете повече в Ръководство за потребителя.
No comment provided by engineer.
-
- Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).
- Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).
- No comment provided by engineer.
-
-
- Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).
- Прочетете повече в [Ръководство на потребителя](https://simplex.chat/docs/guide/readme.html#connect-to-friends).
- No comment provided by engineer.
-
-
- Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).
- Прочетете повече в нашето [GitHub хранилище](https://github.com/simplex-chat/simplex-chat#readme).
+
+ Read more in our GitHub repository.
+ Прочетете повече в нашето GitHub хранилище.
No comment provided by engineer.
@@ -6590,6 +6827,10 @@ swipe action
Relay link
No comment provided by engineer.
+
+ Relay results:
+ alert message
+
Relay server is only used if necessary. Another party can observe your IP address.
Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес.
@@ -6604,6 +6845,10 @@ swipe action
Relay test failed!
No comment provided by engineer.
+
+ Reliability: many relays per channel.
+ No comment provided by engineer.
+
Remove
Премахване
@@ -6860,6 +7105,10 @@ swipe action
SOCKS proxy
No comment provided by engineer.
+
+ Safe web links
+ No comment provided by engineer.
+
Safely receive files
No comment provided by engineer.
@@ -6902,6 +7151,10 @@ chat item action
Запази и уведоми членовете на групата
No comment provided by engineer.
+
+ Save and notify subscribers
+ No comment provided by engineer.
+
Save and reconnect
No comment provided by engineer.
@@ -7086,6 +7339,10 @@ chat item action
Код за сигурност
No comment provided by engineer.
+
+ Security: owners hold channel keys.
+ No comment provided by engineer.
+
Select
Избери
@@ -7205,6 +7462,10 @@ chat item action
Send request without message
No comment provided by engineer.
+
+ Send the link via any messenger - it's secure. Ask to paste into SimpleX.
+ No comment provided by engineer.
+
Send them from gallery or custom keyboards.
Изпрати от галерия или персонализирани клавиатури.
@@ -7215,6 +7476,10 @@ chat item action
Изпращане до последните 100 съобщения на нови членове.
No comment provided by engineer.
+
+ Send up to 100 last messages to new subscribers.
+ No comment provided by engineer.
+
Send your private feedback to groups.
No comment provided by engineer.
@@ -7229,6 +7494,10 @@ chat item action
Подателят може да е изтрил заявката за връзка.
No comment provided by engineer.
+
+ Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later.
+ alert message
+
Sending delivery receipts will be enabled for all contacts in all visible chat profiles.
Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили.
@@ -7464,6 +7733,14 @@ chat item action
Settings were changed.
alert message
+
+ Setup notifications
+ No comment provided by engineer.
+
+
+ Setup routers
+ No comment provided by engineer.
+
Shape profile images
Променете формата на профилните изображения
@@ -7497,11 +7774,14 @@ chat item action
Share address publicly
No comment provided by engineer.
-
- Share address with contacts?
- Сподели адреса с контактите?
+
+ Share address with SimpleX contacts?
alert title
+
+ Share channel
+ No comment provided by engineer.
+
Share from other apps.
No comment provided by engineer.
@@ -7536,9 +7816,12 @@ chat item action
Share to SimpleX
No comment provided by engineer.
-
- Share with contacts
- Сподели с контактите
+
+ Share via chat
+ No comment provided by engineer.
+
+
+ Share with SimpleX contacts
No comment provided by engineer.
@@ -7767,6 +8050,11 @@ report reason
Квадрат, кръг или нещо между тях.
No comment provided by engineer.
+
+ Star on GitHub
+ Звезда в GitHub
+ No comment provided by engineer.
+
Start chat
Започни чат
@@ -7866,6 +8154,10 @@ report reason
Subscriber
No comment provided by engineer.
+
+ Subscriber reports
+ chat feature
+
Subscriber will be removed from channel - this cannot be undone!
alert message
@@ -7874,6 +8166,42 @@ report reason
Subscribers
No comment provided by engineer.
+
+ Subscribers can add message reactions.
+ No comment provided by engineer.
+
+
+ Subscribers can chat with admins.
+ No comment provided by engineer.
+
+
+ Subscribers can irreversibly delete sent messages. (24 hours)
+ No comment provided by engineer.
+
+
+ Subscribers can report messsages to moderators.
+ No comment provided by engineer.
+
+
+ Subscribers can send SimpleX links.
+ No comment provided by engineer.
+
+
+ Subscribers can send direct messages.
+ No comment provided by engineer.
+
+
+ Subscribers can send disappearing messages.
+ No comment provided by engineer.
+
+
+ Subscribers can send files and media.
+ No comment provided by engineer.
+
+
+ Subscribers can send voice messages.
+ No comment provided by engineer.
+
Subscribers use relay link to connect to the channel.
Relay address was used to set up this relay for the channel.
@@ -7951,6 +8279,10 @@ Relay address was used to set up this relay for the channel.
Направи снимка
No comment provided by engineer.
+
+ Talk to someone
+ No comment provided by engineer.
+
Tap Connect to chat
No comment provided by engineer.
@@ -7963,10 +8295,6 @@ Relay address was used to set up this relay for the channel.
Tap Connect to use bot
No comment provided by engineer.
-
Tap Join channel
No comment provided by engineer.
@@ -8000,6 +8328,10 @@ Relay address was used to set up this relay for the channel.
Докосни за инкогнито вход
No comment provided by engineer.
+
+ Tap to open
+ No comment provided by engineer.
+
Tap to paste link
Докосни за поставяне на линк за връзка
@@ -8096,6 +8428,10 @@ It can happen because of some bug or when the connection is compromised.QR кодът, който сканирахте, не е SimpleX линк за връзка.
No comment provided by engineer.
+
+ The connection reached the limit of undelivered messages
+ conn error description
+
The connection reached the limit of undelivered messages, your contact may be offline.
No comment provided by engineer.
@@ -8120,9 +8456,9 @@ It can happen because of some bug or when the connection is compromised.Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване!
No comment provided by engineer.
-
- The future of messaging
- Ново поколение поверителни съобщения
+
+ The first network where you own
+your contacts and groups.
No comment provided by engineer.
@@ -8157,6 +8493,10 @@ It can happen because of some bug or when the connection is compromised.Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита.
No comment provided by engineer.
+
+ The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it.
+ No comment provided by engineer.
+
The same conditions will apply to operator **%@**.
No comment provided by engineer.
@@ -8197,6 +8537,14 @@ It can happen because of some bug or when the connection is compromised.Themes
No comment provided by engineer.
+
+ Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible.
+ No comment provided by engineer.
+
+
+ There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected.
+ No comment provided by engineer.
+
These conditions will also apply for: **%@**.
No comment provided by engineer.
@@ -8312,6 +8660,10 @@ It can happen because of some bug or when the connection is compromised.Скриване на нежелани съобщения.
No comment provided by engineer.
+
+ To make SimpleX Network last.
+ No comment provided by engineer.
+
To make a new connection
За да направите нова връзка
@@ -8390,10 +8742,6 @@ You will be prompted to complete authentication before this feature is enabled.<
За да проверите криптирането от край до край с вашия контакт, сравнете (или сканирайте) кода на вашите устройства.
No comment provided by engineer.
-
- Toggle chat list:
- No comment provided by engineer.
-
Toggle incognito when connecting.
Избор на инкогнито при свързване.
@@ -8407,6 +8755,10 @@ You will be prompted to complete authentication before this feature is enabled.<
Toolbar opacity
No comment provided by engineer.
+
+ Top bar
+ No comment provided by engineer.
+
Total
No comment provided by engineer.
@@ -8570,13 +8922,17 @@ To connect, please ask your contact to create another connection link and check
Unsupported connection link
- No comment provided by engineer.
+ conn error description
Up to 100 last messages are sent to new members.
На новите членове се изпращат до последните 100 съобщения.
No comment provided by engineer.
+
+ Up to 100 last messages are sent to new subscribers.
+ No comment provided by engineer.
+
Update
Актуализация
@@ -8687,11 +9043,6 @@ To connect, please ask your contact to create another connection link and check
Use TCP port 443 for preset servers only.
No comment provided by engineer.
-
- Use chat
- Използвай чата
- No comment provided by engineer.
-
Use current profile
Използвай текущия профил
@@ -8768,6 +9119,10 @@ To connect, please ask your contact to create another connection link and check
Use the app with one hand.
No comment provided by engineer.
+
+ Use this address in your social media profile, website, or email signature.
+ No comment provided by engineer.
+
Use web port
No comment provided by engineer.
@@ -8914,6 +9269,10 @@ To connect, please ask your contact to create another connection link and check
Wait response
relay test step
+
+ Waiting for channel owner to add relays.
+ No comment provided by engineer.
+
Waiting for desktop...
Изчакване на настолно устройство…
@@ -8952,6 +9311,10 @@ To connect, please ask your contact to create another connection link and check
Предупреждение: Може да загубите някои данни!
No comment provided by engineer.
+
+ We made connecting simpler for new users.
+ No comment provided by engineer.
+
WebRTC ICE servers
WebRTC ICE сървъри
@@ -9000,6 +9363,10 @@ To connect, please ask your contact to create another connection link and check
Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани.
No comment provided by engineer.
+
+ Why SimpleX is built.
+ No comment provided by engineer.
+
WiFi
WiFi
@@ -9246,6 +9613,12 @@ Repeat join request?
Не може да изпращате съобщения!
alert title
+
+ You commit to:
+- Only legal content in public groups
+- Respect other users - no spam
+ No comment provided by engineer.
+
You connected to the channel via this relay link.
No comment provided by engineer.
@@ -9255,11 +9628,6 @@ Repeat join request?
Не можахте да бъдете потвърдени; Моля, опитайте отново.
No comment provided by engineer.
-
- You decide who can connect.
- Хората могат да се свържат с вас само чрез ликовете, които споделяте.
- No comment provided by engineer.
-
You have already requested connection!
Repeat connection request?
@@ -9323,6 +9691,10 @@ Repeat connection request?
You should receive notifications.
token info
+
+ You were born without an account
+ No comment provided by engineer.
+
You will be able to send messages **only after your request is accepted**.
No comment provided by engineer.
@@ -9454,6 +9826,10 @@ Repeat connection request?
Вашите контакти ще останат свързани.
No comment provided by engineer.
+
+ Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public.
+ No comment provided by engineer.
+
Your credentials may be sent unencrypted.
No comment provided by engineer.
@@ -9472,6 +9848,10 @@ Repeat connection request?
Your group
No comment provided by engineer.
+
+ Your network
+ No comment provided by engineer.
+
Your preferences
Вашите настройки
@@ -9511,6 +9891,10 @@ Relays can access channel messages.
Your profile was changed. If you save it, the updated profile will be sent to all your contacts.
alert message
+
+ Your public address
+ No comment provided by engineer.
+
Your random profile
Вашият автоматично генериран профил
@@ -9538,21 +9922,11 @@ Relays can access channel messages.
Вашите настройки
No comment provided by engineer.
-
- [Contribute](https://github.com/simplex-chat/simplex-chat#contribute)
- [Допринеси](https://github.com/simplex-chat/simplex-chat#contribute)
- No comment provided by engineer.
-
[Send us email](mailto:chat@simplex.chat)
[Изпратете ни имейл](mailto:chat@simplex.chat)
No comment provided by engineer.
-
- [Star on GitHub](https://github.com/simplex-chat/simplex-chat)
- [Звезда в GitHub](https://github.com/simplex-chat/simplex-chat)
- No comment provided by engineer.
-
\_italic_
\_курсив_
@@ -9700,6 +10074,10 @@ marked deleted chat item preview text
повикване…
call status
+
+ can't broadcast
+ No comment provided by engineer.
+
can't send messages
No comment provided by engineer.
@@ -10332,6 +10710,10 @@ time to disappear
removed (%d attempts)
receive error chat item
+
+ removed by operator
+ No comment provided by engineer.
+
removed contact address
премахнат адрес за контакт
@@ -10636,6 +11018,10 @@ last received msg: %2$@
\~зачеркнат~
No comment provided by engineer.
+
+ ⚠️ Signature verification failed: %@.
+ owner verification
+