From 9d9bb68d50b17e618786413f459f9d30ebaf0e71 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 12 Feb 2022 15:59:43 +0000 Subject: [PATCH] iOS: show message sent/unread status (#293) * light github image for dark mode * show message received status, remove chevrons in chat list * show unread message status * add message send error mark * refactor alerts to use AlertManager * show alert message on tapping undelivered message, simplify text-only alerts --- .../github.imageset/Contents.json | 6 +- .../github.imageset/github32px.png | Bin 1714 -> 0 bytes .../github.imageset/github64px-1.png | Bin 2625 -> 0 bytes .../github.imageset/github64px.png | Bin 2625 -> 0 bytes .../github.imageset/github_1x.png | Bin 0 -> 2604 bytes .../github.imageset/github_2x.png | Bin 0 -> 4555 bytes .../github.imageset/github_3x.png | Bin 0 -> 5971 bytes .../github_light.imageset/Contents.json | 23 +++ .../github_light.imageset/github_light_1x.png | Bin 0 -> 2145 bytes .../github_light.imageset/github_light_2x.png | Bin 0 -> 3528 bytes .../github_light.imageset/github_light_3x.png | Bin 0 -> 4745 bytes apps/ios/Shared/ContentView.swift | 49 +++-- apps/ios/Shared/Model/ChatModel.swift | 168 +++++++++++++++--- apps/ios/Shared/Model/SimpleXAPI.swift | 77 +++++++- apps/ios/Shared/SimpleXApp.swift | 1 - .../Shared/Views/Chat/ChatInfoToolbar.swift | 43 +++++ apps/ios/Shared/Views/Chat/ChatInfoView.swift | 39 ++-- .../Views/Chat/ChatItem/CIMetaView.swift | 47 +++++ .../Views/Chat/ChatItem/EmojiItemView.swift | 8 +- .../Views/Chat/ChatItem/TextItemView.swift | 45 ++--- apps/ios/Shared/Views/Chat/ChatView.swift | 56 +++--- .../Views/ChatList/ChatListNavLink.swift | 73 ++++---- .../Shared/Views/ChatList/ChatListView.swift | 65 ++++--- .../Views/ChatList/ChatPreviewView.swift | 45 ++++- .../Views/ChatList/ContactRequestView.swift | 4 +- .../Shared/Views/Helpers/NavLinkPlain.swift | 35 ++++ .../ios/Shared/Views/Helpers/ShareSheet.swift | 18 ++ .../Shared/Views/NewChat/AddContactView.swift | 8 +- .../Shared/Views/NewChat/NewChatButton.swift | 25 +-- .../ios/Shared/Views/NewChat/ShareSheet.swift | 40 ----- apps/ios/Shared/Views/TerminalView.swift | 3 + .../Views/UserSettings/SettingsView.swift | 3 +- .../Views/UserSettings/UserAddress.swift | 9 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 20 ++- 34 files changed, 635 insertions(+), 275 deletions(-) delete mode 100644 apps/ios/Shared/Assets.xcassets/github.imageset/github32px.png delete mode 100644 apps/ios/Shared/Assets.xcassets/github.imageset/github64px-1.png delete mode 100644 apps/ios/Shared/Assets.xcassets/github.imageset/github64px.png create mode 100644 apps/ios/Shared/Assets.xcassets/github.imageset/github_1x.png create mode 100644 apps/ios/Shared/Assets.xcassets/github.imageset/github_2x.png create mode 100644 apps/ios/Shared/Assets.xcassets/github.imageset/github_3x.png create mode 100644 apps/ios/Shared/Assets.xcassets/github_light.imageset/Contents.json create mode 100644 apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_1x.png create mode 100644 apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_2x.png create mode 100644 apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_3x.png create mode 100644 apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift create mode 100644 apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift create mode 100644 apps/ios/Shared/Views/Helpers/NavLinkPlain.swift create mode 100644 apps/ios/Shared/Views/Helpers/ShareSheet.swift delete mode 100644 apps/ios/Shared/Views/NewChat/ShareSheet.swift 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 8b25551a97921681334176ee143b41510a117d86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1714 zcmaJ?X;2eq7*4oFu!ne{XxAht2qc?8LXr|_LPCfTpaBK7K$c{I0Ld=NLIOeuC;@2) zZ$K%a)k+m-s0>xHmKxL%0V&0TRzzznhgyqrIC$F)0{WwLXLrBvd*^wc_uSc%h%m9E z{W5z3f#4_!7RvAyFh6!S_*<8qJ%KOIm?#E|L=rJQq=gB5C6WLG5;c?r%V0>EmEH#X z5eSwPRa6WXBMs#$5H%GtW2go-in9p>zW@UYDNNWc^XOXZQ? z1QjEV00I#$3^1wQUJ8&-2UsjB-G|9y(LDhMNN3PM{APL4eYi{(m*ERcUnJa{R+-3^ z34^A6;U^v`8N*O6ji%S@sd{fJqD`XFIUJ5zgTe5^5nj414F(y!G&=H(f)Lgzv?>%+ zAsWD}2qhpH7>|TU`X&W6IxDNuO_vET7|j5oG&&VDr!)hUO8+0KR?nh!m<)a!?|%yG zqOwq!CWCcIhE{<$E|F|@g>nP6FoYr6C<8>D?ID9%&5J(4oSbR1I^byW*g@__U z4QsF&uJSEcFeleM3~ChjEQGbHOjsGDMbyAl(p=Ttv9RaVo8~I#js@@Y9C^_2U})yn zzSHU%6FxuY?d;&65MyR({^lU*3$z$ZllDb(o&<7d;A_`h2U+3~BJ2Hv`{W}KEU801#cv_B|9Cm!ynR{S`AMsSn z;7E=B;mb!wx$L;S>yGXG^6=&WlQn9$s?&L%Y1D8TI^MlKB1DqsEng$>f4=xYWBoPI z_S1p!sJ#d2?YI4kPA{k}Eby?F=f-J9zIc`YDl^pzjVm~9ebE?Hn?t0Nx+la|D0MB; z9)2xv1G>a1|A9kQ>~DV<=X3-4yC&n!m8-3K#P z{X@0zRuQsy$+N ziSCoLJU{Z$nQy4A4Y5UJ07$5FA~qL2%Q+cLaqDU?Lz3?=BC5;Nk6BbTmmceEaM>-Z zi>O&-dSE=%ex;vcvCOk{*JQ5^_4M z4lW7%l9IqY(z7pV(?I@@8=KPFO82)O{VDI18-*d-k$YmI^XiuPs_LuFw<^ZcD}yP5 c*NrbeloN*74g`U%%F6r~k%+>C^#XapzmV0H-2eap 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 182a1a3f734fc1b7d712c68b04c29bad9460d6cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2625 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 W4I>>0bz>}E>ge79vm#m=SD{u?K%I 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4cef050214f180ed0a51bda0aca8be24149315a6 GIT binary patch literal 4555 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 ZFCvD7r_yvMWj~&DN>Xo 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a9881b6bca3c1436848426fe402a8fa8aa13be80 GIT binary patch literal 4745 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 literal 0 HcmV?d00001 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 */,