diff --git a/.gitignore b/.gitignore
index 8b4cc543b6..e645225e93 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,12 +5,6 @@
*.so
*.dylib
-# Test binary, built with `go test -c`
-*.test
-
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
# Dependency directories (remove the comment below to include it)
# vendor/
@@ -42,9 +36,9 @@ cabal.project.local~
.ghc.environment.*
stack.yaml.lock
-# Idris
-*.ibc
-
-# chat database
+# Chat database
*.db
*.db.bak
+
+# Temporary test files
+tests/tmp
diff --git a/README.md b/README.md
index 370965983b..602017ea9a 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
# SimpleX Chat
-SimpleX - the most private and secure open-source chat and applications platform - now with double-ratchet E2E encryption.
+SimpleX - private and secure open-source chat and application platform - public beta for iOS now available!
[](https://github.com/simplex-chat/simplex-chat/actions?query=workflow%3Abuild)
[](https://github.com/simplex-chat/simplex-chat/releases)
@@ -10,11 +10,11 @@ SimpleX - the most private and secure open-source chat and applications platform
[](https://twitter.com/simplexchat)
[](https://www.reddit.com/r/SimpleXChat)
-SimpleX Chat is a terminal (command line) UI using [SimpleXMQ](https://github.com/simplex-chat/simplexmq) message broker.
+SimpleX Chat apps (both terminal UI and [iOS public beta](https://testflight.apple.com/join/DWuT2LQu)) use [SimpleXMQ](https://github.com/simplex-chat/simplexmq) message broker.
See [SimpleX overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information on platform objectives and technical design.
-**v1.0.0 is released: [read announcement here](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20220112-simplex-chat-v1-released.md)**
+***SimpleX Chat [public beta for iOS 15 is available via TestFlight](https://testflight.apple.com/join/DWuT2LQu)** - it will help us a lot if you test it! [See the announcement here](https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220214-simplex-chat-ios-public-beta.md).*
### :zap: Quick installation
diff --git a/apps/android/.idea/misc.xml b/apps/android/.idea/misc.xml
index 0daaee7f8b..345106f12f 100644
--- a/apps/android/.idea/misc.xml
+++ b/apps/android/.idea/misc.xml
@@ -5,7 +5,7 @@
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..24cd35b187 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,50 @@ 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) {
+ logger.debug("AlertManager.showAlert")
+ 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/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift
index 904357eec6..073c91c0d2 100644
--- a/apps/ios/Shared/Model/NtfManager.swift
+++ b/apps/ios/Shared/Model/NtfManager.swift
@@ -56,10 +56,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
let model = ChatModel.shared
if UIApplication.shared.applicationState == .active {
switch content.categoryIdentifier {
- case ntfCategoryContactRequest:
- return [.sound, .banner, .list]
- case ntfCategoryContactConnected:
- return model.chatId == nil ? [.sound, .list] : [.sound, .banner, .list]
case ntfCategoryMessageReceived:
if model.chatId == nil {
// in the chat list
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 60daa0871f..051f6df905 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)")
}
@@ -504,6 +575,7 @@ enum ChatErrorType: Decodable {
case chatNotStarted
case invalidConnReq
case invalidChatMessage(message: String)
+ case contactNotReady(contact: Contact)
case contactGroups(contact: Contact, groupNames: [GroupName])
case groupUserRole
case groupContactRole(contactName: ContactName)
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..7a4ff3b5f2 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift
@@ -13,16 +13,15 @@ struct EmojiItemView: View {
var body: some View {
let sent = chatItem.chatDir.sent
+ let s = chatItem.content.text.trimmingCharacters(in: .whitespaces)
- VStack {
- Text(chatItem.content.text.trimmingCharacters(in: .whitespaces))
- .font(emojiFont)
+ VStack(spacing: 1) {
+ Text(s)
+ .font(s.count < 4 ? largeEmojiFont : mediumEmojiFont)
.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 +34,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).. Bool {
func isShortEmoji(_ str: String) -> Bool {
let s = str.trimmingCharacters(in: .whitespaces)
- return s.count > 0 && s.count <= 4 && s.allSatisfy(isEmoji)
+ return s.count > 0 && s.count <= 5 && s.allSatisfy(isEmoji)
}
-let emojiFont = Font.custom("Emoji", size: 48, relativeTo: .largeTitle)
+let largeEmojiFont = Font.custom("Emoji", size: 48, relativeTo: .largeTitle)
+let mediumEmojiFont = Font.custom("Emoji", size: 36, relativeTo: .largeTitle)
diff --git a/apps/ios/Shared/Views/Chat/SendMessageView.swift b/apps/ios/Shared/Views/Chat/SendMessageView.swift
index 60e9144568..af639999fc 100644
--- a/apps/ios/Shared/Views/Chat/SendMessageView.swift
+++ b/apps/ios/Shared/Views/Chat/SendMessageView.swift
@@ -73,7 +73,11 @@ struct SendMessageView: View {
func updateHeight(_ g: GeometryProxy) -> Color {
DispatchQueue.main.async {
teHeight = min(max(g.frame(in: .local).size.height, minHeight), maxHeight)
- teFont = isShortEmoji(message) ? emojiFont : .body
+ teFont = isShortEmoji(message)
+ ? message.count < 4
+ ? largeEmojiFont
+ : mediumEmojiFont
+ : .body
}
return Color.clear
}
diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
index f707660477..75da1fea98 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
@@ -11,14 +11,7 @@ import SwiftUI
struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel
@State var chat: Chat
-
- @State private var showDeleteContactAlert = false
- @State private var showDeleteGroupAlert = false
- @State private var showContactRequestAlert = false
@State private var showContactRequestDialog = false
- @State private var alertContact: Contact?
- @State private var alertGroupInfo: GroupInfo?
- @State private var alertContactRequest: UserContactRequest?
var body: some View {
switch chat.chatInfo {
@@ -46,64 +39,72 @@ struct ChatListNavLink: View {
}
private func contactNavLink(_ contact: Contact) -> 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..4f5d044716 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift
@@ -10,15 +10,14 @@ 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
+ @State private var searchText = ""
var user: User
var body: some View {
- NavigationView {
+ let v = NavigationView {
List {
if chatModel.chats.isEmpty {
VStack(alignment: .leading) {
@@ -30,13 +29,27 @@ struct ChatListView: View {
.padding(.leading)
}
}
- ForEach(chatModel.chats) { chat in
+ ForEach(filteredChats()) { 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)
.listStyle(.plain)
.navigationTitle(chatModel.chats.isEmpty ? "Welcome \(user.displayName)!" : "Your chats")
+ .navigationBarTitleDisplayMode(chatModel.chats.count > 8 ? .inline : .large)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
SettingsButton()
@@ -45,49 +58,49 @@ 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")) {
- do {
- try apiConnect(connReq: link)
- } catch {
- connectAlert = true
- connectError = error
- logger.debug("ChatListView.connectViaUrlAlert: apiConnect error: \(error.localizedDescription)")
- }
- chatModel.appOpenUrl = nil
- }, secondaryButton: .cancel() {
- chatModel.appOpenUrl = nil
- }
- )
- } else {
- return Alert(title: Text("Error: URL is invalid"))
- }
+ if chatModel.chats.count > 8 {
+ v.searchable(text: $searchText)
} else {
- return Alert(title: Text("Error: URL not available"))
+ v
}
}
- private func connectionErrorAlert() -> Alert {
- Alert(
- title: Text("Connection error"),
- message: Text(connectError?.localizedDescription ?? "")
- )
+ private func filteredChats() -> [Chat] {
+ let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
+ return s == ""
+ ? chatModel.chats
+ : chatModel.chats.filter { $0.chatInfo.chatViewName.localizedLowercase.contains(s) }
+ }
+
+ 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)
+ connectionReqSentAlert(path == "contact" ? .contact : .invitation)
+ } catch {
+ let err = error.localizedDescription
+ AlertManager.shared.showAlertMsg(title: "Connection error", message: err)
+ logger.debug("ChatListView.connectViaUrlAlert: apiConnect error: \(err)")
+ }
+ }
+ },
+ secondaryButton: .cancel()
+ )
+ } else {
+ return Alert(title: Text("Error: URL is invalid"))
+ }
}
}
diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
index 741b55e9a2..0d7d2608ee 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,37 @@ 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, 36)
+ .padding(.bottom, 4)
+ if unread > 0 {
+ Text(unread > 999 ? "\(unread / 1000)k" : "\(unread)")
+ .font(.caption)
+ .foregroundColor(.white)
+ .padding(.horizontal, 4)
+ .frame(minWidth: 18, minHeight: 18)
+ .background(Color.accentColor)
+ .cornerRadius(10)
+ }
+ }
+ .padding(.trailing, 8)
}
else if case let .direct(contact) = chat.chatInfo, !contact.ready {
Text("Connecting...")
@@ -61,6 +78,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 +110,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, d. 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: 11, 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..b389f9c47a 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,9 @@ struct NewChatButton: View {
connReqInvitation = try apiAddContact()
addContact = true
} catch {
- addContactAlert = true
- addContactError = error
+ DispatchQueue.global().async {
+ connectionErrorAlert(error)
+ }
logger.error("NewChatButton.addContactAction apiAddContact error: \(error.localizedDescription)")
}
}
@@ -58,21 +49,36 @@ struct NewChatButton: View {
func connectContactSheet() -> some View {
ConnectContactView(completed: { err in
connectContact = false
- if err != nil {
- connectAlert = true
- connectError = err
+ DispatchQueue.global().async {
+ if let error = err {
+ connectionErrorAlert(error)
+ } else {
+ connectionReqSentAlert(.invitation)
+ }
}
})
}
- 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)
}
}
+enum ConnReqType: Equatable {
+ case contact
+ case invitation
+}
+
+func connectionReqSentAlert(_ type: ConnReqType) {
+ let whenConnected = type == .contact
+ ? "your connection request is accepted"
+ : "your contact's device is online"
+ AlertManager.shared.showAlertMsg(
+ title: "Connection request sent!",
+ message: "You will be connected when \(whenConnected), please wait or check later!"
+ )
+}
+
struct NewChatButton_Previews: PreviewProvider {
static var previews: some View {
NewChatButton()
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--iOS--Info.plist b/apps/ios/SimpleX--iOS--Info.plist
index b8279472cb..f996e93668 100644
--- a/apps/ios/SimpleX--iOS--Info.plist
+++ b/apps/ios/SimpleX--iOS--Info.plist
@@ -19,6 +19,8 @@
+ ITSAppUsesNonExemptEncryption
+
UIBackgroundModes
fetch
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index 752c52f814..4ed34fa537 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 */,
@@ -781,7 +799,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 6;
+ CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -801,7 +819,7 @@
LIBRARY_SEARCH_PATHS = "";
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim";
- MARKETING_VERSION = 0.3;
+ MARKETING_VERSION = 0.3.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -821,7 +839,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 6;
+ CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -841,7 +859,7 @@
LIBRARY_SEARCH_PATHS = "";
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim";
- MARKETING_VERSION = 0.3;
+ MARKETING_VERSION = 0.3.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
diff --git a/blog/20220214-simplex-chat-ios-public-beta.md b/blog/20220214-simplex-chat-ios-public-beta.md
new file mode 100644
index 0000000000..4d3a7ab195
--- /dev/null
+++ b/blog/20220214-simplex-chat-ios-public-beta.md
@@ -0,0 +1,40 @@
+# SimpleX announces SimpleX Chat public beta for iOS
+
+**Published:** Feb 14, 2022
+
+## Private and secure chat and application platform - [public beta is now available](https://testflight.apple.com/join/DWuT2LQu) for iPhones with iOS 15.
+
+Our new iPhone app is very basic - right now it only supports text messages and emojis.
+
+Even though the app is new, it uses the same core code as our terminal app, that was used and stabilized over a long time, and it provides the same level of privacy and security that has been available since the release of v1 a month ago:
+- [double-ratchet](https://www.signal.org/docs/specifications/doubleratchet/) E2E encryption.
+- separate keys for each contact.
+- additional layer of E2E encryption in each message queue (to prevent traffic correlation when multiple queues are used in a conversation - something we plan later this year).
+- additional encryption of messages delivered from servers to recipients (also to prevent traffic correlation).
+
+You can read more details in our recent [v1 announcement](https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220112-simplex-chat-v1-released.md).
+
+## Join our public beta!
+
+Install the app [via TestFlight](https://testflight.apple.com/join/DWuT2LQu), connect to us (via **Connect to SimpleX team** link in the app) and to a couple of your friends you usually send messages to - and please let us know what you think!
+
+We would really appreciate any feedback to improve the app and to decide which additional features should be included in our public release in March.
+
+Should it be:
+- images,
+- link previews,
+- or maybe something else we couldn't think of.
+
+Please vote on the features you think are the most needed in our [app roadmap](https://app.loopedin.io/simplex).
+
+## What is SimpleX?
+
+We are building a new platform for distributed Internet applications where privacy of the messages _and_ the network matter.
+
+We aim to provide the best possible protection of messages and metadata. Today there is no messaging application that works without global user identities, so we believe we provide better metadata privacy than alternatives. SimpleX is designed to be truly distributed with no central server, and without any global user identities. This allows for high scalability at low cost, and also makes it virtually impossible to snoop on the network graph.
+
+The first application built on the platform is Simplex Chat, which is available for terminal (command line in Windows/Mac/Linux) and as iOS public beta - with Android app coming in a few weeks. The platform can easily support a private social network feed and a multitude of other services, which can be developed by the Simplex team or third party developers.
+
+SimpleX also allows people to host their own servers to have control of their chat data. SimpleX servers are exceptionally lightweight and require a single process with the initial memory footprint of under 20 Mb, which grows as the server adds in-memory queues (even with 10,000 queues it uses less than 50Mb, not accounting for messages). It should be considered though that while self-hosting the servers provides more control, it may reduce meta-data privacy, as it is easier to correlate the traffic of servers with small number of messages coming through.
+
+Further details on platform objectives and technical design are available [in SimpleX platform overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md).
diff --git a/blog/README.md b/blog/README.md
index ca0fa58dc2..1b3fb20501 100644
--- a/blog/README.md
+++ b/blog/README.md
@@ -1,11 +1,13 @@
# Blog
-Jan 12, 2022. [SimpleX Chat v1 released: the most private and secure chat and application platform](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20220112-simplex-chat-v1-released.md)
+Feb 14, 2022. [SimpleX Chat: join our public beta for iOS!](https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220214-simplex-chat-ios-public-beta.md)
-Dec 08, 2021. [SimpleX Chat v0.5 released: the first chat platform that is 100% private by design - no access to your connections graph](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20211208-simplex-chat-v0.5-released.md)
+Jan 12, 2022. [SimpleX Chat v1 released: the most private and secure chat and application platform](https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20220112-simplex-chat-v1-released.md)
-Sep 14, 2021. [SimpleX Chat v0.4 released: open-source chat that uses privacy-preserving message routing protocol](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20210914-simplex-chat-v0.4-released.md)
+Dec 08, 2021. [SimpleX Chat v0.5 released: the first chat platform that is 100% private by design - no access to your connections graph](https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20211208-simplex-chat-v0.5-released.md)
-May 12, 2021. [SimpleX Chat Prototype!](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20210512-simplex-chat-terminal-ui.md)
+Sep 14, 2021. [SimpleX Chat v0.4 released: open-source chat that uses privacy-preserving message routing protocol](https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20210914-simplex-chat-v0.4-released.md)
-Oct 22, 2020. [SimpleX Chat](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20201022-simplex-chat)
+May 12, 2021. [SimpleX Chat Prototype!](https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20210512-simplex-chat-terminal-ui.md)
+
+Oct 22, 2020. [SimpleX Chat](https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20201022-simplex-chat)
diff --git a/cabal.project b/cabal.project
index 1727e1efd6..5df8a3dfe2 100644
--- a/cabal.project
+++ b/cabal.project
@@ -3,7 +3,7 @@ packages: .
source-repository-package
type: git
location: git://github.com/simplex-chat/simplexmq.git
- tag: c380c795600b887fcae1614a52fb5cda691b569d
+ tag: 229e2607d76f3d6baf0d2623b186c084e3908b8f
source-repository-package
type: git
diff --git a/images/simplex-chat-logo.svg b/images/simplex-chat-logo.svg
index 45cae4ff38..31f954e3ab 100644
--- a/images/simplex-chat-logo.svg
+++ b/images/simplex-chat-logo.svg
@@ -1,5 +1,5 @@