diff --git a/apps/ios/Shared/Assets.xcassets/github.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/github.imageset/Contents.json index e30e4bc7ce..241e667fb5 100644 --- a/apps/ios/Shared/Assets.xcassets/github.imageset/Contents.json +++ b/apps/ios/Shared/Assets.xcassets/github.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "github32px.png", + "filename" : "github_1x.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "github64px.png", + "filename" : "github_2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "github64px-1.png", + "filename" : "github_3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/apps/ios/Shared/Assets.xcassets/github.imageset/github32px.png b/apps/ios/Shared/Assets.xcassets/github.imageset/github32px.png deleted file mode 100644 index 8b25551a97..0000000000 Binary files a/apps/ios/Shared/Assets.xcassets/github.imageset/github32px.png and /dev/null differ diff --git a/apps/ios/Shared/Assets.xcassets/github.imageset/github64px-1.png b/apps/ios/Shared/Assets.xcassets/github.imageset/github64px-1.png deleted file mode 100644 index 182a1a3f73..0000000000 Binary files a/apps/ios/Shared/Assets.xcassets/github.imageset/github64px-1.png and /dev/null differ diff --git a/apps/ios/Shared/Assets.xcassets/github.imageset/github64px.png b/apps/ios/Shared/Assets.xcassets/github.imageset/github64px.png deleted file mode 100644 index 182a1a3f73..0000000000 Binary files a/apps/ios/Shared/Assets.xcassets/github.imageset/github64px.png and /dev/null differ diff --git a/apps/ios/Shared/Assets.xcassets/github.imageset/github_1x.png b/apps/ios/Shared/Assets.xcassets/github.imageset/github_1x.png new file mode 100644 index 0000000000..41be900a34 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/github.imageset/github_1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/github.imageset/github_2x.png b/apps/ios/Shared/Assets.xcassets/github.imageset/github_2x.png new file mode 100644 index 0000000000..4cef050214 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/github.imageset/github_2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/github.imageset/github_3x.png b/apps/ios/Shared/Assets.xcassets/github.imageset/github_3x.png new file mode 100644 index 0000000000..0ccb70bbb0 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/github.imageset/github_3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/github_light.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/github_light.imageset/Contents.json new file mode 100644 index 0000000000..88e84c950b --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/github_light.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "github_light_1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "github_light_2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "github_light_3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_1x.png b/apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_1x.png new file mode 100644 index 0000000000..732eb7a048 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_2x.png b/apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_2x.png new file mode 100644 index 0000000000..4fc75012bc Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_3x.png b/apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_3x.png new file mode 100644 index 0000000000..a9881b6bca Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_3x.png differ diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 0392e7274b..2f39dfa075 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -9,6 +9,7 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var chatModel: ChatModel + @ObservedObject var alertManager = AlertManager.shared @State private var showNotificationAlert = false var body: some View { @@ -23,27 +24,49 @@ struct ContentView: View { } ChatReceiver.shared.start() NtfManager.shared.requestAuthorization(onDeny: { - showNotificationAlert = true + alertManager.showAlert(notificationAlert()) }) } - .alert(isPresented : $showNotificationAlert){ - Alert( - title: Text("Notification are disabled!"), - message: Text("Please open settings to enable"), - primaryButton: .default(Text("Open Settings")) { - DispatchQueue.main.async { - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) - } - }, - secondaryButton: .cancel() - ) - } + .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! } } else { WelcomeView() } } + + func notificationAlert() -> Alert { + Alert( + title: Text("Notification are disabled!"), + message: Text("Please open settings to enable"), + primaryButton: .default(Text("Open Settings")) { + DispatchQueue.main.async { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + } + }, + secondaryButton: .cancel() + ) + } } +final class AlertManager: ObservableObject { + static let shared = AlertManager() + @Published var presentAlert = false + @Published var alertView: Alert? + + func showAlert(_ alert: Alert) { + DispatchQueue.main.async { + self.alertView = alert + self.presentAlert = true + } + } + + func showAlertMsg(title: String, message: String? = nil) { + if let message = message { + showAlert(Alert(title: Text(title), message: Text(message))) + } else { + showAlert(Alert(title: Text(title))) + } + } +} //struct ContentView_Previews: PreviewProvider { // static var previews: some View { diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 91265fb85f..3aa05f6fed 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -17,11 +17,11 @@ final class ChatModel: ObservableObject { // current chat @Published var chatId: String? @Published var chatItems: [ChatItem] = [] + @Published var chatToTop: String? // items in the terminal view @Published var terminalItems: [TerminalItem] = [] @Published var userAddress: String? @Published var appOpenUrl: URL? - @Published var connectViaUrl = false static let shared = ChatModel() func hasChat(_ id: String) -> Bool { @@ -43,8 +43,8 @@ final class ChatModel: ObservableObject { } func updateChatInfo(_ cInfo: ChatInfo) { - if let ix = getChatIndex(cInfo.id) { - chats[ix].chatInfo = cInfo + if let i = getChatIndex(cInfo.id) { + chats[i].chatInfo = cInfo } } @@ -64,8 +64,8 @@ final class ChatModel: ObservableObject { } func replaceChat(_ id: String, _ chat: Chat) { - if let ix = chats.firstIndex(where: { $0.id == id }) { - chats[ix] = chat + if let i = getChatIndex(id) { + chats[i] = chat } else { // invalid state, correcting chats.insert(chat, at: 0) @@ -73,23 +73,101 @@ final class ChatModel: ObservableObject { } func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) { - if let ix = chats.firstIndex(where: { $0.id == cInfo.id }) { - chats[ix].chatItems = [cItem] - if ix > 0 { + // update previews + if let i = getChatIndex(cInfo.id) { + chats[i].chatItems = [cItem] + if case .rcvNew = cItem.meta.itemStatus { + chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1 + } + if i > 0 { if chatId == nil { - withAnimation { popChat(ix) } + withAnimation { popChat_(i) } + } else if chatId == cInfo.id { + chatToTop = cInfo.id } else { - DispatchQueue.main.async { self.popChat(ix) } + popChat_(i) + } + } + } else { + addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) + } + // add to current chat + if chatId == cInfo.id { + withAnimation { chatItems.append(cItem) } + if case .rcvNew = cItem.meta.itemStatus { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + if self.chatId == cInfo.id { + SimpleX.markChatItemRead(cInfo, cItem) + } } } } + } + + func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { + // update previews + var res: Bool + if let chat = getChat(cInfo.id) { + if let pItem = chat.chatItems.last, pItem.id == cItem.id { + chat.chatItems = [cItem] + } + res = false + } else { + addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) + res = true + } + // update current chat if chatId == cInfo.id { - withAnimation { chatItems.append(cItem) } + if let i = chatItems.firstIndex(where: { $0.id == cItem.id }) { + withAnimation(.default) { + self.chatItems[i] = cItem + } + return false + } else { + withAnimation { chatItems.append(cItem) } + return true + } + } else { + return res } } - private func popChat(_ ix: Int) { - let chat = chats.remove(at: ix) + func markChatItemsRead(_ cInfo: ChatInfo) { + // update preview + if let chat = getChat(cInfo.id) { + chat.chatStats = ChatStats() + } + // update current chat + if chatId == cInfo.id { + var i = 0 + while i < chatItems.count { + if case .rcvNew = chatItems[i].meta.itemStatus { + chatItems[i].meta.itemStatus = .rcvRead + } + i = i + 1 + } + } + } + + func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) { + // update preview + if let i = getChatIndex(cInfo.id) { + chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1 + } + // update current chat + if chatId == cInfo.id, let j = chatItems.firstIndex(where: { $0.id == cItem.id }) { + chatItems[j].meta.itemStatus = .rcvRead + } + } + + func popChat(_ id: String) { + if let i = getChatIndex(id) { + popChat_(i) + } + } + + private func popChat_(_ i: Int) { + let chat = chats.remove(at: i) chats.insert(chat, at: 0) } @@ -107,14 +185,6 @@ struct User: Decodable, NamedChat { var profile: Profile var activeUser: Bool -// internal init(userId: Int64, userContactId: Int64, localDisplayName: ContactName, profile: Profile, activeUser: Bool) { -// self.userId = userId -// self.userContactId = userContactId -// self.localDisplayName = localDisplayName -// self.profile = profile -// self.activeUser = activeUser -// } - var displayName: String { get { profile.displayName } } var fullName: String { get { profile.fullName } } @@ -226,6 +296,16 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { } } + var ready: Bool { + get { + switch self { + case let .direct(contact): return contact.ready + case let .group(groupInfo): return groupInfo.ready + case let .contactRequest(contactRequest): return contactRequest.ready + } + } + } + var createdAt: Date { switch self { case let .direct(contact): return contact.createdAt @@ -250,6 +330,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat { final class Chat: ObservableObject, Identifiable { @Published var chatInfo: ChatInfo @Published var chatItems: [ChatItem] + @Published var chatStats: ChatStats @Published var serverInfo = ServerInfo(networkStatus: .unknown) struct ServerInfo: Decodable { @@ -297,11 +378,13 @@ final class Chat: ObservableObject, Identifiable { init(_ cData: ChatData) { self.chatInfo = cData.chatInfo self.chatItems = cData.chatItems + self.chatStats = cData.chatStats } - init(chatInfo: ChatInfo, chatItems: [ChatItem] = []) { + init(chatInfo: ChatInfo, chatItems: [ChatItem] = [], chatStats: ChatStats = ChatStats()) { self.chatInfo = chatInfo self.chatItems = chatItems + self.chatStats = chatStats } var id: ChatId { get { chatInfo.id } } @@ -310,10 +393,16 @@ final class Chat: ObservableObject, Identifiable { struct ChatData: Decodable, Identifiable { var chatInfo: ChatInfo var chatItems: [ChatItem] + var chatStats: ChatStats var id: ChatId { get { chatInfo.id } } } +struct ChatStats: Decodable { + var unreadCount: Int = 0 + var minUnreadItemId: Int64 = 0 +} + struct Contact: Identifiable, Decodable, NamedChat { var contactId: Int64 var localDisplayName: ContactName @@ -351,6 +440,7 @@ struct UserContactRequest: Decodable, NamedChat { var id: ChatId { get { "<@\(contactRequestId)" } } var apiId: Int64 { get { contactRequestId } } + var ready: Bool { get { true } } var displayName: String { get { profile.displayName } } var fullName: String { get { profile.fullName } } @@ -370,6 +460,7 @@ struct GroupInfo: Identifiable, Decodable, NamedChat { var id: ChatId { get { "#\(groupId)" } } var apiId: Int64 { get { groupId } } + var ready: Bool { get { true } } var displayName: String { get { groupProfile.displayName } } var fullName: String { get { groupProfile.fullName } } @@ -424,10 +515,17 @@ struct ChatItem: Identifiable, Decodable { var id: Int64 { get { meta.itemId } } - static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String) -> ChatItem { + var timestampText: String { get { meta.timestampText } } + + func isRcvNew() -> Bool { + if case .rcvNew = meta.itemStatus { return true } + return false + } + + static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew) -> ChatItem { ChatItem( chatDir: dir, - meta: CIMeta.getSample(id, ts, text), + meta: CIMeta.getSample(id, ts, text, status), content: .sndMsgContent(msgContent: .text(text)) ) } @@ -455,23 +553,32 @@ struct CIMeta: Decodable { var itemId: Int64 var itemTs: Date var itemText: String + var itemStatus: CIStatus var createdAt: Date - static func getSample(_ id: Int64, _ ts: Date, _ text: String) -> CIMeta { + var timestampText: String { get { SimpleX.timestampText(itemTs) } } + + static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew) -> CIMeta { CIMeta( itemId: id, itemTs: ts, itemText: text, + itemStatus: status, createdAt: ts ) } } + +func timestampText(_ date: Date) -> String { + date.formatted(date: .omitted, time: .shortened) +} + enum CIStatus: Decodable { case sndNew case sndSent case sndErrorAuth - case sndError(agentErrorType: AgentErrorType) + case sndError(agentError: AgentErrorType) case rcvNew case rcvRead } @@ -479,18 +586,25 @@ enum CIStatus: Decodable { enum CIContent: Decodable { case sndMsgContent(msgContent: MsgContent) case rcvMsgContent(msgContent: MsgContent) - // files etc. + case sndFileInvitation(fileId: Int64, filePath: String) + case rcvFileInvitation(rcvFileTransfer: RcvFileTransfer) var text: String { get { switch self { case let .sndMsgContent(mc): return mc.text case let .rcvMsgContent(mc): return mc.text + case .sndFileInvitation: return "sending files is not supported yet" + case .rcvFileInvitation: return "receiving files is not supported yet" } } } } +struct RcvFileTransfer: Decodable { + +} + enum MsgContent { case text(String) case unknown(type: String, text: String) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 60daa0871f..db11cdd4b8 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -31,6 +31,7 @@ enum ChatCommand { case showMyAddress case apiAcceptContact(contactReqId: Int64) case apiRejectContact(contactReqId: Int64) + case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case string(String) var cmdString: String { @@ -40,21 +41,50 @@ enum ChatCommand { case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)" case .startChat: return "/_start" case .apiGetChats: return "/_get chats" - case let .apiGetChat(type, id): return "/_get chat \(type.rawValue)\(id) count=500" - case let .apiSendMessage(type, id, mc): return "/_send \(type.rawValue)\(id) \(mc.cmdString)" + case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100" + case let .apiSendMessage(type, id, mc): return "/_send \(ref(type, id)) \(mc.cmdString)" case .addContact: return "/connect" case let .connect(connReq): return "/connect \(connReq)" - case let .apiDeleteChat(type, id): return "/_delete \(type.rawValue)\(id)" + case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))" case let .updateProfile(profile): return "/profile \(profile.displayName) \(profile.fullName)" case .createMyAddress: return "/address" case .deleteMyAddress: return "/delete_address" case .showMyAddress: return "/show_address" case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)" case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)" + case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)" case let .string(str): return str } } } + + var cmdType: String { + get { + switch self { + case .showActiveUser: return "showActiveUser" + case .createActiveUser: return "createActiveUser" + case .startChat: return "startChat" + case .apiGetChats: return "apiGetChats" + case .apiGetChat: return "apiGetChat" + case .apiSendMessage: return "apiSendMessage" + case .addContact: return "addContact" + case .connect: return "connect" + case .apiDeleteChat: return "apiDeleteChat" + case .updateProfile: return "updateProfile" + case .createMyAddress: return "createMyAddress" + case .deleteMyAddress: return "deleteMyAddress" + case .showMyAddress: return "showMyAddress" + case .apiAcceptContact: return "apiAcceptContact" + case .apiRejectContact: return "apiRejectContact" + case .apiChatRead: return "apiChatRead" + case .string: return "console command" + } + } + } + + func ref(_ type: ChatType, _ id: Int64) -> String { + "\(type.rawValue)\(id)" + } } struct APIResponse: Decodable { @@ -88,6 +118,8 @@ enum ChatResponse: Decodable, Error { case groupEmpty(groupInfo: GroupInfo) case userContactLinkSubscribed case newChatItem(chatItem: AChatItem) + case chatItemUpdated(chatItem: AChatItem) + case cmdOk case chatCmdError(chatError: ChatError) case chatError(chatError: ChatError) @@ -120,6 +152,8 @@ enum ChatResponse: Decodable, Error { case .groupEmpty: return "groupEmpty" case .userContactLinkSubscribed: return "userContactLinkSubscribed" case .newChatItem: return "newChatItem" + case .chatItemUpdated: return "chatItemUpdated" + case .cmdOk: return "cmdOk" case .chatCmdError: return "chatCmdError" case .chatError: return "chatError" } @@ -155,6 +189,8 @@ enum ChatResponse: Decodable, Error { case let .groupEmpty(groupInfo): return String(describing: groupInfo) case .userContactLinkSubscribed: return noDetails case let .newChatItem(chatItem): return String(describing: chatItem) + case let .chatItemUpdated(chatItem): return String(describing: chatItem) + case .cmdOk: return noDetails case let .chatCmdError(chatError): return String(describing: chatError) case let .chatError(chatError): return String(describing: chatError) } @@ -198,7 +234,9 @@ enum TerminalItem: Identifiable { func chatSendCmd(_ cmd: ChatCommand) throws -> ChatResponse { var c = cmd.cmdString.cString(using: .utf8)! + logger.debug("chatSendCmd \(cmd.cmdType)") let resp = chatResponse(chat_send_cmd(getChatCtrl(), &c)!) + logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") DispatchQueue.main.async { ChatModel.shared.terminalItems.append(.cmd(.now, cmd)) ChatModel.shared.terminalItems.append(.resp(.now, resp)) @@ -315,6 +353,12 @@ func apiRejectContactRequest(contactReqId: Int64) throws { throw r } +func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) throws { + let r = try chatSendCmd(.apiChatRead(type: type, id: id, itemRange: itemRange)) + if case .cmdOk = r { return } + throw r +} + func acceptContactRequest(_ contactRequest: UserContactRequest) { do { let contact = try apiAcceptContactRequest(contactReqId: contactRequest.apiId) @@ -334,6 +378,27 @@ func rejectContactRequest(_ contactRequest: UserContactRequest) { } } +func markChatRead(_ chat: Chat) { + do { + let minItemId = chat.chatStats.minUnreadItemId + let itemRange = (minItemId, chat.chatItems.last?.id ?? minItemId) + let cInfo = chat.chatInfo + try apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange) + ChatModel.shared.markChatItemsRead(cInfo) + } catch { + logger.error("markChatRead apiChatRead error: \(error.localizedDescription)") + } +} + +func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) { + do { + try apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id)) + ChatModel.shared.markChatItemRead(cInfo, cItem) + } catch { + logger.error("markChatItemRead apiChatRead error: \(error.localizedDescription)") + } +} + func initializeChat() { do { ChatModel.shared.currentUser = try apiGetActiveUser() @@ -419,6 +484,12 @@ func processReceivedMsg(_ res: ChatResponse) { let cItem = aChatItem.chatItem chatModel.addChatItem(cInfo, cItem) NtfManager.shared.notifyMessageReceived(cInfo, cItem) + case let .chatItemUpdated(aChatItem): + let cInfo = aChatItem.chatInfo + let cItem = aChatItem.chatItem + if chatModel.upsertChatItem(cInfo, cItem) { + NtfManager.shared.notifyMessageReceived(cInfo, cItem) + } default: logger.debug("unsupported event: \(res.responseType)") } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 2334c70e76..392857d7be 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -28,7 +28,6 @@ struct SimpleXApp: App { .onOpenURL { url in logger.debug("ContentView.onOpenURL: \(url)") chatModel.appOpenUrl = url - chatModel.connectViaUrl = true } .onAppear() { initializeChat() diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift new file mode 100644 index 0000000000..774e8aa1f7 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -0,0 +1,43 @@ +// +// ChatInfoToolbar.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 11/02/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +private let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9) +private let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2 ) +struct ChatInfoToolbar: View { + @Environment(\.colorScheme) var colorScheme + @ObservedObject var chat: Chat + + var body: some View { + let cInfo = chat.chatInfo + return HStack { + ChatInfoImage( + chat: chat, + color: colorScheme == .dark + ? chatImageColorDark + : chatImageColorLight + ) + .frame(width: 32, height: 32) + .padding(.trailing, 4) + VStack { + Text(cInfo.displayName).font(.headline) + if cInfo.fullName != "" && cInfo.displayName != cInfo.fullName { + Text(cInfo.fullName).font(.subheadline) + } + } + } + .foregroundColor(.primary) + } +} + +struct ChatInfoToolbar_Previews: PreviewProvider { + static var previews: some View { + ChatInfoToolbar(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 9799db243f..078f05c530 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -10,11 +10,9 @@ import SwiftUI struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel + @ObservedObject var alertManager = AlertManager.shared @ObservedObject var chat: Chat @Binding var showChatInfo: Bool - @State private var showDeleteContactAlert = false - @State private var alertContact: Contact? - @State private var showNetworkStatusInfo = false var body: some View { VStack{ @@ -30,36 +28,27 @@ struct ChatInfoView: View { if case let .direct(contact) = chat.chatInfo { VStack { HStack { - Button { - showNetworkStatusInfo.toggle() - } label: { - serverImage() - Text(chat.serverInfo.networkStatus.statusString) - .foregroundColor(.primary) - } - } - if showNetworkStatusInfo { - Text(chat.serverInfo.networkStatus.statusExplanation) - .font(.subheadline) - .multilineTextAlignment(.center) - .padding(.horizontal, 64) - .padding(.vertical, 8) + serverImage() + Text(chat.serverInfo.networkStatus.statusString) + .foregroundColor(.primary) } + Text(chat.serverInfo.networkStatus.statusExplanation) + .font(.subheadline) + .multilineTextAlignment(.center) + .padding(.horizontal, 64) + .padding(.vertical, 8) Spacer() Button(role: .destructive) { - alertContact = contact - showDeleteContactAlert = true + alertManager.showAlert(deleteContactAlert(contact)) } label: { Label("Delete contact", systemImage: "trash") } .padding() - .alert(isPresented: $showDeleteContactAlert) { - deleteContactAlert(alertContact!) - } } } } + .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } @@ -81,10 +70,8 @@ struct ChatInfoView: View { } catch let error { logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") } - alertContact = nil - }, secondaryButton: .cancel() { - alertContact = nil - } + }, + secondaryButton: .cancel() ) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift new file mode 100644 index 0000000000..5aa6b98eab --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -0,0 +1,47 @@ +// +// CIMetaView.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 11/02/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct CIMetaView: View { + var chatItem: ChatItem + + var body: some View { + HStack(alignment: .center, spacing: 4) { + switch chatItem.meta.itemStatus { + case .sndSent: + statusImage("checkmark", .secondary) + case .sndErrorAuth: + statusImage("multiply", .red) + case .sndError: + statusImage("exclamationmark.triangle.fill", .yellow) + case .rcvNew: + statusImage("circlebadge.fill", Color.accentColor) + default: EmptyView() + } + + Text(chatItem.timestampText) + .font(.caption) + .foregroundColor(.secondary) + } + } + + private func statusImage(_ systemName: String, _ color: Color) -> some View { + Image(systemName: systemName) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(color) + .frame(maxHeight: 8) + } +} + +struct CIMetaView_Previews: PreviewProvider { + static var previews: some View { + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent)) + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift index 2ee4a93e24..34cc4454c2 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift @@ -14,15 +14,13 @@ struct EmojiItemView: View { var body: some View { let sent = chatItem.chatDir.sent - VStack { + VStack(spacing: 1) { Text(chatItem.content.text.trimmingCharacters(in: .whitespaces)) .font(emojiFont) .padding(.top, 8) .padding(.horizontal, 6) .frame(maxWidth: .infinity, alignment: sent ? .trailing : .leading) - Text(getDateFormatter().string(from: chatItem.meta.itemTs)) - .font(.caption) - .foregroundColor(.secondary) + CIMetaView(chatItem: chatItem) .padding(.bottom, 8) .padding(.horizontal, 12) .frame(maxWidth: .infinity, alignment: sent ? .trailing : .leading) @@ -35,7 +33,7 @@ struct EmojiItemView: View { struct EmojiItemView_Previews: PreviewProvider { static var previews: some View { Group{ - EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂")) + EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent)) EmojiItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "👍")) } .previewLayout(.fixed(width: 360, height: 70)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/TextItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/TextItemView.swift index 0a396b16fc..be3cfdaada 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/TextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/TextItemView.swift @@ -12,7 +12,7 @@ private let emailRegex = try! NSRegularExpression(pattern: "^[a-z0-9.!#$%&'*+/=? private let phoneRegex = try! NSRegularExpression(pattern: "^\\+?[0-9\\.\\(\\)\\-]{7,20}$") -private let sentColorLigth = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12) +private let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12) private let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17) private let linkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1) @@ -24,30 +24,22 @@ struct TextItemView: View { var body: some View { let sent = chatItem.chatDir.sent -// let minWidth = min(200, width) let maxWidth = width * 0.78 - let meta = getDateFormatter().string(from: chatItem.meta.itemTs) return ZStack(alignment: .bottomTrailing) { - (messageText(chatItem) + reserveSpaceForMeta(meta)) - .padding(.top, 6) - .padding(.bottom, 7) + (messageText(chatItem) + reserveSpaceForMeta(chatItem.timestampText)) + .padding(.vertical, 6) .padding(.horizontal, 12) .frame(minWidth: 0, alignment: .leading) -// .foregroundColor(sent ? .white : .primary) .textSelection(.enabled) - Text(meta) - .font(.caption) - .foregroundColor(.secondary) -// .foregroundColor(sent ? Color(uiColor: .secondarySystemBackground) : .secondary) - .padding(.bottom, 4) - .padding(.horizontal, 12) + CIMetaView(chatItem: chatItem) + .padding(.trailing, 12) + .padding(.bottom, 6) } -// .background(sent ? .blue : Color(uiColor: .tertiarySystemGroupedBackground)) .background( sent - ? (colorScheme == .light ? sentColorLigth : sentColorDark) + ? (colorScheme == .light ? sentColorLight : sentColorDark) : Color(uiColor: .tertiarySystemGroupedBackground) ) .cornerRadius(18) @@ -57,6 +49,13 @@ struct TextItemView: View { maxHeight: .infinity, alignment: sent ? .trailing : .leading ) + .onTapGesture { + switch chatItem.meta.itemStatus { + case .sndErrorAuth: msgDeliveryError("Most likely this contact has deleted the connection with you.") + case let .sndError(agentError): msgDeliveryError("Unexpected error: \(String(describing: agentError))") + default: return + } + } } private func messageText(_ chatItem: ChatItem) -> Text { @@ -82,10 +81,9 @@ struct TextItemView: View { } private func reserveSpaceForMeta(_ meta: String) -> Text { - Text(AttributedString(" \(meta)", attributes: AttributeContainer([ - .font: UIFont.preferredFont(forTextStyle: .caption1) as Any, - .foregroundColor: UIColor.clear as Any, - ]))) + Text(" \(meta)") + .font(.caption) + .foregroundColor(.clear) } private func wordToText(_ s: String.SubSequence) -> Text { @@ -126,6 +124,13 @@ struct TextItemView: View { private func mdText(_ s: String.SubSequence) -> Text { Text(s[s.index(s.startIndex, offsetBy: 1).. some View { - NavigationLink( + NavLinkPlain( tag: chat.chatInfo.id, selection: $chatModel.chatId, destination: { chatView() }, - label: { ChatPreviewView(chat: chat) } + label: { ChatPreviewView(chat: chat) }, + disabled: !contact.ready ) - .disabled(!contact.ready) - .swipeActions(edge: .trailing, allowsFullSwipe: true) { + .swipeActions(edge: .leading) { + if chat.chatStats.unreadCount > 0 { + markReadButton() + } + } + .swipeActions(edge: .trailing) { Button(role: .destructive) { - alertContact = contact - showDeleteContactAlert = true + AlertManager.shared.showAlert(deleteContactAlert(contact)) } label: { Label("Delete", systemImage: "trash") } } - .alert(isPresented: $showDeleteContactAlert) { - deleteContactAlert(alertContact!) - } .frame(height: 80) } private func groupNavLink(_ groupInfo: GroupInfo) -> some View { - NavigationLink( + NavLinkPlain( tag: chat.chatInfo.id, selection: $chatModel.chatId, destination: { chatView() }, - label: { ChatPreviewView(chat: chat) } + label: { ChatPreviewView(chat: chat) }, + disabled: !groupInfo.ready ) + .swipeActions(edge: .leading) { + if chat.chatStats.unreadCount > 0 { + markReadButton() + } + } .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { - alertGroupInfo = groupInfo - showDeleteGroupAlert = true + AlertManager.shared.showAlert(deleteGroupAlert(groupInfo)) } label: { Label("Delete", systemImage: "trash") } } - .alert(isPresented: $showDeleteGroupAlert) { - deleteGroupAlert(alertGroupInfo!) - } .frame(height: 80) } + private func markReadButton() -> some View { + Button { + markChatRead(chat) + } label: { + Label("Read", systemImage: "checkmark") + } + .tint(Color.accentColor) + } + private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View { ContactRequestView(contactRequest: contactRequest) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { acceptContactRequest(contactRequest) } label: { Label("Accept", systemImage: "checkmark") } - .tint(.blue) + .tint(Color.accentColor) Button(role: .destructive) { - alertContactRequest = contactRequest - showContactRequestAlert = true + AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest)) } label: { Label("Reject", systemImage: "multiply") } } - .alert(isPresented: $showContactRequestAlert) { - contactRequestAlert(alertContactRequest!) - } .frame(height: 80) .onTapGesture { showContactRequestDialog = true } .confirmationDialog("Connection request", isPresented: $showContactRequestDialog, titleVisibility: .visible) { @@ -123,10 +124,8 @@ struct ChatListNavLink: View { } catch let error { logger.error("ChatListNavLink.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") } - alertContact = nil - }, secondaryButton: .cancel() { - alertContact = nil - } + }, + secondaryButton: .cancel() ) } @@ -137,16 +136,14 @@ struct ChatListNavLink: View { ) } - private func contactRequestAlert(_ contactRequest: UserContactRequest) -> Alert { + private func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert { Alert( title: Text("Reject contact request"), message: Text("The sender will NOT be notified"), primaryButton: .destructive(Text("Reject")) { rejectContactRequest(contactRequest) - alertContactRequest = nil - }, secondaryButton: .cancel { - alertContactRequest = nil - } + }, + secondaryButton: .cancel() ) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index af43f7badc..e6777bd0c0 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -10,8 +10,6 @@ import SwiftUI struct ChatListView: View { @EnvironmentObject var chatModel: ChatModel - @State private var connectAlert = false - @State private var connectError: Error? // not really used in this view @State private var showSettings = false @@ -32,6 +30,19 @@ struct ChatListView: View { } ForEach(chatModel.chats) { chat in ChatListNavLink(chat: chat) + .padding(.trailing, -16) + } + } + .onChange(of: chatModel.chatId) { _ in + if chatModel.chatId == nil, let chatId = chatModel.chatToTop { + chatModel.chatToTop = nil + chatModel.popChat(chatId) + } + } + .onChange(of: chatModel.appOpenUrl) { _ in + if let url = chatModel.appOpenUrl { + chatModel.appOpenUrl = nil + AlertManager.shared.showAlert(connectViaUrlAlert(url)) } } .offset(x: -8) @@ -45,50 +56,36 @@ struct ChatListView: View { NewChatButton() } } - .alert(isPresented: $chatModel.connectViaUrl) { connectViaUrlAlert() } } .navigationViewStyle(.stack) - .alert(isPresented: $connectAlert) { connectionErrorAlert() } } - private func connectViaUrlAlert() -> Alert { - logger.debug("ChatListView.connectViaUrlAlert") - if let url = chatModel.appOpenUrl { - var path = url.path - logger.debug("ChatListView.connectViaUrlAlert path: \(path)") - if (path == "/contact" || path == "/invitation") { - path.removeFirst() - let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") - return Alert( - title: Text("Connect via \(path) link?"), - message: Text("Your profile will be sent to the contact that you received this link from: \(link)"), - primaryButton: .default(Text("Connect")) { + private func connectViaUrlAlert(_ url: URL) -> Alert { + var path = url.path + logger.debug("ChatListView.connectViaUrlAlert path: \(path)") + if (path == "/contact" || path == "/invitation") { + path.removeFirst() + let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") + return Alert( + title: Text("Connect via \(path) link?"), + message: Text("Your profile will be sent to the contact that you received this link from: \(link)"), + primaryButton: .default(Text("Connect")) { + DispatchQueue.main.async { do { try apiConnect(connReq: link) } catch { - connectAlert = true - connectError = error - logger.debug("ChatListView.connectViaUrlAlert: apiConnect error: \(error.localizedDescription)") + let err = error.localizedDescription + AlertManager.shared.showAlertMsg(title: "Connection error", message: err) + logger.debug("ChatListView.connectViaUrlAlert: apiConnect error: \(err)") } - chatModel.appOpenUrl = nil - }, secondaryButton: .cancel() { - chatModel.appOpenUrl = nil } - ) - } else { - return Alert(title: Text("Error: URL is invalid")) - } + }, + secondaryButton: .cancel() + ) } else { - return Alert(title: Text("Error: URL not available")) + return Alert(title: Text("Error: URL is invalid")) } } - - private func connectionErrorAlert() -> Alert { - Alert( - title: Text("Connection error"), - message: Text(connectError?.localizedDescription ?? "") - ) - } } struct ChatListView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 741b55e9a2..c82f520bb4 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -15,6 +15,7 @@ struct ChatPreviewView: View { var body: some View { let cItem = chat.chatItems.last + let unread = chat.chatStats.unreadCount return HStack(spacing: 8) { ZStack(alignment: .bottomLeading) { ChatInfoImage(chat: chat) @@ -35,21 +36,36 @@ struct ChatPreviewView: View { Text(chat.chatInfo.chatViewName) .font(.title3) .fontWeight(.bold) + .foregroundColor(chat.chatInfo.ready ? .primary : .secondary) .frame(maxHeight: .infinity, alignment: .topLeading) Spacer() - Text(getDateFormatter().string(from: cItem?.meta.itemTs ?? chat.chatInfo.createdAt)) + Text(cItem?.timestampText ?? timestampText(chat.chatInfo.createdAt)) .font(.subheadline) .frame(minWidth: 60, alignment: .trailing) .foregroundColor(.secondary) + .padding(.top, 4) + } .padding(.top, 4) .padding(.horizontal, 8) if let cItem = cItem { - Text(chatItemText(cItem)) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading) - .padding([.leading, .trailing], 8) - .padding(.bottom, 4) + ZStack(alignment: .topTrailing) { + (itemStatusMark(cItem) + Text(chatItemText(cItem))) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading) + .padding(.leading, 8) + .padding(.trailing, 32) + .padding(.bottom, 4) + if unread > 0 { + Text(unread > 9 ? "" : "\(unread)") + .font(.caption) + .foregroundColor(.white) + .frame(width: 18, height: 18) + .background(Color.accentColor) //Color(.sRGB, red: 0, green: 0.57, blue: 1, opacity: 0.89)) + .cornerRadius(10) + } + } + .padding(.trailing, 8) } else if case let .direct(contact) = chat.chatInfo, !contact.ready { Text("Connecting...") @@ -61,6 +77,20 @@ struct ChatPreviewView: View { } } + private func itemStatusMark(_ cItem: ChatItem) -> Text { + switch cItem.meta.itemStatus { + case .sndErrorAuth: + return Text(Image(systemName: "multiply")) + .font(.caption) + .foregroundColor(.red) + Text(" ") + case .sndError: + return Text(Image(systemName: "exclamationmark.triangle.fill")) + .font(.caption) + .foregroundColor(.yellow) + Text(" ") + default: return Text("") + } + } + private func chatItemText(_ cItem: ChatItem) -> String { let t = cItem.content.text if case let .groupRcv(groupMember) = cItem.chatDir { @@ -79,11 +109,12 @@ struct ChatPreviewView_Previews: PreviewProvider { )) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, - chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)] )) ChatPreviewView(chat: 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.")] + 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.")], + chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0) )) } .previewLayout(.fixed(width: 360, height: 78)) diff --git a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift index 2acd47c707..d66af40c6b 100644 --- a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift @@ -28,9 +28,9 @@ struct ContactRequestView: View { .padding(.top, 4) .frame(maxHeight: .infinity, alignment: .topLeading) Spacer() - Text(getDateFormatter().string(from: contactRequest.createdAt)) + Text(timestampText(contactRequest.createdAt)) .font(.subheadline) - .padding(.trailing, 28) + .padding(.trailing, 8) .padding(.top, 4) .frame(minWidth: 60, alignment: .trailing) .foregroundColor(.secondary) diff --git a/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift b/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift new file mode 100644 index 0000000000..fb12292b6a --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift @@ -0,0 +1,35 @@ +// +// NavLinkPlain.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 11/02/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct NavLinkPlain: View { + @State var tag: V + @Binding var selection: V? + @ViewBuilder var destination: () -> Destination + @ViewBuilder var label: () -> Label + var disabled = false + + var body: some View { + ZStack { + Button("") { selection = tag } + .disabled(disabled) + label() + } + .background { + NavigationLink("", tag: tag, selection: $selection, destination: destination) + .hidden() + } + } +} + +//struct NavLinkPlain_Previews: PreviewProvider { +// static var previews: some View { +// NavLinkPlain() +// } +//} diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift new file mode 100644 index 0000000000..15883f8340 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -0,0 +1,18 @@ +// +// ShareSheet.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 30/01/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +func showShareSheet(items: [Any]) { + let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene + if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first, + let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController { + let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil) + presentedViewController.present(activityViewController, animated: true) + } +} diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift index 0f0b1521c1..f6d62deb26 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactView.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactView.swift @@ -11,7 +11,6 @@ import CoreImage.CIFilterBuiltins struct AddContactView: View { var connReqInvitation: String - @State private var shareInvitation = false var body: some View { VStack { @@ -27,11 +26,12 @@ struct AddContactView: View { .font(.subheadline) .multilineTextAlignment(.center) .padding(.horizontal) - Button { shareInvitation = true } label: { - Label("Share", systemImage: "square.and.arrow.up") + Button { + showShareSheet(items: [connReqInvitation]) + } label: { + Label("Share invitation link", systemImage: "square.and.arrow.up") } .padding() - .shareSheet(isPresented: $shareInvitation, items: [connReqInvitation]) } } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift index 0064ac9292..95add21038 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift @@ -11,12 +11,8 @@ import SwiftUI struct NewChatButton: View { @State private var showAddChat = false @State private var addContact = false - @State private var addContactAlert = false - @State private var addContactError: Error? @State private var connReqInvitation: String = "" @State private var connectContact = false - @State private var connectAlert = false - @State private var connectError: Error? @State private var createGroup = false var body: some View { @@ -32,15 +28,9 @@ struct NewChatButton: View { .sheet(isPresented: $addContact, content: { AddContactView(connReqInvitation: connReqInvitation) }) - .alert(isPresented: $addContactAlert) { - connectionError(addContactError) - } .sheet(isPresented: $connectContact, content: { connectContactSheet() }) - .alert(isPresented: $connectAlert) { - connectionError(connectError) - } .sheet(isPresented: $createGroup, content: { CreateGroupView() }) } @@ -49,8 +39,7 @@ struct NewChatButton: View { connReqInvitation = try apiAddContact() addContact = true } catch { - addContactAlert = true - addContactError = error + connectionErrorAlert(error) logger.error("NewChatButton.addContactAction apiAddContact error: \(error.localizedDescription)") } } @@ -58,18 +47,14 @@ struct NewChatButton: View { func connectContactSheet() -> some View { ConnectContactView(completed: { err in connectContact = false - if err != nil { - connectAlert = true - connectError = err + if let error = err { + connectionErrorAlert(error) } }) } - func connectionError(_ error: Error?) -> Alert { - Alert( - title: Text("Connection error"), - message: Text(error?.localizedDescription ?? "") - ) + func connectionErrorAlert(_ error: Error) { + AlertManager.shared.showAlertMsg(title: "Connection error", message: error.localizedDescription) } } diff --git a/apps/ios/Shared/Views/NewChat/ShareSheet.swift b/apps/ios/Shared/Views/NewChat/ShareSheet.swift deleted file mode 100644 index 3b9dbcb5e1..0000000000 --- a/apps/ios/Shared/Views/NewChat/ShareSheet.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ShareSheet.swift -// SimpleX -// -// Created by Evgeny Poberezkin on 30/01/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI - -extension UIApplication { - static let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first - static let keyWindowScene = shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene -} - -extension View { - func shareSheet(isPresented: Binding, items: [Any]) -> some View { - guard isPresented.wrappedValue else { return self } - let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil) - let presentedViewController = UIApplication.keyWindow?.rootViewController?.presentedViewController ?? UIApplication.keyWindow?.rootViewController - activityViewController.completionWithItemsHandler = { _, _, _, _ in isPresented.wrappedValue = false } - presentedViewController?.present(activityViewController, animated: true) - return self - } -} - -struct ShareSheetTest: View { - @State private var isPresentingShareSheet = false - - var body: some View { - Button("Show Share Sheet") { isPresentingShareSheet = true } - .shareSheet(isPresented: $isPresentingShareSheet, items: ["Share me!"]) - } -} - -struct ShareSheetTest_Previews: PreviewProvider { - static var previews: some View { - ShareSheetTest() - } -} diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 74015c2def..9d561a9c26 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -8,6 +8,8 @@ import SwiftUI +private let terminalFont = Font.custom("Menlo", size: 16) + struct TerminalView: View { @EnvironmentObject var chatModel: ChatModel @State var inProgress: Bool = false @@ -31,6 +33,7 @@ struct TerminalView: View { Text(item.label) .frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading) } + .font(terminalFont) .padding(.horizontal) } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 35d969e1f0..e48dececc7 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -11,6 +11,7 @@ import SwiftUI let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")! struct SettingsView: View { + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var chatModel: ChatModel @Binding var showSettings: Bool @@ -95,7 +96,7 @@ struct SettingsView: View { } } HStack { - Image("github") + Image(colorScheme == .dark ? "github_light" : "github") .resizable() .frame(width: 24, height: 24) .padding(.trailing, 8) diff --git a/apps/ios/Shared/Views/UserSettings/UserAddress.swift b/apps/ios/Shared/Views/UserSettings/UserAddress.swift index 7d6c8cca65..6ed2d03744 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddress.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddress.swift @@ -10,7 +10,6 @@ import SwiftUI struct UserAddress: View { @EnvironmentObject var chatModel: ChatModel - @State private var shareAddressLink = false @State private var deleteAddressAlert = false var body: some View { @@ -20,13 +19,14 @@ struct UserAddress: View { if let userAdress = chatModel.userAddress { QRCode(uri: userAdress) HStack { - Button { shareAddressLink = true } label: { + Button { + showShareSheet(items: [userAdress]) + } label: { Label("Share link", systemImage: "square.and.arrow.up") } .padding() - .shareSheet(isPresented: $shareAddressLink, items: [userAdress]) - Button { deleteAddressAlert = true } label: { + Button(role: .destructive) { deleteAddressAlert = true } label: { Label("Delete address", systemImage: "trash") } .padding() @@ -44,7 +44,6 @@ struct UserAddress: View { }, secondaryButton: .cancel() ) } - .shareSheet(isPresented: $shareAddressLink, items: [userAdress]) } .frame(maxWidth: .infinity) } else { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 752c52f814..08139a2870 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -34,6 +34,12 @@ 5C75059E27B5CD9300BE3227 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059927B5CD9300BE3227 /* libffi.a */; }; 5C75059F27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059A27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a */; }; 5C7505A027B5CD9300BE3227 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059B27B5CD9300BE3227 /* libgmpxx.a */; }; + 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; + 5C7505A327B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; + 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; }; + 5C7505A627B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; }; + 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; }; + 5C7505A927B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; }; 5C764E80279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; }; 5C764E81279C7276000C6508 /* dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E7F279C7276000C6508 /* dummy.m */; }; 5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C764E7B279C71D4000C6508 /* libiconv.tbd */; }; @@ -127,6 +133,9 @@ 5C75059927B5CD9300BE3227 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5C75059A27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a"; sourceTree = ""; }; 5C75059B27B5CD9300BE3227 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = ""; }; + 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = ""; }; + 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = ""; }; 5C764E7B279C71D4000C6508 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; }; 5C764E7C279C71DB000C6508 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; 5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX (iOS)-Bridging-Header.h"; sourceTree = ""; }; @@ -225,6 +234,7 @@ children = ( 5CE4407427ADB657007B033A /* ChatItem */, 5C2E260E27A30FDC00F70299 /* ChatView.swift */, + 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */, 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */, 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */, 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */, @@ -270,6 +280,8 @@ isa = PBXGroup; children = ( 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */, + 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */, + 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */, ); path = Helpers; sourceTree = ""; @@ -348,7 +360,6 @@ 5CCD403627A5F9A200368C90 /* ConnectContactView.swift */, 5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */, 5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */, - 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */, ); path = NewChat; sourceTree = ""; @@ -380,6 +391,7 @@ isa = PBXGroup; children = ( 5CE4407527ADB66A007B033A /* TextItemView.swift */, + 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */, 5CE4407827ADB701007B033A /* EmojiItemView.swift */, ); path = ChatItem; @@ -559,6 +571,7 @@ 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */, 5C764E80279C7276000C6508 /* dummy.m in Sources */, + 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */, 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */, @@ -570,6 +583,8 @@ 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */, 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */, + 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */, + 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */, 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */, 5CA05A4C27974EB60002BEB4 /* WelcomeView.swift in Sources */, 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */, @@ -599,6 +614,7 @@ 5CE4407A27ADB701007B033A /* EmojiItemView.swift in Sources */, 5C5346A927B59A6A004DF848 /* ChatHelp.swift in Sources */, 5C764E81279C7276000C6508 /* dummy.m in Sources */, + 5C7505A927B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, 5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */, 5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5C35CFCC27B2E91D00FB6C6D /* NtfManager.swift in Sources */, @@ -610,6 +626,8 @@ 5CB9250E27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 5CA059EE279559F40002BEB4 /* ContentView.swift in Sources */, 5CCD403527A5F6DF00368C90 /* AddContactView.swift in Sources */, + 5C7505A627B679EE00BE3227 /* NavLinkPlain.swift in Sources */, + 5C7505A327B65FDB00BE3227 /* CIMetaView.swift in Sources */, 5C35CFC927B2782E00FB6C6D /* BGManager.swift in Sources */, 5CA05A4D27974EB60002BEB4 /* WelcomeView.swift in Sources */, 5C2E261027A30FDC00F70299 /* ChatView.swift in Sources */,