ios: more responsive UI, especially during app start (#2942)
* ios: more responsive UI, especially during app start * move terminal items to actor * fix for iOS 15/16
This commit is contained in:
parent
45b7d09f83
commit
01a95f88bb
@ -11,6 +11,38 @@ import Combine
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SimpleXChat
|
import SimpleXChat
|
||||||
|
|
||||||
|
actor TerminalItems {
|
||||||
|
private var terminalItems: [TerminalItem] = []
|
||||||
|
|
||||||
|
static let shared = TerminalItems()
|
||||||
|
|
||||||
|
func items() -> [TerminalItem] {
|
||||||
|
terminalItems
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(_ item: TerminalItem) async {
|
||||||
|
addTermItem(&terminalItems, item)
|
||||||
|
let m = ChatModel.shared
|
||||||
|
if m.showingTerminal {
|
||||||
|
await MainActor.run {
|
||||||
|
addTermItem(&m.terminalItems, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCommand(_ start: Date, _ cmd: ChatCommand, _ resp: ChatResponse) async {
|
||||||
|
addTermItem(&terminalItems, .cmd(start, cmd))
|
||||||
|
addTermItem(&terminalItems, .resp(.now, resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) {
|
||||||
|
if items.count >= 200 {
|
||||||
|
items.removeFirst()
|
||||||
|
}
|
||||||
|
items.append(item)
|
||||||
|
}
|
||||||
|
|
||||||
final class ChatModel: ObservableObject {
|
final class ChatModel: ObservableObject {
|
||||||
@Published var onboardingStage: OnboardingStage?
|
@Published var onboardingStage: OnboardingStage?
|
||||||
@Published var setDeliveryReceipts = false
|
@Published var setDeliveryReceipts = false
|
||||||
@ -33,6 +65,7 @@ final class ChatModel: ObservableObject {
|
|||||||
@Published var chatToTop: String?
|
@Published var chatToTop: String?
|
||||||
@Published var groupMembers: [GroupMember] = []
|
@Published var groupMembers: [GroupMember] = []
|
||||||
// items in the terminal view
|
// items in the terminal view
|
||||||
|
@Published var showingTerminal = false
|
||||||
@Published var terminalItems: [TerminalItem] = []
|
@Published var terminalItems: [TerminalItem] = []
|
||||||
@Published var userAddress: UserContactLink?
|
@Published var userAddress: UserContactLink?
|
||||||
@Published var chatItemTTL: ChatItemTTL = .none
|
@Published var chatItemTTL: ChatItemTTL = .none
|
||||||
@ -592,13 +625,6 @@ final class ChatModel: ObservableObject {
|
|||||||
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
|
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
|
||||||
networkStatuses[contact.activeConn.agentConnId] ?? .unknown
|
networkStatuses[contact.activeConn.agentConnId] ?? .unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTerminalItem(_ item: TerminalItem) {
|
|
||||||
if terminalItems.count >= 500 {
|
|
||||||
terminalItems.removeFirst()
|
|
||||||
}
|
|
||||||
terminalItems.append(item)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NTFContactRequest {
|
struct NTFContactRequest {
|
||||||
|
@ -94,9 +94,8 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
|
|||||||
if case let .response(_, json) = resp {
|
if case let .response(_, json) = resp {
|
||||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
Task {
|
||||||
ChatModel.shared.addTerminalItem(.cmd(start, cmd.obfuscated))
|
await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp)
|
||||||
ChatModel.shared.addTerminalItem(.resp(.now, resp))
|
|
||||||
}
|
}
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
@ -321,12 +320,18 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
|
|||||||
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
|
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
|
||||||
let r: ChatResponse
|
let r: ChatResponse
|
||||||
if type == .direct {
|
if type == .direct {
|
||||||
var cItem: ChatItem!
|
var cItem: ChatItem? = nil
|
||||||
let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } })
|
let endTask = beginBGTask({
|
||||||
|
if let cItem = cItem {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
chatModel.messageDelivery.removeValue(forKey: cItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
r = await chatSendCmd(cmd, bgTask: false)
|
r = await chatSendCmd(cmd, bgTask: false)
|
||||||
if case let .newChatItem(_, aChatItem) = r {
|
if case let .newChatItem(_, aChatItem) = r {
|
||||||
cItem = aChatItem.chatItem
|
cItem = aChatItem.chatItem
|
||||||
chatModel.messageDelivery[cItem.id] = endTask
|
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
|
||||||
return cItem
|
return cItem
|
||||||
}
|
}
|
||||||
if let networkErrorAlert = networkErrorAlert(r) {
|
if let networkErrorAlert = networkErrorAlert(r) {
|
||||||
@ -804,7 +809,7 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
|
|||||||
|
|
||||||
func receiveFile(user: User, fileId: Int64, auto: Bool = false) async {
|
func receiveFile(user: User, fileId: Int64, auto: Bool = false) async {
|
||||||
if let chatItem = await apiReceiveFile(fileId: fileId, auto: auto) {
|
if let chatItem = await apiReceiveFile(fileId: fileId, auto: auto) {
|
||||||
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
|
await chatItemSimpleUpdate(user, chatItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -842,7 +847,7 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil, auto: Bool = false) asyn
|
|||||||
|
|
||||||
func cancelFile(user: User, fileId: Int64) async {
|
func cancelFile(user: User, fileId: Int64) async {
|
||||||
if let chatItem = await apiCancelFile(fileId: fileId) {
|
if let chatItem = await apiCancelFile(fileId: fileId) {
|
||||||
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
|
await chatItemSimpleUpdate(user, chatItem)
|
||||||
cleanupFile(chatItem)
|
cleanupFile(chatItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1244,38 +1249,50 @@ class ChatReceiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func processReceivedMsg(_ res: ChatResponse) async {
|
func processReceivedMsg(_ res: ChatResponse) async {
|
||||||
|
Task {
|
||||||
|
await TerminalItems.shared.add(.resp(.now, res))
|
||||||
|
}
|
||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
await MainActor.run {
|
logger.debug("processReceivedMsg: \(res.responseType)")
|
||||||
m.addTerminalItem(.resp(.now, res))
|
switch res {
|
||||||
logger.debug("processReceivedMsg: \(res.responseType)")
|
case let .newContactConnection(user, connection):
|
||||||
switch res {
|
if active(user) {
|
||||||
case let .newContactConnection(user, connection):
|
await MainActor.run {
|
||||||
if active(user) {
|
|
||||||
m.updateContactConnection(connection)
|
m.updateContactConnection(connection)
|
||||||
}
|
}
|
||||||
case let .contactConnectionDeleted(user, connection):
|
}
|
||||||
if active(user) {
|
case let .contactConnectionDeleted(user, connection):
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
m.removeChat(connection.id)
|
m.removeChat(connection.id)
|
||||||
}
|
}
|
||||||
case let .contactConnected(user, contact, _):
|
}
|
||||||
if active(user) && contact.directOrUsed {
|
case let .contactConnected(user, contact, _):
|
||||||
|
if active(user) && contact.directOrUsed {
|
||||||
|
await MainActor.run {
|
||||||
m.updateContact(contact)
|
m.updateContact(contact)
|
||||||
m.dismissConnReqView(contact.activeConn.id)
|
m.dismissConnReqView(contact.activeConn.id)
|
||||||
m.removeChat(contact.activeConn.id)
|
m.removeChat(contact.activeConn.id)
|
||||||
}
|
}
|
||||||
if contact.directOrUsed {
|
}
|
||||||
NtfManager.shared.notifyContactConnected(user, contact)
|
if contact.directOrUsed {
|
||||||
}
|
NtfManager.shared.notifyContactConnected(user, contact)
|
||||||
|
}
|
||||||
|
await MainActor.run {
|
||||||
m.setContactNetworkStatus(contact, .connected)
|
m.setContactNetworkStatus(contact, .connected)
|
||||||
case let .contactConnecting(user, contact):
|
}
|
||||||
if active(user) && contact.directOrUsed {
|
case let .contactConnecting(user, contact):
|
||||||
|
if active(user) && contact.directOrUsed {
|
||||||
|
await MainActor.run {
|
||||||
m.updateContact(contact)
|
m.updateContact(contact)
|
||||||
m.dismissConnReqView(contact.activeConn.id)
|
m.dismissConnReqView(contact.activeConn.id)
|
||||||
m.removeChat(contact.activeConn.id)
|
m.removeChat(contact.activeConn.id)
|
||||||
}
|
}
|
||||||
case let .receivedContactRequest(user, contactRequest):
|
}
|
||||||
if active(user) {
|
case let .receivedContactRequest(user, contactRequest):
|
||||||
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
|
if active(user) {
|
||||||
|
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
|
||||||
|
await MainActor.run {
|
||||||
if m.hasChat(contactRequest.id) {
|
if m.hasChat(contactRequest.id) {
|
||||||
m.updateChatInfo(cInfo)
|
m.updateChatInfo(cInfo)
|
||||||
} else {
|
} else {
|
||||||
@ -1285,234 +1302,285 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NtfManager.shared.notifyContactRequest(user, contactRequest)
|
}
|
||||||
case let .contactUpdated(user, toContact):
|
NtfManager.shared.notifyContactRequest(user, contactRequest)
|
||||||
if active(user) && m.hasChat(toContact.id) {
|
case let .contactUpdated(user, toContact):
|
||||||
|
if active(user) && m.hasChat(toContact.id) {
|
||||||
|
await MainActor.run {
|
||||||
let cInfo = ChatInfo.direct(contact: toContact)
|
let cInfo = ChatInfo.direct(contact: toContact)
|
||||||
m.updateChatInfo(cInfo)
|
m.updateChatInfo(cInfo)
|
||||||
}
|
}
|
||||||
case let .contactsMerged(user, intoContact, mergedContact):
|
}
|
||||||
if active(user) && m.hasChat(mergedContact.id) {
|
case let .contactsMerged(user, intoContact, mergedContact):
|
||||||
|
if active(user) && m.hasChat(mergedContact.id) {
|
||||||
|
await MainActor.run {
|
||||||
if m.chatId == mergedContact.id {
|
if m.chatId == mergedContact.id {
|
||||||
m.chatId = intoContact.id
|
m.chatId = intoContact.id
|
||||||
}
|
}
|
||||||
m.removeChat(mergedContact.id)
|
m.removeChat(mergedContact.id)
|
||||||
}
|
}
|
||||||
case let .contactsSubscribed(_, contactRefs):
|
}
|
||||||
updateContactsStatus(contactRefs, status: .connected)
|
case let .contactsSubscribed(_, contactRefs):
|
||||||
case let .contactsDisconnected(_, contactRefs):
|
await updateContactsStatus(contactRefs, status: .connected)
|
||||||
updateContactsStatus(contactRefs, status: .disconnected)
|
case let .contactsDisconnected(_, contactRefs):
|
||||||
case let .contactSubError(user, contact, chatError):
|
await updateContactsStatus(contactRefs, status: .disconnected)
|
||||||
|
case let .contactSubError(user, contact, chatError):
|
||||||
|
await MainActor.run {
|
||||||
if active(user) {
|
if active(user) {
|
||||||
m.updateContact(contact)
|
m.updateContact(contact)
|
||||||
}
|
}
|
||||||
processContactSubError(contact, chatError)
|
processContactSubError(contact, chatError)
|
||||||
case let .contactSubSummary(user, contactSubscriptions):
|
}
|
||||||
|
case let .contactSubSummary(_, contactSubscriptions):
|
||||||
|
await MainActor.run {
|
||||||
for sub in contactSubscriptions {
|
for sub in contactSubscriptions {
|
||||||
if active(user) {
|
// no need to update contact here, and it is slow
|
||||||
m.updateContact(sub.contact)
|
// if active(user) {
|
||||||
}
|
// m.updateContact(sub.contact)
|
||||||
|
// }
|
||||||
if let err = sub.contactError {
|
if let err = sub.contactError {
|
||||||
processContactSubError(sub.contact, err)
|
processContactSubError(sub.contact, err)
|
||||||
} else {
|
} else {
|
||||||
m.setContactNetworkStatus(sub.contact, .connected)
|
m.setContactNetworkStatus(sub.contact, .connected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .newChatItem(user, aChatItem):
|
}
|
||||||
let cInfo = aChatItem.chatInfo
|
case let .newChatItem(user, aChatItem):
|
||||||
let cItem = aChatItem.chatItem
|
let cInfo = aChatItem.chatInfo
|
||||||
|
let cItem = aChatItem.chatItem
|
||||||
|
await MainActor.run {
|
||||||
if active(user) {
|
if active(user) {
|
||||||
m.addChatItem(cInfo, cItem)
|
m.addChatItem(cInfo, cItem)
|
||||||
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
|
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
|
||||||
m.increaseUnreadCounter(user: user)
|
m.increaseUnreadCounter(user: user)
|
||||||
}
|
}
|
||||||
if let file = cItem.autoReceiveFile() {
|
}
|
||||||
Task {
|
if let file = cItem.autoReceiveFile() {
|
||||||
await receiveFile(user: user, fileId: file.fileId, auto: true)
|
Task {
|
||||||
}
|
await receiveFile(user: user, fileId: file.fileId, auto: true)
|
||||||
}
|
}
|
||||||
if cItem.showNotification {
|
}
|
||||||
|
if cItem.showNotification {
|
||||||
|
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||||
|
}
|
||||||
|
case let .chatItemStatusUpdated(user, aChatItem):
|
||||||
|
let cInfo = aChatItem.chatInfo
|
||||||
|
let cItem = aChatItem.chatItem
|
||||||
|
if !cItem.isDeletedContent {
|
||||||
|
let added = active(user) ? await MainActor.run { m.upsertChatItem(cInfo, cItem) } : true
|
||||||
|
if added && cItem.showNotification {
|
||||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||||
}
|
}
|
||||||
case let .chatItemStatusUpdated(user, aChatItem):
|
}
|
||||||
let cInfo = aChatItem.chatInfo
|
if let endTask = m.messageDelivery[cItem.id] {
|
||||||
let cItem = aChatItem.chatItem
|
switch cItem.meta.itemStatus {
|
||||||
if !cItem.isDeletedContent {
|
case .sndSent: endTask()
|
||||||
let added = active(user) ? m.upsertChatItem(cInfo, cItem) : true
|
case .sndErrorAuth: endTask()
|
||||||
if added && cItem.showNotification {
|
case .sndError: endTask()
|
||||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
default: ()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let endTask = m.messageDelivery[cItem.id] {
|
}
|
||||||
switch cItem.meta.itemStatus {
|
case let .chatItemUpdated(user, aChatItem):
|
||||||
case .sndSent: endTask()
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
case .sndErrorAuth: endTask()
|
case let .chatItemReaction(user, _, r):
|
||||||
case .sndError: endTask()
|
if active(user) {
|
||||||
default: ()
|
await MainActor.run {
|
||||||
}
|
|
||||||
}
|
|
||||||
case let .chatItemUpdated(user, aChatItem):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
case let .chatItemReaction(user, _, r):
|
|
||||||
if active(user) {
|
|
||||||
m.updateChatItem(r.chatInfo, r.chatReaction.chatItem)
|
m.updateChatItem(r.chatInfo, r.chatReaction.chatItem)
|
||||||
}
|
}
|
||||||
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
|
}
|
||||||
if !active(user) {
|
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
|
||||||
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
|
if !active(user) {
|
||||||
|
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
|
||||||
|
await MainActor.run {
|
||||||
m.decreaseUnreadCounter(user: user)
|
m.decreaseUnreadCounter(user: user)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
if let toChatItem = toChatItem {
|
if let toChatItem = toChatItem {
|
||||||
_ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem)
|
_ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem)
|
||||||
} else {
|
} else {
|
||||||
m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem)
|
m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem)
|
||||||
}
|
}
|
||||||
case let .receivedGroupInvitation(user, groupInfo, _, _):
|
}
|
||||||
if active(user) {
|
case let .receivedGroupInvitation(user, groupInfo, _, _):
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
|
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
|
||||||
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
|
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
|
||||||
}
|
}
|
||||||
case let .userAcceptedGroupSent(user, groupInfo, hostContact):
|
}
|
||||||
if !active(user) { return }
|
case let .userAcceptedGroupSent(user, groupInfo, hostContact):
|
||||||
|
if !active(user) { return }
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
m.updateGroup(groupInfo)
|
m.updateGroup(groupInfo)
|
||||||
if let hostContact = hostContact {
|
if let hostContact = hostContact {
|
||||||
m.dismissConnReqView(hostContact.activeConn.id)
|
m.dismissConnReqView(hostContact.activeConn.id)
|
||||||
m.removeChat(hostContact.activeConn.id)
|
m.removeChat(hostContact.activeConn.id)
|
||||||
}
|
}
|
||||||
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
|
}
|
||||||
if active(user) {
|
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
_ = m.upsertGroupMember(groupInfo, member)
|
_ = m.upsertGroupMember(groupInfo, member)
|
||||||
}
|
}
|
||||||
case let .deletedMemberUser(user, groupInfo, _): // TODO update user member
|
}
|
||||||
if active(user) {
|
case let .deletedMemberUser(user, groupInfo, _): // TODO update user member
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
m.updateGroup(groupInfo)
|
m.updateGroup(groupInfo)
|
||||||
}
|
}
|
||||||
case let .deletedMember(user, groupInfo, _, deletedMember):
|
}
|
||||||
if active(user) {
|
case let .deletedMember(user, groupInfo, _, deletedMember):
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
_ = m.upsertGroupMember(groupInfo, deletedMember)
|
_ = m.upsertGroupMember(groupInfo, deletedMember)
|
||||||
}
|
}
|
||||||
case let .leftMember(user, groupInfo, member):
|
}
|
||||||
if active(user) {
|
case let .leftMember(user, groupInfo, member):
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
_ = m.upsertGroupMember(groupInfo, member)
|
_ = m.upsertGroupMember(groupInfo, member)
|
||||||
}
|
}
|
||||||
case let .groupDeleted(user, groupInfo, _): // TODO update user member
|
}
|
||||||
if active(user) {
|
case let .groupDeleted(user, groupInfo, _): // TODO update user member
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
m.updateGroup(groupInfo)
|
m.updateGroup(groupInfo)
|
||||||
}
|
}
|
||||||
case let .userJoinedGroup(user, groupInfo):
|
}
|
||||||
if active(user) {
|
case let .userJoinedGroup(user, groupInfo):
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
m.updateGroup(groupInfo)
|
m.updateGroup(groupInfo)
|
||||||
}
|
}
|
||||||
case let .joinedGroupMember(user, groupInfo, member):
|
}
|
||||||
if active(user) {
|
case let .joinedGroupMember(user, groupInfo, member):
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
_ = m.upsertGroupMember(groupInfo, member)
|
_ = m.upsertGroupMember(groupInfo, member)
|
||||||
}
|
}
|
||||||
case let .connectedToGroupMember(user, groupInfo, member, memberContact):
|
}
|
||||||
if active(user) {
|
case let .connectedToGroupMember(user, groupInfo, member, memberContact):
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
_ = m.upsertGroupMember(groupInfo, member)
|
_ = m.upsertGroupMember(groupInfo, member)
|
||||||
}
|
}
|
||||||
if let contact = memberContact {
|
}
|
||||||
|
if let contact = memberContact {
|
||||||
|
await MainActor.run {
|
||||||
m.setContactNetworkStatus(contact, .connected)
|
m.setContactNetworkStatus(contact, .connected)
|
||||||
}
|
}
|
||||||
case let .groupUpdated(user, toGroup):
|
}
|
||||||
if active(user) {
|
case let .groupUpdated(user, toGroup):
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
m.updateGroup(toGroup)
|
m.updateGroup(toGroup)
|
||||||
}
|
}
|
||||||
case let .memberRole(user, groupInfo, _, _, _, _):
|
}
|
||||||
if active(user) {
|
case let .memberRole(user, groupInfo, _, _, _, _):
|
||||||
|
if active(user) {
|
||||||
|
await MainActor.run {
|
||||||
m.updateGroup(groupInfo)
|
m.updateGroup(groupInfo)
|
||||||
}
|
}
|
||||||
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
case let .rcvFileStart(user, aChatItem):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
case let .rcvFileComplete(user, aChatItem):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
case let .rcvFileSndCancelled(user, aChatItem, _):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
cleanupFile(aChatItem)
|
|
||||||
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
case let .rcvFileError(user, aChatItem):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
cleanupFile(aChatItem)
|
|
||||||
case let .sndFileStart(user, aChatItem, _):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
case let .sndFileComplete(user, aChatItem, _):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
cleanupDirectFile(aChatItem)
|
|
||||||
case let .sndFileRcvCancelled(user, aChatItem, _):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
cleanupDirectFile(aChatItem)
|
|
||||||
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
case let .sndFileCompleteXFTP(user, aChatItem, _):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
cleanupFile(aChatItem)
|
|
||||||
case let .sndFileError(user, aChatItem):
|
|
||||||
chatItemSimpleUpdate(user, aChatItem)
|
|
||||||
cleanupFile(aChatItem)
|
|
||||||
case let .callInvitation(invitation):
|
|
||||||
m.callInvitations[invitation.contact.id] = invitation
|
|
||||||
activateCall(invitation)
|
|
||||||
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
|
||||||
withCall(contact) { call in
|
|
||||||
call.callState = .offerReceived
|
|
||||||
call.peerMedia = callType.media
|
|
||||||
call.sharedKey = sharedKey
|
|
||||||
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
|
||||||
let iceServers = getIceServers()
|
|
||||||
logger.debug(".callOffer useRelay \(useRelay)")
|
|
||||||
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
|
|
||||||
m.callCommand = .offer(
|
|
||||||
offer: offer.rtcSession,
|
|
||||||
iceCandidates: offer.rtcIceCandidates,
|
|
||||||
media: callType.media, aesKey: sharedKey,
|
|
||||||
iceServers: iceServers,
|
|
||||||
relay: useRelay
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case let .callAnswer(_, contact, answer):
|
|
||||||
withCall(contact) { call in
|
|
||||||
call.callState = .answerReceived
|
|
||||||
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
|
|
||||||
}
|
|
||||||
case let .callExtraInfo(_, contact, extraInfo):
|
|
||||||
withCall(contact) { _ in
|
|
||||||
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
|
|
||||||
}
|
|
||||||
case let .callEnded(_, contact):
|
|
||||||
if let invitation = m.callInvitations.removeValue(forKey: contact.id) {
|
|
||||||
CallController.shared.reportCallRemoteEnded(invitation: invitation)
|
|
||||||
}
|
|
||||||
withCall(contact) { call in
|
|
||||||
m.callCommand = .end
|
|
||||||
CallController.shared.reportCallRemoteEnded(call: call)
|
|
||||||
}
|
|
||||||
case .chatSuspended:
|
|
||||||
chatSuspended()
|
|
||||||
case let .contactSwitch(_, contact, switchProgress):
|
|
||||||
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
|
|
||||||
case let .groupMemberSwitch(_, groupInfo, member, switchProgress):
|
|
||||||
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
|
|
||||||
case let .contactRatchetSync(_, contact, ratchetSyncProgress):
|
|
||||||
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
|
|
||||||
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress):
|
|
||||||
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
|
||||||
default:
|
|
||||||
logger.debug("unsupported event: \(res.responseType)")
|
|
||||||
}
|
}
|
||||||
|
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
case let .rcvFileStart(user, aChatItem):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
case let .rcvFileComplete(user, aChatItem):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
case let .rcvFileSndCancelled(user, aChatItem, _):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
Task { cleanupFile(aChatItem) }
|
||||||
|
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
case let .rcvFileError(user, aChatItem):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
Task { cleanupFile(aChatItem) }
|
||||||
|
case let .sndFileStart(user, aChatItem, _):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
case let .sndFileComplete(user, aChatItem, _):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
Task { cleanupDirectFile(aChatItem) }
|
||||||
|
case let .sndFileRcvCancelled(user, aChatItem, _):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
Task { cleanupDirectFile(aChatItem) }
|
||||||
|
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
case let .sndFileCompleteXFTP(user, aChatItem, _):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
Task { cleanupFile(aChatItem) }
|
||||||
|
case let .sndFileError(user, aChatItem):
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
Task { cleanupFile(aChatItem) }
|
||||||
|
case let .callInvitation(invitation):
|
||||||
|
m.callInvitations[invitation.contact.id] = invitation
|
||||||
|
activateCall(invitation)
|
||||||
|
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
||||||
|
await withCall(contact) { call in
|
||||||
|
call.callState = .offerReceived
|
||||||
|
call.peerMedia = callType.media
|
||||||
|
call.sharedKey = sharedKey
|
||||||
|
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
||||||
|
let iceServers = getIceServers()
|
||||||
|
logger.debug(".callOffer useRelay \(useRelay)")
|
||||||
|
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
|
||||||
|
m.callCommand = .offer(
|
||||||
|
offer: offer.rtcSession,
|
||||||
|
iceCandidates: offer.rtcIceCandidates,
|
||||||
|
media: callType.media, aesKey: sharedKey,
|
||||||
|
iceServers: iceServers,
|
||||||
|
relay: useRelay
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case let .callAnswer(_, contact, answer):
|
||||||
|
await withCall(contact) { call in
|
||||||
|
call.callState = .answerReceived
|
||||||
|
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
|
||||||
|
}
|
||||||
|
case let .callExtraInfo(_, contact, extraInfo):
|
||||||
|
await withCall(contact) { _ in
|
||||||
|
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
|
||||||
|
}
|
||||||
|
case let .callEnded(_, contact):
|
||||||
|
if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) {
|
||||||
|
CallController.shared.reportCallRemoteEnded(invitation: invitation)
|
||||||
|
}
|
||||||
|
await withCall(contact) { call in
|
||||||
|
m.callCommand = .end
|
||||||
|
CallController.shared.reportCallRemoteEnded(call: call)
|
||||||
|
}
|
||||||
|
case .chatSuspended:
|
||||||
|
chatSuspended()
|
||||||
|
case let .contactSwitch(_, contact, switchProgress):
|
||||||
|
await MainActor.run {
|
||||||
|
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
|
||||||
|
}
|
||||||
|
case let .groupMemberSwitch(_, groupInfo, member, switchProgress):
|
||||||
|
await MainActor.run {
|
||||||
|
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
|
||||||
|
}
|
||||||
|
case let .contactRatchetSync(_, contact, ratchetSyncProgress):
|
||||||
|
await MainActor.run {
|
||||||
|
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
|
||||||
|
}
|
||||||
|
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress):
|
||||||
|
await MainActor.run {
|
||||||
|
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logger.debug("unsupported event: \(res.responseType)")
|
||||||
|
}
|
||||||
|
|
||||||
func withCall(_ contact: Contact, _ perform: (Call) -> Void) {
|
func withCall(_ contact: Contact, _ perform: (Call) -> Void) async {
|
||||||
if let call = m.activeCall, call.contact.apiId == contact.apiId {
|
if let call = m.activeCall, call.contact.apiId == contact.apiId {
|
||||||
perform(call)
|
await MainActor.run { perform(call) }
|
||||||
} else {
|
} else {
|
||||||
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
|
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1521,19 +1589,23 @@ func active(_ user: User) -> Bool {
|
|||||||
user.id == ChatModel.shared.currentUser?.id
|
user.id == ChatModel.shared.currentUser?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) {
|
func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) async {
|
||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
let cInfo = aChatItem.chatInfo
|
let cInfo = aChatItem.chatInfo
|
||||||
let cItem = aChatItem.chatItem
|
let cItem = aChatItem.chatItem
|
||||||
if active(user) && m.upsertChatItem(cInfo, cItem) {
|
if active(user) {
|
||||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) {
|
||||||
|
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) {
|
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async {
|
||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
for c in contactRefs {
|
await MainActor.run {
|
||||||
m.networkStatuses[c.agentConnId] = status
|
for c in contactRefs {
|
||||||
|
m.networkStatuses[c.agentConnId] = status
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1572,7 +1644,9 @@ func activateCall(_ callInvitation: RcvCallInvitation) {
|
|||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
|
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
|
DispatchQueue.main.async {
|
||||||
|
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
|
||||||
|
}
|
||||||
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
|
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
|
||||||
} else {
|
} else {
|
||||||
logger.debug("reportNewIncomingCall success")
|
logger.debug("reportNewIncomingCall success")
|
||||||
|
@ -22,10 +22,28 @@ struct TerminalView: View {
|
|||||||
@State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
|
@State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
|
||||||
@State private var terminalItem: TerminalItem?
|
@State private var terminalItem: TerminalItem?
|
||||||
@State private var scrolled = false
|
@State private var scrolled = false
|
||||||
|
@State private var showing = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if authorized {
|
if authorized {
|
||||||
terminalView()
|
terminalView()
|
||||||
|
.onAppear {
|
||||||
|
if showing { return }
|
||||||
|
showing = true
|
||||||
|
Task {
|
||||||
|
let items = await TerminalItems.shared.items()
|
||||||
|
await MainActor.run {
|
||||||
|
chatModel.terminalItems = items
|
||||||
|
chatModel.showingTerminal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
if terminalItem == nil {
|
||||||
|
chatModel.showingTerminal = false
|
||||||
|
chatModel.terminalItems = []
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(action: runAuth) { Label("Unlock", systemImage: "lock") }
|
Button(action: runAuth) { Label("Unlock", systemImage: "lock") }
|
||||||
.onAppear(perform: runAuth)
|
.onAppear(perform: runAuth)
|
||||||
@ -118,9 +136,8 @@ struct TerminalView: View {
|
|||||||
let cmd = ChatCommand.string(composeState.message)
|
let cmd = ChatCommand.string(composeState.message)
|
||||||
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
|
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
|
||||||
let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
||||||
DispatchQueue.main.async {
|
Task {
|
||||||
ChatModel.shared.addTerminalItem(.cmd(.now, cmd))
|
await TerminalItems.shared.addCommand(.now, cmd, resp)
|
||||||
ChatModel.shared.addTerminalItem(.resp(.now, resp))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.global().async {
|
DispatchQueue.global().async {
|
||||||
|
@ -299,6 +299,10 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.navigationTitle("Your settings")
|
.navigationTitle("Your settings")
|
||||||
}
|
}
|
||||||
|
.onDisappear {
|
||||||
|
chatModel.showingTerminal = false
|
||||||
|
chatModel.terminalItems = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func chatDatabaseRow() -> some View {
|
private func chatDatabaseRow() -> some View {
|
||||||
|
Loading…
Reference in New Issue
Block a user