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
@@ -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"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
BIN
apps/ios/Shared/Assets.xcassets/github.imageset/github_1x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
apps/ios/Shared/Assets.xcassets/github.imageset/github_2x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
apps/ios/Shared/Assets.xcassets/github.imageset/github_3x.png
vendored
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
23
apps/ios/Shared/Assets.xcassets/github_light.imageset/Contents.json
vendored
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
BIN
apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_1x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_2x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
apps/ios/Shared/Assets.xcassets/github_light.imageset/github_light_3x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ struct SimpleXApp: App {
|
||||
.onOpenURL { url in
|
||||
logger.debug("ContentView.onOpenURL: \(url)")
|
||||
chatModel.appOpenUrl = url
|
||||
chatModel.connectViaUrl = true
|
||||
}
|
||||
.onAppear() {
|
||||
initializeChat()
|
||||
|
||||
43
apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift
Normal file
@@ -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: []))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
47
apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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)..<s.index(s.endIndex, offsetBy: -1)])
|
||||
}
|
||||
|
||||
private func msgDeliveryError(_ err: String) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Message delivery error",
|
||||
message: err
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct TextItemView_Previews: PreviewProvider {
|
||||
@@ -133,7 +138,7 @@ struct TextItemView_Previews: PreviewProvider {
|
||||
Group{
|
||||
TextItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), width: 360)
|
||||
TextItemView(chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello"), width: 360)
|
||||
TextItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat"), width: 360)
|
||||
TextItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent), width: 360)
|
||||
TextItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), width: 360)
|
||||
TextItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), width: 360)
|
||||
TextItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), width: 360)
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
|
||||
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 ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@@ -31,8 +28,15 @@ struct ChatView: View {
|
||||
ChatItemView(chatItem: $0, width: g.size.width)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: $0.chatDir.sent ? .trailing : .leading)
|
||||
}
|
||||
.onAppear { scrollToBottom(proxy) }
|
||||
.onChange(of: chatModel.chatItems.count) { _ in scrollToBottom(proxy) }
|
||||
.onAppear {
|
||||
DispatchQueue.main.async {
|
||||
scrollToFirstUnread(proxy)
|
||||
}
|
||||
markAllRead()
|
||||
}
|
||||
.onChange(of: chatModel.chatItems.count) { _ in
|
||||
scrollToBottom(proxy)
|
||||
}
|
||||
.onChange(of: keyboardVisible) { _ in
|
||||
if keyboardVisible {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
@@ -42,7 +46,6 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.coordinateSpace(name: "scrollView")
|
||||
.onTapGesture {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
@@ -72,23 +75,7 @@ struct ChatView: View {
|
||||
Button {
|
||||
showChatInfo = true
|
||||
} label: {
|
||||
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)
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.sheet(isPresented: $showChatInfo) {
|
||||
ChatInfoView(chat: chat, showChatInfo: $showChatInfo)
|
||||
@@ -99,9 +86,28 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) {
|
||||
withAnimation(animation) { scrollToBottom_(proxy) }
|
||||
}
|
||||
|
||||
func scrollToBottom_(_ proxy: ScrollViewProxy) {
|
||||
if let id = chatModel.chatItems.last?.id {
|
||||
withAnimation(animation) {
|
||||
proxy.scrollTo(id, anchor: .bottom)
|
||||
proxy.scrollTo(id, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
// align first unread with the top or the last unread with bottom
|
||||
func scrollToFirstUnread(_ proxy: ScrollViewProxy) {
|
||||
if let cItem = chatModel.chatItems.first(where: { $0.isRcvNew() }) {
|
||||
proxy.scrollTo(cItem.id)
|
||||
} else {
|
||||
scrollToBottom_(proxy)
|
||||
}
|
||||
}
|
||||
|
||||
func markAllRead() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
if chatModel.chatId == chat.id {
|
||||
markChatRead(chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
35
apps/ios/Shared/Views/Helpers/NavLinkPlain.swift
Normal file
@@ -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<V: Hashable, Destination: View, Label: View>: 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()
|
||||
// }
|
||||
//}
|
||||
18
apps/ios/Shared/Views/Helpers/ShareSheet.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Bool>, 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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
5C75059A27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a"; sourceTree = "<group>"; };
|
||||
5C75059B27B5CD9300BE3227 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
|
||||
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
||||
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -348,7 +360,6 @@
|
||||
5CCD403627A5F9A200368C90 /* ConnectContactView.swift */,
|
||||
5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */,
|
||||
5CC1C99127A6C7F5000D9FF6 /* QRCode.swift */,
|
||||
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */,
|
||||
);
|
||||
path = NewChat;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||