mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
redesign wip
This commit is contained in:
@@ -9,6 +9,13 @@ import SwiftUI
|
||||
import Intents
|
||||
import SimpleXChat
|
||||
|
||||
enum HomeTab {
|
||||
case settings
|
||||
case contacts
|
||||
case chats
|
||||
case newChat
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@@ -27,7 +34,7 @@ struct ContentView: View {
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
|
||||
@State private var showSettings = false
|
||||
@State private var homeTab: HomeTab = .chats
|
||||
@State private var showWhatsNew = false
|
||||
@State private var showChooseLAMode = false
|
||||
@State private var showSetPasscode = false
|
||||
@@ -74,7 +81,7 @@ struct ContentView: View {
|
||||
callView(call)
|
||||
}
|
||||
|
||||
if !showSettings, let la = chatModel.laRequest {
|
||||
if homeTab != .settings, let la = chatModel.laRequest {
|
||||
LocalAuthView(authRequest: la)
|
||||
.onDisappear {
|
||||
// this flag is separate from accessAuthenticated to show initializationView while we wait for authentication
|
||||
@@ -97,9 +104,6 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(showSettings: $showSettings)
|
||||
}
|
||||
.confirmationDialog("SimpleX Lock mode", isPresented: $showChooseLAMode, titleVisibility: .visible) {
|
||||
Button("System authentication") { initialEnableLA() }
|
||||
Button("Passcode entry") { showSetPasscode = true }
|
||||
@@ -230,7 +234,7 @@ struct ContentView: View {
|
||||
|
||||
private func mainView() -> some View {
|
||||
ZStack(alignment: .top) {
|
||||
ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
|
||||
HomeView(homeTab: $homeTab).privacySensitive(protectScreen)
|
||||
.onAppear {
|
||||
requestNtfAuthorization()
|
||||
// Local Authentication notice is to be shown on next start after onboarding is complete
|
||||
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
|
||||
struct ChatHelp: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var homeTab: HomeTab
|
||||
@State private var newChatMenuOption: NewChatMenuOption? = nil
|
||||
|
||||
var body: some View {
|
||||
@@ -24,7 +24,7 @@ struct ChatHelp: View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text("To ask any questions and to receive updates:")
|
||||
Button("connect to SimpleX Chat developers.") {
|
||||
showSettings = false
|
||||
homeTab = .chats
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(simplexTeamURL)
|
||||
}
|
||||
@@ -63,7 +63,6 @@ struct ChatHelp: View {
|
||||
|
||||
struct ChatHelp_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var showSettings = false
|
||||
return ChatHelp(showSettings: $showSettings)
|
||||
return ChatHelp(homeTab: Binding.constant(.chats))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
UserPicker(
|
||||
showSettings: $showSettings,
|
||||
// showSettings: $showSettings,
|
||||
homeTab: Binding.constant(.chats),
|
||||
showConnectDesktop: $showConnectDesktop,
|
||||
userPickerVisible: $userPickerVisible
|
||||
)
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
//
|
||||
// ChatsView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 01.05.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatsView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var searchMode = false
|
||||
@FocusState private var searchFocussed
|
||||
@State private var searchText = ""
|
||||
@State private var searchShowingSimplexLink = false
|
||||
@State private var searchChatFilteredBySimplexLink: String? = nil
|
||||
@State private var newChatMenuOption: NewChatMenuOption? = nil // TODO remove?
|
||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
viewBody.scrollDismissesKeyboard(.immediately)
|
||||
} else {
|
||||
viewBody
|
||||
}
|
||||
}
|
||||
|
||||
private var viewBody: some View {
|
||||
ZStack {
|
||||
NavStackCompat(
|
||||
isActive: Binding(
|
||||
get: { chatModel.chatId != nil },
|
||||
set: { _ in }
|
||||
),
|
||||
destination: chatView
|
||||
) {
|
||||
VStack {
|
||||
if chatModel.chats.isEmpty {
|
||||
onboardingButtons()
|
||||
}
|
||||
chatsView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var chatsView: some View {
|
||||
VStack {
|
||||
chatList
|
||||
}
|
||||
.refreshable {
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Reconnect servers?"),
|
||||
message: Text("Reconnect all connected servers to force message delivery. It uses additional traffic."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
Task {
|
||||
do {
|
||||
try await reconnectAllServers()
|
||||
} catch let error {
|
||||
AlertManager.shared.showAlertMsg(title: "Error", message: "\(responseError(error))")
|
||||
}
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarHidden(searchMode)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack(spacing: 4) {
|
||||
Text("Chats")
|
||||
.font(.headline)
|
||||
if chatModel.chats.count > 0 {
|
||||
toggleFilterButton()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleFilterButton() -> some View {
|
||||
Button {
|
||||
showUnreadAndFavorites = !showUnreadAndFavorites
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : ""))
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private var chatList: some View {
|
||||
let cs = filteredChats()
|
||||
ZStack {
|
||||
VStack {
|
||||
List {
|
||||
if !chatModel.chats.isEmpty {
|
||||
ChatsSearchBar(
|
||||
searchMode: $searchMode,
|
||||
searchFocussed: $searchFocussed,
|
||||
searchText: $searchText,
|
||||
searchShowingSimplexLink: $searchShowingSimplexLink,
|
||||
searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink
|
||||
)
|
||||
.listRowSeparator(.hidden)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
.padding(.trailing, -16)
|
||||
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||
}
|
||||
.offset(x: -8)
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
if chatModel.chatId == nil, let chatId = chatModel.chatToTop {
|
||||
chatModel.chatToTop = nil
|
||||
chatModel.popChat(chatId)
|
||||
}
|
||||
}
|
||||
if cs.isEmpty && !chatModel.chats.isEmpty {
|
||||
Text("No filtered chats").foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
// TODO remove?
|
||||
private func onboardingButtons() -> some View {
|
||||
VStack(alignment: .trailing, spacing: 0) {
|
||||
Path { p in
|
||||
p.move(to: CGPoint(x: 8, y: 0))
|
||||
p.addLine(to: CGPoint(x: 16, y: 10))
|
||||
p.addLine(to: CGPoint(x: 0, y: 10))
|
||||
p.addLine(to: CGPoint(x: 8, y: 0))
|
||||
}
|
||||
.fill(Color.accentColor)
|
||||
.frame(width: 20, height: 10)
|
||||
.padding(.trailing, 12)
|
||||
|
||||
connectButton("Tap to start a new chat") {
|
||||
newChatMenuOption = .newContact
|
||||
}
|
||||
|
||||
Spacer()
|
||||
Text("You have no chats")
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.padding(.trailing, 6)
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
|
||||
private func connectButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View {
|
||||
Button(action: action) {
|
||||
Text(label)
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.background(Color.accentColor)
|
||||
.foregroundColor(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatView() -> some View {
|
||||
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
|
||||
ChatView(chat: chat).onAppear {
|
||||
loadChat(chat: chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func filteredChats() -> [Chat] {
|
||||
if let linkChatId = searchChatFilteredBySimplexLink {
|
||||
return chatModel.chats.filter { $0.id == linkChatId }
|
||||
} else {
|
||||
let s = searchString()
|
||||
return s == "" && !showUnreadAndFavorites
|
||||
? chatModel.chats
|
||||
: chatModel.chats.filter { chat in
|
||||
let cInfo = chat.chatInfo
|
||||
switch cInfo {
|
||||
case let .direct(contact):
|
||||
return s == ""
|
||||
? filtered(chat)
|
||||
: (viewNameContains(cInfo, s) ||
|
||||
contact.profile.displayName.localizedLowercase.contains(s) ||
|
||||
contact.fullName.localizedLowercase.contains(s))
|
||||
case let .group(gInfo):
|
||||
return s == ""
|
||||
? (filtered(chat) || gInfo.membership.memberStatus == .memInvited)
|
||||
: viewNameContains(cInfo, s)
|
||||
case .local:
|
||||
return s == "" || viewNameContains(cInfo, s)
|
||||
case .contactRequest:
|
||||
return s == "" || viewNameContains(cInfo, s)
|
||||
case let .contactConnection(conn):
|
||||
return s != "" && conn.localAlias.localizedLowercase.contains(s)
|
||||
case .invalidJSON:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func searchString() -> String {
|
||||
searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
}
|
||||
|
||||
func filtered(_ chat: Chat) -> Bool {
|
||||
(chat.chatInfo.chatSettings?.favorite ?? false) ||
|
||||
chat.chatStats.unreadChat ||
|
||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||
}
|
||||
|
||||
func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool {
|
||||
cInfo.chatViewName.localizedLowercase.contains(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatsSearchBar: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Binding var searchMode: Bool
|
||||
@FocusState.Binding var searchFocussed: Bool
|
||||
@Binding var searchText: String
|
||||
@Binding var searchShowingSimplexLink: Bool
|
||||
@Binding var searchChatFilteredBySimplexLink: String?
|
||||
@State private var ignoreSearchTextChange = false
|
||||
@State private var showScanCodeSheet = false
|
||||
@State private var alert: PlanAndConnectAlert?
|
||||
@State private var sheet: PlanAndConnectActionSheet?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
TextField("Search or paste SimpleX link", text: $searchText)
|
||||
.foregroundColor(searchShowingSimplexLink ? .secondary : .primary)
|
||||
.disabled(searchShowingSimplexLink)
|
||||
.focused($searchFocussed)
|
||||
.frame(maxWidth: .infinity)
|
||||
if !searchText.isEmpty {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.onTapGesture {
|
||||
searchText = ""
|
||||
}
|
||||
} else if !searchFocussed {
|
||||
HStack(spacing: 24) {
|
||||
if m.pasteboardHasStrings {
|
||||
Image(systemName: "doc")
|
||||
.onTapGesture {
|
||||
if let str = UIPasteboard.general.string {
|
||||
searchText = str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image(systemName: "qrcode")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.onTapGesture {
|
||||
showScanCodeSheet = true
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 2)
|
||||
}
|
||||
}
|
||||
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
|
||||
.foregroundColor(.secondary)
|
||||
.background(Color(.tertiarySystemFill))
|
||||
.cornerRadius(10.0)
|
||||
|
||||
if searchFocussed {
|
||||
Text("Cancel")
|
||||
.foregroundColor(.accentColor)
|
||||
.onTapGesture {
|
||||
searchText = ""
|
||||
searchFocussed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
.sheet(isPresented: $showScanCodeSheet) {
|
||||
NewChatView(selection: .connect, showQRCodeScanner: true)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) // fixes .refreshable in ChatsView affecting nested view
|
||||
}
|
||||
.onChange(of: searchFocussed) { sf in
|
||||
withAnimation { searchMode = sf }
|
||||
}
|
||||
.onChange(of: searchText) { t in
|
||||
if ignoreSearchTextChange {
|
||||
ignoreSearchTextChange = false
|
||||
} else {
|
||||
if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue
|
||||
searchFocussed = false
|
||||
if case let .simplexLink(linkType, _, smpHosts) = link.format {
|
||||
ignoreSearchTextChange = true
|
||||
searchText = simplexLinkText(linkType, smpHosts)
|
||||
}
|
||||
searchShowingSimplexLink = true
|
||||
searchChatFilteredBySimplexLink = nil
|
||||
connect(link.text)
|
||||
} else {
|
||||
if t != "" { // if some other text is pasted, enter search mode
|
||||
searchFocussed = true
|
||||
}
|
||||
searchShowingSimplexLink = false
|
||||
searchChatFilteredBySimplexLink = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { a in
|
||||
planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" })
|
||||
}
|
||||
.actionSheet(item: $sheet) { s in
|
||||
planAndConnectActionSheet(s, dismiss: true, cleanup: { searchText = "" })
|
||||
}
|
||||
}
|
||||
|
||||
private func connect(_ link: String) {
|
||||
planAndConnect(
|
||||
link,
|
||||
showAlert: { alert = $0 },
|
||||
showActionSheet: { sheet = $0 },
|
||||
dismiss: false,
|
||||
incognito: nil,
|
||||
filterKnownContact: { searchChatFilteredBySimplexLink = $0.id },
|
||||
filterKnownGroup: { searchChatFilteredBySimplexLink = $0.id }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO remove
|
||||
func chatsStoppedIcon() -> some View {
|
||||
Button {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Chat is stopped",
|
||||
message: "You can start chat via app Settings / Database or by restarting the app"
|
||||
)
|
||||
} label: {
|
||||
Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.chats = [
|
||||
Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
|
||||
),
|
||||
Chat(
|
||||
chatInfo: ChatInfo.sampleData.group,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")]
|
||||
),
|
||||
Chat(
|
||||
chatInfo: ChatInfo.sampleData.contactRequest,
|
||||
chatItems: []
|
||||
)
|
||||
|
||||
]
|
||||
return Group {
|
||||
ChatsView()
|
||||
.environmentObject(chatModel)
|
||||
ChatsView()
|
||||
.environmentObject(ChatModel())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ struct UserPicker: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var homeTab: HomeTab
|
||||
@Binding var showConnectDesktop: Bool
|
||||
@Binding var userPickerVisible: Bool
|
||||
@State var scrollViewContentSize: CGSize = .zero
|
||||
@@ -72,7 +72,7 @@ struct UserPicker: View {
|
||||
}
|
||||
Divider()
|
||||
menuButton("Settings", icon: "gearshape") {
|
||||
showSettings = true
|
||||
homeTab = .settings
|
||||
withAnimation {
|
||||
userPickerVisible.toggle()
|
||||
}
|
||||
@@ -106,7 +106,7 @@ struct UserPicker: View {
|
||||
let user = u.user
|
||||
return Button(action: {
|
||||
if user.activeUser {
|
||||
showSettings = true
|
||||
homeTab = .settings
|
||||
withAnimation {
|
||||
userPickerVisible.toggle()
|
||||
}
|
||||
@@ -181,7 +181,7 @@ struct UserPicker_Previews: PreviewProvider {
|
||||
let m = ChatModel()
|
||||
m.users = [UserInfo.sampleData, UserInfo.sampleData]
|
||||
return UserPicker(
|
||||
showSettings: Binding.constant(false),
|
||||
homeTab: Binding.constant(.chats),
|
||||
showConnectDesktop: Binding.constant(false),
|
||||
userPickerVisible: Binding.constant(true)
|
||||
)
|
||||
|
||||
@@ -41,7 +41,7 @@ enum DatabaseAlert: Identifiable {
|
||||
|
||||
struct DatabaseView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var homeTab: HomeTab
|
||||
@State private var runChat = false
|
||||
@State private var alert: DatabaseAlert? = nil
|
||||
@State private var showFileImporter = false
|
||||
@@ -409,7 +409,7 @@ struct DatabaseView: View {
|
||||
|
||||
private func startChat() {
|
||||
if m.chatDbChanged {
|
||||
showSettings = false
|
||||
homeTab = .chats
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
resetChatCtrl()
|
||||
do {
|
||||
@@ -493,6 +493,6 @@ func deleteChatAsync() async throws {
|
||||
|
||||
struct DatabaseView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DatabaseView(showSettings: Binding.constant(false), chatItemTTL: .none)
|
||||
DatabaseView(homeTab: Binding.constant(.chats), chatItemTTL: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
//
|
||||
// HomeView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 01.05.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct HomeView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Binding var homeTab: HomeTab
|
||||
@State private var userPickerVisible = false
|
||||
@State private var showConnectDesktop = false
|
||||
@State private var newChatMenuOption: NewChatMenuOption? = nil
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottomLeading) {
|
||||
switch homeTab {
|
||||
case .settings: settingsView()
|
||||
case .contacts: contactsView()
|
||||
case .chats: chatsView()
|
||||
case .newChat: newChatView()
|
||||
}
|
||||
if userPickerVisible {
|
||||
Rectangle().fill(.white.opacity(0.001)).onTapGesture {
|
||||
withAnimation {
|
||||
userPickerVisible.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
UserPicker(
|
||||
homeTab: $homeTab,
|
||||
showConnectDesktop: $showConnectDesktop,
|
||||
userPickerVisible: $userPickerVisible
|
||||
)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
settingsButton()
|
||||
Spacer()
|
||||
contactsButton()
|
||||
Spacer()
|
||||
chatsButton()
|
||||
Spacer()
|
||||
newChatButton()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showConnectDesktop) {
|
||||
ConnectDesktopView()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func settingsButton() -> some View {
|
||||
let user = chatModel.currentUser ?? User.sampleData
|
||||
let multiUser = chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1
|
||||
Button {
|
||||
if multiUser {
|
||||
withAnimation {
|
||||
userPickerVisible.toggle()
|
||||
}
|
||||
} else {
|
||||
homeTab = .settings
|
||||
}
|
||||
} label: {
|
||||
if user.image != nil {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel))
|
||||
.padding(.trailing, 4)
|
||||
let allRead = chatModel.users
|
||||
.filter { u in !u.user.activeUser && !u.user.hidden }
|
||||
.allSatisfy { u in u.unreadCount == 0 }
|
||||
if !allRead {
|
||||
userUnreadBadge(size: 12)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: multiUser ? "person.2.fill" : "gearshape.fill")
|
||||
Text("Users")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(homeTab == .settings ? .accentColor : .secondary)
|
||||
}
|
||||
|
||||
private func userUnreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
private func contactsButton() -> some View {
|
||||
Button {
|
||||
homeTab = .contacts
|
||||
} label: {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: "person.crop.circle.fill")
|
||||
Text("Contacts")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.foregroundColor(homeTab == .contacts ? .accentColor : .secondary)
|
||||
}
|
||||
|
||||
private func chatsButton() -> some View {
|
||||
Button {
|
||||
homeTab = .chats
|
||||
} label: {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: "message.fill")
|
||||
Text("Chats")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.foregroundColor(homeTab == .chats ? .accentColor : .secondary)
|
||||
}
|
||||
|
||||
@ViewBuilder private func newChatButton() -> some View {
|
||||
if homeTab != .newChat {
|
||||
Menu {
|
||||
Button {
|
||||
newChatMenuOption = .newContact
|
||||
homeTab = .newChat
|
||||
} label: {
|
||||
Text("Add contact")
|
||||
}
|
||||
Button {
|
||||
newChatMenuOption = .newGroup
|
||||
homeTab = .newChat
|
||||
} label: {
|
||||
Text("Create group")
|
||||
}
|
||||
} label: {
|
||||
newChatButtonLabel()
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Button {} label: {
|
||||
newChatButtonLabel()
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
private func newChatButtonLabel() -> some View {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: "square.and.pencil")
|
||||
Text("New chat")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
private func settingsView() -> some View {
|
||||
SettingsView(homeTab: $homeTab)
|
||||
}
|
||||
|
||||
private func contactsView() -> some View {
|
||||
// TODO
|
||||
VStack {
|
||||
Text("Contacts")
|
||||
}
|
||||
}
|
||||
|
||||
private func chatsView() -> some View {
|
||||
// TODO remove top bar, move search to bottom
|
||||
// TODO hide toolbar when in chat
|
||||
// TODO onboarding buttons (remove?)
|
||||
ChatsView()
|
||||
}
|
||||
|
||||
@ViewBuilder private func newChatView() -> some View {
|
||||
// TODO doesn't fit
|
||||
// TODO alerts don't work
|
||||
// TODO dismiss on connect
|
||||
// TODO dismiss when creating group
|
||||
// TODO chat stopped (see chatsStoppedIcon in ChatsView)
|
||||
switch newChatMenuOption {
|
||||
case .newContact:
|
||||
NewChatView(selection: .invite)
|
||||
case .newGroup:
|
||||
AddGroupView()
|
||||
case nil:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HomeView(homeTab: Binding.constant(.chats))
|
||||
}
|
||||
@@ -54,7 +54,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
|
||||
struct MigrateFromDevice: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var homeTab: HomeTab
|
||||
@Binding var showProgressOnSettings: Bool
|
||||
@State private var migrationState: MigrationFromState = .chatStopInProgress
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@@ -555,7 +555,7 @@ struct MigrateFromDevice: View {
|
||||
} catch let error {
|
||||
fatalError("Error starting chat \(responseError(error))")
|
||||
}
|
||||
showSettings = false
|
||||
homeTab = .chats
|
||||
}
|
||||
} catch let error {
|
||||
alert = .error(title: "Error deleting database", error: responseError(error))
|
||||
@@ -579,7 +579,7 @@ struct MigrateFromDevice: View {
|
||||
// Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered
|
||||
if dismiss || m.chatDbStatus != .ok {
|
||||
await MainActor.run {
|
||||
showSettings = false
|
||||
homeTab = .chats
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -729,6 +729,6 @@ private class MigrationChatReceiver {
|
||||
|
||||
struct MigrateFromDevice_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MigrateFromDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false))
|
||||
MigrateFromDevice(homeTab: Binding.constant(.chats), showProgressOnSettings: Binding.constant(false))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ struct SettingsView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var homeTab: HomeTab
|
||||
@State private var showProgress: Bool = false
|
||||
|
||||
var body: some View {
|
||||
@@ -185,7 +185,7 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
UserProfilesView(showSettings: $showSettings)
|
||||
UserProfilesView(homeTab: $homeTab)
|
||||
} label: {
|
||||
settingsRow("person.crop.rectangle.stack") { Text("Your chat profiles") }
|
||||
}
|
||||
@@ -215,7 +215,7 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
|
||||
MigrateFromDevice(homeTab: $homeTab, showProgressOnSettings: $showProgress)
|
||||
.navigationTitle("Migrate device")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
@@ -276,7 +276,7 @@ struct SettingsView: View {
|
||||
Section("Help") {
|
||||
if let user = user {
|
||||
NavigationLink {
|
||||
ChatHelp(showSettings: $showSettings)
|
||||
ChatHelp(homeTab: $homeTab)
|
||||
.navigationTitle("Welcome \(user.displayName)!")
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
} label: {
|
||||
@@ -298,7 +298,7 @@ struct SettingsView: View {
|
||||
}
|
||||
settingsRow("number") {
|
||||
Button("Send questions and ideas") {
|
||||
showSettings = false
|
||||
homeTab = .chats
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(simplexTeamURL)
|
||||
}
|
||||
@@ -352,7 +352,7 @@ struct SettingsView: View {
|
||||
|
||||
private func chatDatabaseRow() -> some View {
|
||||
NavigationLink {
|
||||
DatabaseView(showSettings: $showSettings, chatItemTTL: chatModel.chatItemTTL)
|
||||
DatabaseView(homeTab: $homeTab, chatItemTTL: chatModel.chatItemTTL)
|
||||
.navigationTitle("Your chat database")
|
||||
} label: {
|
||||
let color: Color = chatModel.chatDbEncrypted == false ? .orange : .secondary
|
||||
@@ -446,9 +446,8 @@ struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.currentUser = User.sampleData
|
||||
@State var showSettings = false
|
||||
|
||||
return SettingsView(showSettings: $showSettings)
|
||||
return SettingsView(homeTab: Binding.constant(.chats))
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import SimpleXChat
|
||||
|
||||
struct UserProfilesView: View {
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var homeTab: HomeTab
|
||||
@Environment(\.editMode) private var editMode
|
||||
@AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true
|
||||
@AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true
|
||||
@@ -280,7 +280,7 @@ struct UserProfilesView: View {
|
||||
await MainActor.run {
|
||||
onboardingStageDefault.set(.step1_SimpleXInfo)
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
showSettings = false
|
||||
homeTab = .chats
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -403,6 +403,6 @@ public func chatPasswordHash(_ pwd: String, _ salt: String) -> String {
|
||||
|
||||
struct UserProfilesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UserProfilesView(showSettings: Binding.constant(true))
|
||||
UserProfilesView(homeTab: Binding.constant(.settings))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
|
||||
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; };
|
||||
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
|
||||
642CE4952BE2651E00AD7757 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642CE4942BE2651E00AD7757 /* HomeView.swift */; };
|
||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
||||
@@ -168,6 +169,7 @@
|
||||
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
|
||||
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */; };
|
||||
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
|
||||
64593F122BE28E5F00CD75D2 /* ChatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64593F112BE28E5F00CD75D2 /* ChatsView.swift */; };
|
||||
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; };
|
||||
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
|
||||
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; };
|
||||
@@ -450,6 +452,7 @@
|
||||
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
|
||||
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
|
||||
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
|
||||
642CE4942BE2651E00AD7757 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
|
||||
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
||||
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
||||
@@ -463,6 +466,7 @@
|
||||
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
|
||||
644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkedDeletedItemView.swift; sourceTree = "<group>"; };
|
||||
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
|
||||
64593F112BE28E5F00CD75D2 /* ChatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsView.swift; sourceTree = "<group>"; };
|
||||
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
|
||||
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
|
||||
647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = "<group>"; };
|
||||
@@ -562,6 +566,7 @@
|
||||
5C2E260D27A30E2400F70299 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
642CE4932BE2650500AD7757 /* Home */,
|
||||
5CB0BA8C282711BC00B3292C /* Onboarding */,
|
||||
3C714775281C080100CB4D4B /* Call */,
|
||||
5C971E1F27AEBF7000C8A3CE /* Helpers */,
|
||||
@@ -802,6 +807,7 @@
|
||||
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
|
||||
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */,
|
||||
18415835CBD939A9ABDC108A /* UserPicker.swift */,
|
||||
64593F112BE28E5F00CD75D2 /* ChatsView.swift */,
|
||||
);
|
||||
path = ChatList;
|
||||
sourceTree = "<group>";
|
||||
@@ -899,6 +905,14 @@
|
||||
path = Database;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
642CE4932BE2650500AD7757 /* Home */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
642CE4942BE2651E00AD7757 /* HomeView.swift */,
|
||||
);
|
||||
path = Home;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6440CA01288AEC770062C672 /* Group */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1212,6 +1226,7 @@
|
||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */,
|
||||
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
|
||||
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
|
||||
642CE4952BE2651E00AD7757 /* HomeView.swift in Sources */,
|
||||
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */,
|
||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
||||
@@ -1273,6 +1288,7 @@
|
||||
18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */,
|
||||
1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */,
|
||||
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */,
|
||||
64593F122BE28E5F00CD75D2 /* ChatsView.swift in Sources */,
|
||||
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */,
|
||||
18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */,
|
||||
184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */,
|
||||
|
||||
Reference in New Issue
Block a user