ios: Multiusers feature continue (#1793)

* ios: Multiusers feature continue

* Logging of user in responses

* UserId

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* Undo ugly user inclusion into functions.  Now it's in backend

* Do not set active user if it's unchanged

* Blank line

* if

* Change active user function

* refactor

* refactor

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* Alert

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko
2023-01-19 16:22:56 +00:00
committed by GitHub
parent ba29d0242e
commit ad6aa10cd2
16 changed files with 557 additions and 407 deletions

View File

@@ -50,7 +50,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
try await apiVerifyToken(token: token, nonce: nonce, code: verification)
m.tokenStatus = .active
} catch {
if let cr = error as? ChatResponse, case .chatCmdError(.errorAgent(.NTF(.AUTH))) = cr {
if let cr = error as? ChatResponse, case .chatCmdError(_, .errorAgent(.NTF(.AUTH))) = cr {
m.tokenStatus = .expired
}
logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))")

View File

@@ -16,7 +16,7 @@ final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage?
@Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get()
@Published var currentUser: User?
@Published var users: [UserInfo] = []
@Published private(set) var users: [UserInfo] = []
@Published var chatInitialized = false
@Published var chatRunning: Bool?
@Published var chatDbChanged = false
@@ -177,6 +177,7 @@ final class ChatModel: ObservableObject {
chats[i].chatItems = [cItem]
if case .rcvNew = cItem.meta.itemStatus {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
increaseUnreadCounter(user: currentUser!)
NtfManager.shared.incNtfBadgeCount()
}
if i > 0 {
@@ -344,6 +345,7 @@ final class ChatModel: ObservableObject {
if markedCount > 0 {
NtfManager.shared.decNtfBadgeCount(by: markedCount)
chat.chatStats.unreadCount -= markedCount
self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount)
}
}
}
@@ -395,6 +397,21 @@ final class ChatModel: ObservableObject {
func decreaseUnreadCounter(_ cInfo: ChatInfo) {
if let i = getChatIndex(cInfo.id) {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1
decreaseUnreadCounter(user: currentUser!)
}
}
func increaseUnreadCounter(user: User) {
changeUnreadCounter(user: user, by: 1)
}
func decreaseUnreadCounter(user: User, by: Int = 1) {
changeUnreadCounter(user: user, by: -by)
}
private func changeUnreadCounter(user: User, by: Int) {
if let i = users.firstIndex(where: { $0.user.id == user.id }) {
users[i].unreadCount += Int64(by)
}
}
@@ -480,6 +497,31 @@ final class ChatModel: ObservableObject {
while i < maxIx && inView(i) { i += 1 }
return reversedChatItems[min(i - 1, maxIx)]
}
func updateUsers(_ new: [UserInfo]) {
users = new
.sorted { $0.user.chatViewName.compare($1.user.chatViewName) == .orderedAscending }
.sorted { first, _ in first.user.activeUser }
}
func changeActiveUser(_ toUserId: Int64) {
do {
let activeUser = try apiSetActiveUser(toUserId)
var users = users
let oldActiveIndex = users.firstIndex(where: { $0.user.userId == currentUser?.userId })!
var oldActive = users[oldActiveIndex]
oldActive.user.activeUser = false
users[oldActiveIndex] = oldActive
currentUser = activeUser
let currentActiveIndex = users.firstIndex(where: { $0.user.userId == activeUser.userId })!
users[currentActiveIndex] = UserInfo(user: activeUser, unreadCount: users[currentActiveIndex].unreadCount)
updateUsers(users)
try getUserChatData(self)
} catch {
logger.error("Unable to set active user: \(error.localizedDescription)")
}
}
}
struct UnreadChatItemCounts {

View File

@@ -28,7 +28,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
private var granted = false
private var prevNtfTime: Dictionary<ChatId, Date> = [:]
// Handle notification when app is in background
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
@@ -38,6 +37,10 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
let chatModel = ChatModel.shared
let action = response.actionIdentifier
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
if let userId = content.userInfo["userId"] as? Int64,
userId != chatModel.currentUser?.userId {
chatModel.changeActiveUser(userId)
}
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
let chatId = content.userInfo["chatId"] as? String {
if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
@@ -189,20 +192,20 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
center.delegate = self
}
func notifyContactRequest(_ contactRequest: UserContactRequest) {
func notifyContactRequest(_ user: User, _ contactRequest: UserContactRequest) {
logger.debug("NtfManager.notifyContactRequest")
addNotification(createContactRequestNtf(contactRequest))
addNotification(createContactRequestNtf(user, contactRequest))
}
func notifyContactConnected(_ contact: Contact) {
func notifyContactConnected(_ user: User, _ contact: Contact) {
logger.debug("NtfManager.notifyContactConnected")
addNotification(createContactConnectedNtf(contact))
addNotification(createContactConnectedNtf(user, contact))
}
func notifyMessageReceived(_ cInfo: ChatInfo, _ cItem: ChatItem) {
func notifyMessageReceived(_ user: User, _ cInfo: ChatInfo, _ cItem: ChatItem) {
logger.debug("NtfManager.notifyMessageReceived")
if cInfo.ntfsEnabled {
addNotification(createMessageReceivedNtf(cInfo, cItem))
addNotification(createMessageReceivedNtf(user, cInfo, cItem))
}
}

View File

@@ -120,7 +120,7 @@ func apiGetActiveUser() throws -> User? {
let r = chatSendCmdSync(.showActiveUser)
switch r {
case let .activeUser(user): return user
case .chatCmdError(.error(.noActiveUser)): return nil
case .chatCmdError(_, .error(.noActiveUser)): return nil
default: throw r
}
}
@@ -209,19 +209,19 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th
func apiGetChats() throws -> [ChatData] {
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetChats: no current user") }
let r = chatSendCmdSync(.apiGetChats(userId: userId))
if case let .apiChats(chats) = r { return chats }
if case let .apiChats(_, chats) = r { return chats }
throw r
}
func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat {
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50), search: search))
if case let .apiChat(chat) = r { return Chat.init(chat) }
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
throw r
}
func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> [ChatItem] {
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search))
if case let .apiChat(chat) = r { return chat.chatItems }
if case let .apiChat(_, chat) = r { return chat.chatItems }
throw r
}
@@ -246,7 +246,7 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
var cItem: ChatItem!
let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } })
r = await chatSendCmd(cmd, bgTask: false)
if case let .newChatItem(aChatItem) = r {
if case let .newChatItem(_, aChatItem) = r {
cItem = aChatItem.chatItem
chatModel.messageDelivery[cItem.id] = endTask
return cItem
@@ -258,7 +258,7 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
return nil
} else {
r = await chatSendCmd(cmd, bgDelay: msgDelay)
if case let .newChatItem(aChatItem) = r {
if case let .newChatItem(_, aChatItem) = r {
return aChatItem.chatItem
}
sendMessageErrorAlert(r)
@@ -276,13 +276,13 @@ private func sendMessageErrorAlert(_ r: ChatResponse) {
func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool = false) async throws -> ChatItem {
let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, msg: msg, live: live), bgDelay: msgDelay)
if case let .chatItemUpdated(aChatItem) = r { return aChatItem.chatItem }
if case let .chatItemUpdated(_, aChatItem) = r { return aChatItem.chatItem }
throw r
}
func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> (ChatItem, ChatItem?) {
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay)
if case let .chatItemDeleted(deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
throw r
}
@@ -290,7 +290,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
let r = chatSendCmdSync(.apiGetNtfToken)
switch r {
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
case .chatCmdError(.errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
default:
logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)")
return (nil, nil, .off)
@@ -331,7 +331,7 @@ func apiDeleteToken(token: DeviceToken) async throws {
func getUserSMPServers() throws -> ([ServerCfg], [String]) {
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getUserSMPServers: no current user") }
let r = chatSendCmdSync(.apiGetUserSMPServers(userId: userId))
if case let .userSMPServers(smpServers, presetServers) = r { return (smpServers, presetServers) }
if case let .userSMPServers(_, smpServers, presetServers) = r { return (smpServers, presetServers) }
throw r
}
@@ -343,7 +343,7 @@ func setUserSMPServers(smpServers: [ServerCfg]) async throws {
func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> {
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("testSMPServer: no current user") }
let r = await chatSendCmd(.testSMPServer(userId: userId, smpServer: smpServer))
if case let .smpTestResult(testFailure) = r {
if case let .smpTestResult(_, testFailure) = r {
if let t = testFailure {
return .failure(t)
}
@@ -355,7 +355,7 @@ func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure>
func getChatItemTTL() throws -> ChatItemTTL {
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getChatItemTTL: no current user") }
let r = chatSendCmdSync(.apiGetChatItemTTL(userId: userId))
if case let .chatItemTTL(chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
if case let .chatItemTTL(_, chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
throw r
}
@@ -382,13 +382,13 @@ func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) a
func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profile?) {
let r = await chatSendCmd(.apiContactInfo(contactId: contactId))
if case let .contactInfo(_, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
if case let .contactInfo(_, _, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
throw r
}
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (ConnectionStats?) {
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberInfo(_, _, connStats_) = r { return (connStats_) }
if case let .groupMemberInfo(_, _, _, connStats_) = r { return (connStats_) }
throw r
}
@@ -402,26 +402,26 @@ func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws
func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
let r = await chatSendCmd(.apiGetContactCode(contactId: contactId))
if case let .contactCode(contact, connectionCode) = r { return (contact, connectionCode) }
if case let .contactCode(_, contact, connectionCode) = r { return (contact, connectionCode) }
throw r
}
func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, String) {
let r = chatSendCmdSync(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberCode(_, member, connectionCode) = r { return (member, connectionCode) }
if case let .groupMemberCode(_, _, member, connectionCode) = r { return (member, connectionCode) }
throw r
}
func apiVerifyContact(_ contactId: Int64, connectionCode: String?) -> (Bool, String)? {
let r = chatSendCmdSync(.apiVerifyContact(contactId: contactId, connectionCode: connectionCode))
if case let .connectionVerified(verified, expectedCode) = r { return (verified, expectedCode) }
if case let .connectionVerified(_, verified, expectedCode) = r { return (verified, expectedCode) }
logger.error("apiVerifyContact error: \(String(describing: r))")
return nil
}
func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCode: String?) -> (Bool, String)? {
let r = chatSendCmdSync(.apiVerifyGroupMember(groupId: groupId, groupMemberId: groupMemberId, connectionCode: connectionCode))
if case let .connectionVerified(verified, expectedCode) = r { return (verified, expectedCode) }
if case let .connectionVerified(_, verified, expectedCode) = r { return (verified, expectedCode) }
logger.error("apiVerifyGroupMember error: \(String(describing: r))")
return nil
}
@@ -432,7 +432,7 @@ func apiAddContact() async -> String? {
return nil
}
let r = await chatSendCmd(.apiAddContact(userId: userId), bgTask: false)
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
if case let .invitation(_, connReqInvitation) = r { return connReqInvitation }
connectionErrorAlert(r)
return nil
}
@@ -447,7 +447,7 @@ func apiConnect(connReq: String) async -> ConnReqType? {
switch r {
case .sentConfirmation: return .invitation
case .sentInvitation: return .contact
case let .contactAlreadyExists(contact):
case let .contactAlreadyExists(_, contact):
let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) {
await MainActor.run { m.chatId = c.id }
@@ -457,19 +457,19 @@ func apiConnect(connReq: String) async -> ConnReqType? {
message: "You are already connected to \(contact.displayName)."
)
return nil
case .chatCmdError(.error(.invalidConnReq)):
case .chatCmdError(_, .error(.invalidConnReq)):
am.showAlertMsg(
title: "Invalid connection link",
message: "Please check that you used the correct link or ask your contact to send you another one."
)
return nil
case .chatCmdError(.errorAgent(.SMP(.AUTH))):
case .chatCmdError(_, .errorAgent(.SMP(.AUTH))):
am.showAlertMsg(
title: "Connection error (AUTH)",
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
)
return nil
case let .chatCmdError(.errorAgent(.INTERNAL(internalErr))):
case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))):
if internalErr == "SEUniqueID" {
am.showAlertMsg(
title: "Already connected?",
@@ -516,7 +516,7 @@ func deleteChat(_ chat: Chat) async {
func apiClearChat(type: ChatType, id: Int64) async throws -> ChatInfo {
let r = await chatSendCmd(.apiClearChat(type: type, id: id), bgTask: false)
if case let .chatCleared(updatedChatInfo) = r { return updatedChatInfo }
if case let .chatCleared(_, updatedChatInfo) = r { return updatedChatInfo }
throw r
}
@@ -533,7 +533,7 @@ func clearChat(_ chat: Chat) async {
func apiListContacts() throws -> [Contact] {
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiListContacts: no current user") }
let r = chatSendCmdSync(.apiListContacts(userId: userId))
if case let .contactsList(contacts) = r { return contacts }
if case let .contactsList(_, contacts) = r { return contacts }
throw r
}
@@ -542,33 +542,33 @@ func apiUpdateProfile(profile: Profile) async throws -> Profile? {
let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile))
switch r {
case .userProfileNoChange: return nil
case let .userProfileUpdated(_, toProfile): return toProfile
case let .userProfileUpdated(_, _, toProfile): return toProfile
default: throw r
}
}
func apiSetContactPrefs(contactId: Int64, preferences: Preferences) async throws -> Contact? {
let r = await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences))
if case let .contactPrefsUpdated(_, toContact) = r { return toContact }
if case let .contactPrefsUpdated(_, _, toContact) = r { return toContact }
throw r
}
func apiSetContactAlias(contactId: Int64, localAlias: String) async throws -> Contact? {
let r = await chatSendCmd(.apiSetContactAlias(contactId: contactId, localAlias: localAlias))
if case let .contactAliasUpdated(toContact) = r { return toContact }
if case let .contactAliasUpdated(_, toContact) = r { return toContact }
throw r
}
func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> PendingContactConnection? {
let r = await chatSendCmd(.apiSetConnectionAlias(connId: connId, localAlias: localAlias))
if case let .connectionAliasUpdated(toConnection) = r { return toConnection }
if case let .connectionAliasUpdated(_, toConnection) = r { return toConnection }
throw r
}
func apiCreateUserAddress() async throws -> String {
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiCreateUserAddress: no current user") }
let r = await chatSendCmd(.apiCreateMyAddress(userId: userId))
if case let .userContactLinkCreated(connReq) = r { return connReq }
if case let .userContactLinkCreated(_, connReq) = r { return connReq }
throw r
}
@@ -583,8 +583,8 @@ func apiGetUserAddress() throws -> UserContactLink? {
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetUserAddress: no current user") }
let r = chatSendCmdSync(.apiShowMyAddress(userId: userId))
switch r {
case let .userContactLink(contactLink): return contactLink
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
case let .userContactLink(_, contactLink): return contactLink
case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
default: throw r
}
}
@@ -593,8 +593,8 @@ func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContac
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("userAddressAutoAccept: no current user") }
let r = await chatSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept))
switch r {
case let .userContactLinkUpdated(contactLink): return contactLink
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
case let .userContactLinkUpdated(_, contactLink): return contactLink
case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
default: throw r
}
}
@@ -603,8 +603,8 @@ func apiAcceptContactRequest(contactReqId: Int64) async -> Contact? {
let r = await chatSendCmd(.apiAcceptContact(contactReqId: contactReqId))
let am = AlertManager.shared
if case let .acceptingContactRequest(contact) = r { return contact }
if case .chatCmdError(.errorAgent(.SMP(.AUTH))) = r {
if case let .acceptingContactRequest(_, contact) = r { return contact }
if case .chatCmdError(_, .errorAgent(.SMP(.AUTH))) = r {
am.showAlertMsg(
title: "Connection error (AUTH)",
message: "Sender may have deleted the connection request."
@@ -643,7 +643,7 @@ func receiveFile(fileId: Int64) async {
func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
let r = await chatSendCmd(.receiveFile(fileId: fileId, inline: inline))
let am = AlertManager.shared
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
if case .rcvFileAcceptedSndCancelled = r {
am.showAlertMsg(
title: "Cannot receive file",
@@ -652,7 +652,7 @@ func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
} else if !networkErrorAlert(r) {
logger.error("apiReceiveFile error: \(String(describing: r))")
switch r {
case .chatCmdError(.error(.fileAlreadyReceiving)):
case .chatCmdError(_, .error(.fileAlreadyReceiving)):
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
default:
am.showAlertMsg(
@@ -667,13 +667,13 @@ func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
func networkErrorAlert(_ r: ChatResponse) -> Bool {
let am = AlertManager.shared
switch r {
case let .chatCmdError(.errorAgent(.BROKER(addr, .TIMEOUT))):
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
am.showAlertMsg(
title: "Connection timeout",
message: "Please check your network connection with \(serverHostname(addr)) and try again."
)
return true
case let .chatCmdError(.errorAgent(.BROKER(addr, .NETWORK))):
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
am.showAlertMsg(
title: "Connection error",
message: "Please check your network connection with \(serverHostname(addr)) and try again."
@@ -788,13 +788,13 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo {
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiNewGroup: no current user") }
let r = chatSendCmdSync(.apiNewGroup(userId: userId, groupProfile: p))
if case let .groupCreated(groupInfo) = r { return groupInfo }
if case let .groupCreated(_, groupInfo) = r { return groupInfo }
throw r
}
func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember {
let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole))
if case let .sentGroupInvitation(_, _, member) = r { return member }
if case let .sentGroupInvitation(_, _, _, member) = r { return member }
throw r
}
@@ -807,22 +807,22 @@ enum JoinGroupResult {
func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult {
let r = await chatSendCmd(.apiJoinGroup(groupId: groupId))
switch r {
case let .userAcceptedGroupSent(groupInfo, _): return .joined(groupInfo: groupInfo)
case .chatCmdError(.errorAgent(.SMP(.AUTH))): return .invitationRemoved
case .chatCmdError(.errorStore(.groupNotFound)): return .groupNotFound
case let .userAcceptedGroupSent(_, groupInfo, _): return .joined(groupInfo: groupInfo)
case .chatCmdError(_, .errorAgent(.SMP(.AUTH))): return .invitationRemoved
case .chatCmdError(_, .errorStore(.groupNotFound)): return .groupNotFound
default: throw r
}
}
func apiRemoveMember(_ groupId: Int64, _ memberId: Int64) async throws -> GroupMember {
let r = await chatSendCmd(.apiRemoveMember(groupId: groupId, memberId: memberId), bgTask: false)
if case let .userDeletedMember(_, member) = r { return member }
if case let .userDeletedMember(_, _, member) = r { return member }
throw r
}
func apiMemberRole(_ groupId: Int64, _ memberId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember {
let r = await chatSendCmd(.apiMemberRole(groupId: groupId, memberId: memberId, memberRole: memberRole), bgTask: false)
if case let .memberRoleUser(_, member, _, _) = r { return member }
if case let .memberRoleUser(_, _, member, _, _) = r { return member }
throw r
}
@@ -837,19 +837,19 @@ func leaveGroup(_ groupId: Int64) async {
func apiLeaveGroup(_ groupId: Int64) async throws -> GroupInfo {
let r = await chatSendCmd(.apiLeaveGroup(groupId: groupId), bgTask: false)
if case let .leftMemberUser(groupInfo) = r { return groupInfo }
if case let .leftMemberUser(_, groupInfo) = r { return groupInfo }
throw r
}
func apiListMembers(_ groupId: Int64) async -> [GroupMember] {
let r = await chatSendCmd(.apiListMembers(groupId: groupId))
if case let .groupMembers(group) = r { return group.members }
if case let .groupMembers(_, group) = r { return group.members }
return []
}
func apiListMembersSync(_ groupId: Int64) -> [GroupMember] {
let r = chatSendCmdSync(.apiListMembers(groupId: groupId))
if case let .groupMembers(group) = r { return group.members }
if case let .groupMembers(_, group) = r { return group.members }
return []
}
@@ -863,13 +863,13 @@ func filterMembersToAdd(_ ms: [GroupMember]) -> [Contact] {
func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws -> GroupInfo {
let r = await chatSendCmd(.apiUpdateGroupProfile(groupId: groupId, groupProfile: groupProfile))
if case let .groupUpdated(toGroup) = r { return toGroup }
if case let .groupUpdated(_, toGroup) = r { return toGroup }
throw r
}
func apiCreateGroupLink(_ groupId: Int64) async throws -> String {
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId))
if case let .groupLinkCreated(_, connReq) = r { return connReq }
if case let .groupLinkCreated(_, _, connReq) = r { return connReq }
throw r
}
@@ -882,9 +882,9 @@ func apiDeleteGroupLink(_ groupId: Int64) async throws {
func apiGetGroupLink(_ groupId: Int64) throws -> String? {
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
switch r {
case let .groupLink(_, connReq):
case let .groupLink(_, _, connReq):
return connReq
case .chatCmdError(chatError: .errorStore(storeError: .groupLinkNotFound)):
case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)):
return nil
default: throw r
}
@@ -917,9 +917,9 @@ func startChat() throws {
let m = ChatModel.shared
try setNetworkConfig(getNetCfg())
let justStarted = try apiStartChat()
m.updateUsers(listUsers())
if justStarted {
try getUserChatData(m)
m.users = listUsers()
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCount())
try refreshCallInvitations()
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
@@ -927,7 +927,7 @@ func startChat() throws {
registerToken(token: token)
}
withAnimation {
m.onboardingStage = m.onboardingStage == .step2_CreateProfile
m.onboardingStage = m.onboardingStage == .step2_CreateProfile && m.users.count == 1
? .step3_SetNotificationsMode
: .onboardingComplete
}
@@ -988,25 +988,31 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.terminalItems.append(.resp(.now, res))
logger.debug("processReceivedMsg: \(res.responseType)")
switch res {
case let .newContactConnection(connection):
m.updateContactConnection(connection)
case let .contactConnectionDeleted(connection):
m.removeChat(connection.id)
case let .contactConnected(contact, _):
if contact.directOrUsed {
case let .newContactConnection(user, connection):
if active(user) {
m.updateContactConnection(connection)
}
case let .contactConnectionDeleted(user, connection):
if active(user) {
m.removeChat(connection.id)
}
case let .contactConnected(user, contact, _):
if active(user) && contact.directOrUsed {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
m.updateNetworkStatus(contact.id, .connected)
NtfManager.shared.notifyContactConnected(contact)
NtfManager.shared.notifyContactConnected(user, contact)
}
case let .contactConnecting(contact):
if contact.directOrUsed {
case let .contactConnecting(user, contact):
if active(user) && contact.directOrUsed {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
}
case let .receivedContactRequest(contactRequest):
case let .receivedContactRequest(user, contactRequest):
if !active(user) { return }
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
if m.hasChat(contactRequest.id) {
m.updateChatInfo(cInfo)
@@ -1015,27 +1021,34 @@ func processReceivedMsg(_ res: ChatResponse) async {
chatInfo: cInfo,
chatItems: []
))
NtfManager.shared.notifyContactRequest(contactRequest)
NtfManager.shared.notifyContactRequest(user, contactRequest)
}
case let .contactUpdated(toContact):
let cInfo = ChatInfo.direct(contact: toContact)
if m.hasChat(toContact.id) {
case let .contactUpdated(user, toContact):
if active(user) && m.hasChat(toContact.id) {
let cInfo = ChatInfo.direct(contact: toContact)
m.updateChatInfo(cInfo)
}
case let .contactsMerged(intoContact, mergedContact):
if m.hasChat(mergedContact.id) {
case let .contactsMerged(user, intoContact, mergedContact):
if active(user) && m.hasChat(mergedContact.id) {
if m.chatId == mergedContact.id {
m.chatId = intoContact.id
}
m.removeChat(mergedContact.id)
}
case let .contactsSubscribed(_, contactRefs):
updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs):
updateContactsStatus(contactRefs, status: .disconnected)
case let .contactSubError(contact, chatError):
processContactSubError(contact, chatError)
case let .contactSubSummary(contactSubscriptions):
case let .contactsSubscribed(user, _, contactRefs):
if active(user) {
updateContactsStatus(contactRefs, status: .connected)
}
case let .contactsDisconnected(user, _, contactRefs):
if active(user) {
updateContactsStatus(contactRefs, status: .disconnected)
}
case let .contactSubError(user, contact, chatError):
if active(user) {
processContactSubError(contact, chatError)
}
case let .contactSubSummary(user, contactSubscriptions):
if !active(user) { return }
for sub in contactSubscriptions {
if let err = sub.contactError {
processContactSubError(sub.contact, err)
@@ -1044,7 +1057,14 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.updateNetworkStatus(sub.contact.id, .connected)
}
}
case let .newChatItem(aChatItem):
case let .newChatItem(user, aChatItem):
if !active(user) {
if case .rcvNew = aChatItem.chatItem.meta.itemStatus {
m.increaseUnreadCounter(user: user)
}
return
}
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
m.addChatItem(cInfo, cItem)
@@ -1060,9 +1080,11 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
}
if cItem.showNotification {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
case let .chatItemStatusUpdated(aChatItem):
case let .chatItemStatusUpdated(user, aChatItem):
if !active(user) { return }
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
var res = false
@@ -1070,7 +1092,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
res = m.upsertChatItem(cInfo, cItem)
}
if res {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
} else if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
case .sndSent: endTask()
@@ -1079,48 +1101,87 @@ func processReceivedMsg(_ res: ChatResponse) async {
default: break
}
}
case let .chatItemUpdated(aChatItem):
chatItemSimpleUpdate(aChatItem)
case let .chatItemDeleted(deletedChatItem, toChatItem, _):
case let .chatItemUpdated(user, aChatItem):
if active(user) {
chatItemSimpleUpdate(aChatItem)
}
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
if !active(user) {
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew {
m.decreaseUnreadCounter(user: user)
}
return
}
if let toChatItem = toChatItem {
_ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem)
} else {
m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem)
}
case let .receivedGroupInvitation(groupInfo, _, _):
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
case let .userAcceptedGroupSent(groupInfo, hostContact):
case let .receivedGroupInvitation(user, groupInfo, _, _):
if active(user) {
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
}
case let .userAcceptedGroupSent(user, groupInfo, hostContact):
if !active(user) { return }
m.updateGroup(groupInfo)
if let hostContact = hostContact {
m.dismissConnReqView(hostContact.activeConn.id)
m.removeChat(hostContact.activeConn.id)
}
case let .joinedGroupMemberConnecting(groupInfo, _, member):
_ = m.upsertGroupMember(groupInfo, member)
case let .deletedMemberUser(groupInfo, _): // TODO update user member
m.updateGroup(groupInfo)
case let .deletedMember(groupInfo, _, deletedMember):
_ = m.upsertGroupMember(groupInfo, deletedMember)
case let .leftMember(groupInfo, member):
_ = m.upsertGroupMember(groupInfo, member)
case let .groupDeleted(groupInfo, _): // TODO update user member
m.updateGroup(groupInfo)
case let .userJoinedGroup(groupInfo):
m.updateGroup(groupInfo)
case let .joinedGroupMember(groupInfo, member):
_ = m.upsertGroupMember(groupInfo, member)
case let .connectedToGroupMember(groupInfo, member):
_ = m.upsertGroupMember(groupInfo, member)
case let .groupUpdated(toGroup):
m.updateGroup(toGroup)
case let .rcvFileStart(aChatItem):
chatItemSimpleUpdate(aChatItem)
case let .rcvFileComplete(aChatItem):
chatItemSimpleUpdate(aChatItem)
case let .sndFileStart(aChatItem, _):
chatItemSimpleUpdate(aChatItem)
case let .sndFileComplete(aChatItem, _):
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
case let .deletedMemberUser(user, groupInfo, _): // TODO update user member
if active(user) {
m.updateGroup(groupInfo)
}
case let .deletedMember(user, groupInfo, _, deletedMember):
if active(user) {
_ = m.upsertGroupMember(groupInfo, deletedMember)
}
case let .leftMember(user, groupInfo, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
case let .groupDeleted(user, groupInfo, _): // TODO update user member
if active(user) {
m.updateGroup(groupInfo)
}
case let .userJoinedGroup(user, groupInfo):
if active(user) {
m.updateGroup(groupInfo)
}
case let .joinedGroupMember(user, groupInfo, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
case let .connectedToGroupMember(user, groupInfo, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
case let .groupUpdated(user, toGroup):
if active(user) {
m.updateGroup(toGroup)
}
case let .rcvFileStart(user, aChatItem):
if active(user) {
chatItemSimpleUpdate(aChatItem)
}
case let .rcvFileComplete(user, aChatItem):
if active(user) {
chatItemSimpleUpdate(aChatItem)
}
case let .sndFileStart(user, aChatItem, _):
if active(user) {
chatItemSimpleUpdate(aChatItem)
}
case let .sndFileComplete(user, aChatItem, _):
if !active(user) { return }
chatItemSimpleUpdate(aChatItem)
let cItem = aChatItem.chatItem
let mc = cItem.content.msgContent
@@ -1145,7 +1206,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
// logger.debug("reportNewIncomingVoIPPushPayload success for \(contact.id)")
// }
// }
case let .callOffer(contact, callType, offer, sharedKey, _):
case let .callOffer(_, contact, callType, offer, sharedKey, _):
withCall(contact) { call in
call.callState = .offerReceived
call.peerMedia = callType.media
@@ -1163,16 +1224,16 @@ func processReceivedMsg(_ res: ChatResponse) async {
relay: useRelay
)
}
case let .callAnswer(contact, answer):
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):
case let .callExtraInfo(_, contact, extraInfo):
withCall(contact) { _ in
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
}
case let .callEnded(contact):
case let .callEnded(_, contact):
if let invitation = m.callInvitations.removeValue(forKey: contact.id) {
CallController.shared.reportCallRemoteEnded(invitation: invitation)
}
@@ -1186,6 +1247,10 @@ func processReceivedMsg(_ res: ChatResponse) async {
logger.debug("unsupported event: \(res.responseType)")
}
func active(_ user: User) -> Bool {
user.id == m.currentUser?.id
}
func withCall(_ contact: Contact, _ perform: (Call) -> Void) {
if let call = m.activeCall, call.contact.apiId == contact.apiId {
perform(call)
@@ -1201,7 +1266,7 @@ func chatItemSimpleUpdate(_ aChatItem: AChatItem) {
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
if m.upsertChatItem(cInfo, cItem) {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
NtfManager.shared.notifyMessageReceived(m.currentUser!, cInfo, cItem)
}
}

View File

@@ -36,6 +36,7 @@ class CallManager {
func answerIncomingCall(invitation: RcvCallInvitation) {
let m = ChatModel.shared
// TODO: change active user
m.callInvitations.removeValue(forKey: invitation.contact.id)
m.activeCall = Call(
direction: .incoming,

View File

@@ -416,9 +416,9 @@ struct ErrorAlert {
func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert {
switch error as? ChatResponse {
case let .chatCmdError(.errorAgent(.BROKER(addr, .TIMEOUT))):
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
return ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
case let .chatCmdError(.errorAgent(.BROKER(addr, .NETWORK))):
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
return ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
default:
return ErrorAlert(title: title, message: "Error: \(responseError(error))")

View File

@@ -29,45 +29,56 @@ struct UserPicker: View {
Spacer().frame(height: 1)
VStack(spacing: 0) {
ScrollView {
VStack(spacing: 0) {
ForEach(Array(chatModel.users.enumerated()), id: \.0) { i, userInfo in
Button(action: {
if !userInfo.user.activeUser {
changeActiveUser(toUser: userInfo)
}
}, label: {
HStack(spacing: 0) {
ProfileImage(imageStr: userInfo.user.image)
ScrollViewReader { sp in
VStack(spacing: 0) {
ForEach(Array(chatModel.users.enumerated()), id: \.0) { i, userInfo in
Button(action: {
if !userInfo.user.activeUser {
chatModel.changeActiveUser(userInfo.user.userId)
userPickerVisible = false
}
}, label: {
HStack(spacing: 0) {
ProfileImage(imageStr: userInfo.user.image)
.frame(width: 44, height: 44)
.padding(.trailing, 12)
Text(userInfo.user.chatViewName)
Text(userInfo.user.chatViewName)
.fontWeight(i == 0 ? .medium : .regular)
.foregroundColor(.primary)
.overlay(DetermineWidth())
Spacer()
if i == 0 {
Image(systemName: "checkmark")
} else if userInfo.unreadCount > 0 {
unreadCounter(userInfo.unreadCount)
Spacer()
if i == 0 {
Image(systemName: "checkmark")
} else if userInfo.unreadCount > 0 {
unreadCounter(userInfo.unreadCount)
}
}
.padding(.trailing)
.padding([.leading, .vertical], 12)
})
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
if i < chatModel.users.count - 1 {
Divider()
}
.padding(.trailing)
.padding([.leading, .vertical], 12)
})
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
if i < chatModel.users.count - 1 {
Divider()
}
}
}
.overlay {
GeometryReader { geo -> Color in
DispatchQueue.main.async {
scrollViewContentSize = geo.size
let layoutFrame = UIApplication.shared.windows[0].safeAreaLayoutGuide.layoutFrame
disableScrolling = scrollViewContentSize.height + menuButtonHeight * 2 + 10 < layoutFrame.height
.overlay {
GeometryReader { geo -> Color in
DispatchQueue.main.async {
scrollViewContentSize = geo.size
let scenes = UIApplication.shared.connectedScenes
if let windowScene = scenes.first as? UIWindowScene {
let layoutFrame = windowScene.windows[0].safeAreaLayoutGuide.layoutFrame
disableScrolling = scrollViewContentSize.height + menuButtonHeight * 2 + 10 < layoutFrame.height
}
}
return Color.clear
}
}
.onChange(of: userPickerVisible) { visible in
if visible {
sp.scrollTo(0)
}
return Color.clear
}
}
}
@@ -108,36 +119,15 @@ struct UserPicker: View {
private func reloadCurrentUser() {
if let updatedUser = chatModel.currentUser, let index = chatModel.users.firstIndex(where: { $0.user.userId == updatedUser.userId }) {
let removed = chatModel.users.remove(at: index)
chatModel.users.insert(UserInfo(user: updatedUser, unreadCount: removed.unreadCount), at: 0)
}
}
private func changeActiveUser(toUser: UserInfo) {
Task {
do {
let activeUser = try apiSetActiveUser(toUser.user.userId)
let oldActiveIndex = chatModel.users.firstIndex(where: { $0.user.userId == chatModel.currentUser?.userId })!
var oldActive = chatModel.users[oldActiveIndex]
oldActive.user.activeUser = false
chatModel.users[oldActiveIndex] = oldActive
chatModel.currentUser = activeUser
let currentActiveIndex = chatModel.users.firstIndex(where: { $0.user.userId == activeUser.userId })!
let removed = chatModel.users.remove(at: currentActiveIndex)
chatModel.users.insert(UserInfo(user: activeUser, unreadCount: removed.unreadCount), at: 0)
chatModel.users = chatModel.users.map { $0 }
try getUserChatData(chatModel)
userPickerVisible = false
} catch {
logger.error("Unable to set active user: \(error.localizedDescription)")
}
var users = chatModel.users
users[index] = UserInfo(user: updatedUser, unreadCount: users[index].unreadCount)
chatModel.updateUsers(users)
}
}
private func reloadUsers() {
Task {
chatModel.users = listUsers().sorted { one, two -> Bool in one.user.activeUser }
chatModel.updateUsers(listUsers())
}
}
@@ -171,7 +161,7 @@ func unreadCounter(_ unread: Int64) -> some View {
struct UserPicker_Previews: PreviewProvider {
static var previews: some View {
let m = ChatModel()
m.users = [UserInfo.sampleData, UserInfo.sampleData]
m.updateUsers([UserInfo.sampleData, UserInfo.sampleData])
return UserPicker(
showSettings: Binding.constant(false),
userPickerVisible: Binding.constant(true)

View File

@@ -152,7 +152,7 @@ struct DatabaseEncryptionView: View {
await operationEnded(.databaseEncrypted)
}
} catch let error {
if case .chatCmdError(.errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
await operationEnded(.currentPassphraseError)
} else {
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))

View File

@@ -11,6 +11,7 @@ import SimpleXChat
struct CreateProfile: View {
@EnvironmentObject var m: ChatModel
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var displayName: String = ""
@State private var fullName: String = ""
@FocusState private var focusDisplayName
@@ -97,8 +98,12 @@ struct CreateProfile: View {
do {
m.currentUser = try apiCreateActiveUser(profile)
try startChat()
withAnimation { m.onboardingStage = .step3_SetNotificationsMode }
if m.users.count == 1 {
withAnimation { m.onboardingStage = .step3_SetNotificationsMode }
} else {
presentationMode.wrappedValue.dismiss()
try getUserChatData(m)
}
} catch {
fatalError("Failed to create user or start chat: \(responseError(error))")
}

View File

@@ -108,7 +108,7 @@ struct TerminalView: View {
func sendMessage() {
let cmd = ChatCommand.string(composeState.message)
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
let resp = ChatResponse.chatCmdError(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 {
ChatModel.shared.terminalItems.append(.cmd(.now, cmd))
ChatModel.shared.terminalItems.append(.resp(.now, resp))

View File

@@ -11,23 +11,24 @@ struct UserProfilesView: View {
@Environment(\.editMode) private var editMode
@State private var selectedUser: Int? = nil
@State private var showAddUser: Bool? = false
@State private var showScanSMPServer = false
@State private var testing = false
var body: some View {
List {
Section("Your profiles") {
ForEach(Array($m.users.enumerated()), id: \.0) { i, userInfo in
ForEach(Array(m.users.enumerated()), id: \.0) { i, userInfo in
userProfileView(userInfo, index: i)
.deleteDisabled(userInfo.wrappedValue.user.activeUser)
.deleteDisabled(userInfo.user.activeUser)
}
.onDelete { indexSet in
do {
try apiDeleteUser(m.users[indexSet.first!].user.userId)
m.users.remove(atOffsets: indexSet)
} catch {
fatalError("Failed to delete user: \(responseError(error))")
}
AlertManager.shared.showAlert(
Alert(
title: Text("Delete profile?"),
message: Text("All chats and messages will be deleted - this cannot be undone!"),
primaryButton: .destructive(Text("Delete")) {
removeUser(index: indexSet.first!)
},
secondaryButton: .cancel()
))
}
NavigationLink(destination: CreateProfile(), tag: true, selection: $showAddUser) {
Text("Add profile…")
@@ -37,8 +38,21 @@ struct UserProfilesView: View {
.toolbar { EditButton() }
}
private func userProfileView(_ userBinding: Binding<UserInfo>, index: Int) -> some View {
let user = userBinding.wrappedValue.user
private func removeUser(index: Int) {
do {
try apiDeleteUser(m.users[index].user.userId)
var users = m.users
users.remove(at: index)
m.updateUsers(users)
} catch {
AlertManager.shared.showAlertMsg(
title: "Failed to delete the user",
message: "Error: \(responseError(error))"
)
}
}
private func userProfileView(_ userBinding: UserInfo, index: Int) -> some View {
let user = userBinding.user
return NavigationLink(tag: index, selection: $selectedUser) {
// UserPrefs(user: userBinding, index: index)
// .navigationBarTitle(user.chatViewName)

View File

@@ -107,20 +107,22 @@ class NotificationService: UNNotificationServiceExtension {
let encNtfInfo = ntfData["message"] as? String,
let dbStatus = startChat() {
if case .ok = dbStatus,
let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)")
if let connEntity = ntfMsgInfo.connEntity {
setBestAttemptNtf(createConnectionEventNtf(connEntity))
if let id = connEntity.id {
Task {
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)")
await PendingNtfs.shared.createStream(id)
await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count)
deliverBestAttemptNtf()
let ntfMsgInfos = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
for ntfMsgInfo in ntfMsgInfos {
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)")
if let connEntity = ntfMsgInfo.connEntity {
setBestAttemptNtf(createConnectionEventNtf(ntfMsgInfo.user, connEntity))
if let id = connEntity.id {
Task {
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)")
await PendingNtfs.shared.createStream(id)
await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count)
deliverBestAttemptNtf()
}
}
return
}
}
return
} else {
setBestAttemptNtf(createErrorNtf(dbStatus))
}
@@ -209,13 +211,13 @@ func chatRecvMsg() async -> ChatResponse? {
func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotificationContent)? {
logger.debug("NotificationService processReceivedMsg: \(res.responseType)")
switch res {
case let .contactConnected(contact, _):
return (contact.id, createContactConnectedNtf(contact))
case let .contactConnected(user, contact, _):
return (contact.id, createContactConnectedNtf(user, contact))
// case let .contactConnecting(contact):
// TODO profile update
case let .receivedContactRequest(contactRequest):
return (UserContact(contactRequest: contactRequest).id, createContactRequestNtf(contactRequest))
case let .newChatItem(aChatItem):
case let .receivedContactRequest(user, contactRequest):
return (UserContact(contactRequest: contactRequest).id, createContactRequestNtf(user, contactRequest))
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
var cItem = aChatItem.chatItem
if case .image = cItem.content.msgContent {
@@ -234,7 +236,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotification
cItem = apiReceiveFile(fileId: file.fileId, inline: inline)?.chatItem ?? cItem
}
}
return cItem.showMutableNotification ? (aChatItem.chatId, createMessageReceivedNtf(cInfo, cItem)) : nil
return cItem.showMutableNotification ? (aChatItem.chatId, createMessageReceivedNtf(user, cInfo, cItem)) : nil
case let .callInvitation(invitation):
return (invitation.contact.id, createCallInvitationNtf(invitation))
default:
@@ -256,12 +258,21 @@ func updateNetCfg() {
}
}
func listUsers() -> [UserInfo] {
let r = sendSimpleXCmd(.listUsers)
logger.debug("listUsers sendSimpleXCmd response: \(String(describing: r))")
switch r {
case let .usersList(users): return users
default: return []
}
}
func apiGetActiveUser() -> User? {
let r = sendSimpleXCmd(.showActiveUser)
logger.debug("apiGetActiveUser sendSimpleXCmd responce: \(String(describing: r))")
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(String(describing: r))")
switch r {
case let .activeUser(user): return user
case .chatCmdError(.error(.noActiveUser)): return nil
case .chatCmdError(_, .error(.noActiveUser)): return nil
default:
logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))")
return nil
@@ -289,22 +300,26 @@ func apiSetIncognito(incognito: Bool) throws {
throw r
}
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
guard let user = apiGetActiveUser() else {
logger.debug("no active user")
return nil
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> [NtfMessages]? {
let users = listUsers()
if users.isEmpty {
logger.debug("no users")
return []
}
let r = sendSimpleXCmd(.apiGetNtfMessage(userId: user.userId, nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r {
return NtfMessages(connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages)
var result: [NtfMessages] = []
users.forEach {
let r = sendSimpleXCmd(.apiGetNtfMessage(userId: $0.user.userId, nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfMessages(user, connEntity, msgTs, ntfMessages) = r {
result.append(NtfMessages(user: user, connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages))
}
logger.debug("apiGetNtfMessage ignored response: \(String.init(describing: r), privacy: .public)")
}
logger.debug("apiGetNtfMessage ignored response: \(String.init(describing: r), privacy: .public)")
return nil
return result
}
func apiReceiveFile(fileId: Int64, inline: Bool) -> AChatItem? {
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, inline: inline))
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
logger.error("receiveFile error: \(responseError(r))")
return nil
}
@@ -316,6 +331,7 @@ func setNetworkConfig(_ cfg: NetCfg) throws {
}
struct NtfMessages {
var user: User
var connEntity: ConnectionEntity?
var msgTs: Date?
var ntfMessages: [NtfMsgInfo]

View File

@@ -134,6 +134,7 @@ public func chatResponse(_ s: String) -> ChatResponse {
type = jResp.allKeys[0] as? String
if type == "apiChats" {
if let jApiChats = jResp["apiChats"] as? NSDictionary,
let user: User = try? decodeObject(jApiChats["user"] as Any),
let jChats = jApiChats["chats"] as? NSArray {
let chats = jChats.map { jChat in
if let chatData = try? parseChatData(jChat) {
@@ -141,13 +142,14 @@ public func chatResponse(_ s: String) -> ChatResponse {
}
return ChatData.invalidJSON(prettyJSON(jChat) ?? "")
}
return .apiChats(chats: chats)
return .apiChats(user: user, chats: chats)
}
} else if type == "apiChat" {
if let jApiChat = jResp["apiChat"] as? NSDictionary,
let user: User = try? decodeObject(jApiChat["user"] as Any),
let jChat = jApiChat["chat"] as? NSDictionary,
let chat = try? parseChatData(jChat) {
return .apiChat(chat: chat)
return .apiChat(user: user, chat: chat)
}
}
}

View File

@@ -316,102 +316,102 @@ public enum ChatResponse: Decodable, Error {
case chatRunning
case chatStopped
case chatSuspended
case apiChats(chats: [ChatData])
case apiChat(chat: ChatData)
case userSMPServers(smpServers: [ServerCfg], presetSMPServers: [String])
case smpTestResult(smpTestFailure: SMPTestFailure?)
case chatItemTTL(chatItemTTL: Int64?)
case apiChats(user: User, chats: [ChatData])
case apiChat(user: User, chat: ChatData)
case userSMPServers(user: User, smpServers: [ServerCfg], presetSMPServers: [String])
case smpTestResult(user: User, smpTestFailure: SMPTestFailure?)
case chatItemTTL(user: User, chatItemTTL: Int64?)
case networkConfig(networkConfig: NetCfg)
case contactInfo(contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?)
case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
case contactCode(contact: Contact, connectionCode: String)
case groupMemberCode(groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
case connectionVerified(verified: Bool, expectedCode: String)
case invitation(connReqInvitation: String)
case sentConfirmation
case sentInvitation
case contactAlreadyExists(contact: Contact)
case contactDeleted(contact: Contact)
case chatCleared(chatInfo: ChatInfo)
case userProfileNoChange
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
case contactAliasUpdated(toContact: Contact)
case connectionAliasUpdated(toConnection: PendingContactConnection)
case contactPrefsUpdated(fromContact: Contact, toContact: Contact)
case userContactLink(contactLink: UserContactLink)
case userContactLinkUpdated(contactLink: UserContactLink)
case userContactLinkCreated(connReqContact: String)
case userContactLinkDeleted
case contactConnected(contact: Contact, userCustomProfile: Profile?)
case contactConnecting(contact: Contact)
case receivedContactRequest(contactRequest: UserContactRequest)
case acceptingContactRequest(contact: Contact)
case contactRequestRejected
case contactUpdated(toContact: Contact)
case contactsSubscribed(server: String, contactRefs: [ContactRef])
case contactsDisconnected(server: String, contactRefs: [ContactRef])
case contactSubError(contact: Contact, chatError: ChatError)
case contactSubSummary(contactSubscriptions: [ContactSubStatus])
case groupSubscribed(groupInfo: GroupInfo)
case memberSubErrors(memberSubErrors: [MemberSubError])
case groupEmpty(groupInfo: GroupInfo)
case contactInfo(user: User, contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?)
case groupMemberInfo(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
case contactCode(user: User, contact: Contact, connectionCode: String)
case groupMemberCode(user: User, groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
case connectionVerified(user: User, verified: Bool, expectedCode: String)
case invitation(user: User, connReqInvitation: String)
case sentConfirmation(user: User)
case sentInvitation(user: User)
case contactAlreadyExists(user: User, contact: Contact)
case contactDeleted(user: User, contact: Contact)
case chatCleared(user: User, chatInfo: ChatInfo)
case userProfileNoChange(user: User)
case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile)
case contactAliasUpdated(user: User, toContact: Contact)
case connectionAliasUpdated(user: User, toConnection: PendingContactConnection)
case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact)
case userContactLink(user: User, contactLink: UserContactLink)
case userContactLinkUpdated(user: User, contactLink: UserContactLink)
case userContactLinkCreated(user: User, connReqContact: String)
case userContactLinkDeleted(user: User)
case contactConnected(user: User, contact: Contact, userCustomProfile: Profile?)
case contactConnecting(user: User, contact: Contact)
case receivedContactRequest(user: User, contactRequest: UserContactRequest)
case acceptingContactRequest(user: User, contact: Contact)
case contactRequestRejected(user: User)
case contactUpdated(user: User, toContact: Contact)
case contactsSubscribed(user: User, server: String, contactRefs: [ContactRef])
case contactsDisconnected(user: User, server: String, contactRefs: [ContactRef])
case contactSubError(user: User, contact: Contact, chatError: ChatError)
case contactSubSummary(user: User, contactSubscriptions: [ContactSubStatus])
case groupSubscribed(user: User, groupInfo: GroupInfo)
case memberSubErrors(user: User, memberSubErrors: [MemberSubError])
case groupEmpty(user: User, groupInfo: GroupInfo)
case userContactLinkSubscribed
case newChatItem(chatItem: AChatItem)
case chatItemStatusUpdated(chatItem: AChatItem)
case chatItemUpdated(chatItem: AChatItem)
case chatItemDeleted(deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool)
case contactsList(contacts: [Contact])
case newChatItem(user: User, chatItem: AChatItem)
case chatItemStatusUpdated(user: User, chatItem: AChatItem)
case chatItemUpdated(user: User, chatItem: AChatItem)
case chatItemDeleted(user: User, deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool)
case contactsList(user: User, contacts: [Contact])
// group events
case groupCreated(groupInfo: GroupInfo)
case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact, member: GroupMember)
case userAcceptedGroupSent(groupInfo: GroupInfo, hostContact: Contact?)
case userDeletedMember(groupInfo: GroupInfo, member: GroupMember)
case leftMemberUser(groupInfo: GroupInfo)
case groupMembers(group: Group)
case receivedGroupInvitation(groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole)
case groupDeletedUser(groupInfo: GroupInfo)
case joinedGroupMemberConnecting(groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember)
case memberRole(groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole)
case memberRoleUser(groupInfo: GroupInfo, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole)
case deletedMemberUser(groupInfo: GroupInfo, member: GroupMember)
case deletedMember(groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember)
case leftMember(groupInfo: GroupInfo, member: GroupMember)
case groupDeleted(groupInfo: GroupInfo, member: GroupMember)
case contactsMerged(intoContact: Contact, mergedContact: Contact)
case groupInvitation(groupInfo: GroupInfo) // unused
case userJoinedGroup(groupInfo: GroupInfo)
case joinedGroupMember(groupInfo: GroupInfo, member: GroupMember)
case connectedToGroupMember(groupInfo: GroupInfo, member: GroupMember)
case groupRemoved(groupInfo: GroupInfo) // unused
case groupUpdated(toGroup: GroupInfo)
case groupLinkCreated(groupInfo: GroupInfo, connReqContact: String)
case groupLink(groupInfo: GroupInfo, connReqContact: String)
case groupLinkDeleted(groupInfo: GroupInfo)
case groupCreated(user: User, groupInfo: GroupInfo)
case sentGroupInvitation(user: User, groupInfo: GroupInfo, contact: Contact, member: GroupMember)
case userAcceptedGroupSent(user: User, groupInfo: GroupInfo, hostContact: Contact?)
case userDeletedMember(user: User, groupInfo: GroupInfo, member: GroupMember)
case leftMemberUser(user: User, groupInfo: GroupInfo)
case groupMembers(user: User, group: Group)
case receivedGroupInvitation(user: User, groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole)
case groupDeletedUser(user: User, groupInfo: GroupInfo)
case joinedGroupMemberConnecting(user: User, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember)
case memberRole(user: User, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole)
case memberRoleUser(user: User, groupInfo: GroupInfo, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole)
case deletedMemberUser(user: User, groupInfo: GroupInfo, member: GroupMember)
case deletedMember(user: User, groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember)
case leftMember(user: User, groupInfo: GroupInfo, member: GroupMember)
case groupDeleted(user: User, groupInfo: GroupInfo, member: GroupMember)
case contactsMerged(user: User, intoContact: Contact, mergedContact: Contact)
case groupInvitation(user: User, groupInfo: GroupInfo) // unused
case userJoinedGroup(user: User, groupInfo: GroupInfo)
case joinedGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
case connectedToGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
case groupRemoved(user: User, groupInfo: GroupInfo) // unused
case groupUpdated(user: User, toGroup: GroupInfo)
case groupLinkCreated(user: User, groupInfo: GroupInfo, connReqContact: String)
case groupLink(user: User, groupInfo: GroupInfo, connReqContact: String)
case groupLinkDeleted(user: User, groupInfo: GroupInfo)
// receiving file events
case rcvFileAccepted(chatItem: AChatItem)
case rcvFileAcceptedSndCancelled(rcvFileTransfer: RcvFileTransfer)
case rcvFileStart(chatItem: AChatItem)
case rcvFileComplete(chatItem: AChatItem)
case rcvFileAccepted(user: User, chatItem: AChatItem)
case rcvFileAcceptedSndCancelled(user: User, rcvFileTransfer: RcvFileTransfer)
case rcvFileStart(user: User, chatItem: AChatItem)
case rcvFileComplete(user: User, chatItem: AChatItem)
// sending file events
case sndFileStart(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileComplete(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileStart(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileComplete(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileRcvCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndGroupFileCancelled(chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
case sndFileRcvCancelled(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndGroupFileCancelled(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
case callInvitation(callInvitation: RcvCallInvitation)
case callOffer(contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
case callAnswer(contact: Contact, answer: WebRTCSession)
case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
case callEnded(contact: Contact)
case callOffer(user: User, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
case callAnswer(user: User, contact: Contact, answer: WebRTCSession)
case callExtraInfo(user: User, contact: Contact, extraInfo: WebRTCExtraInfo)
case callEnded(user: User, contact: Contact)
case callInvitations(callInvitations: [RcvCallInvitation])
case ntfTokenStatus(status: NtfTknStatus)
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
case ntfMessages(connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
case newContactConnection(connection: PendingContactConnection)
case contactConnectionDeleted(connection: PendingContactConnection)
case cmdOk
case chatCmdError(chatError: ChatError)
case chatError(chatError: ChatError)
case ntfMessages(user: User, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
case newContactConnection(user: User, connection: PendingContactConnection)
case contactConnectionDeleted(user: User, connection: PendingContactConnection)
case cmdOk(user: User?)
case chatCmdError(user: User?, chatError: ChatError)
case chatError(user: User?, chatError: ChatError)
public var responseType: String {
get {
@@ -530,104 +530,111 @@ public enum ChatResponse: Decodable, Error {
case .chatRunning: return noDetails
case .chatStopped: return noDetails
case .chatSuspended: return noDetails
case let .apiChats(chats): return String(describing: chats)
case let .apiChat(chat): return String(describing: chat)
case let .userSMPServers(smpServers, presetServers): return "smpServers: \(String(describing: smpServers))\npresetServers: \(String(describing: presetServers))"
case let .smpTestResult(smpTestFailure): return String(describing: smpTestFailure)
case let .chatItemTTL(chatItemTTL): return String(describing: chatItemTTL)
case let .apiChats(u, chats): return withUser(u, String(describing: chats))
case let .apiChat(u, chat): return withUser(u, String(describing: chat))
case let .userSMPServers(u, smpServers, presetServers): return withUser(u, "smpServers: \(String(describing: smpServers))\npresetServers: \(String(describing: presetServers))")
case let .smpTestResult(u, smpTestFailure): return withUser(u, String(describing: smpTestFailure))
case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL))
case let .networkConfig(networkConfig): return String(describing: networkConfig)
case let .contactInfo(contact, connectionStats, customUserProfile): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))"
case let .groupMemberInfo(groupInfo, member, connectionStats_): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_)))"
case let .contactCode(contact, connectionCode): return "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)"
case let .groupMemberCode(groupInfo, member, connectionCode): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)"
case let .connectionVerified(verified, expectedCode): return "verified: \(verified)\nconnectionCode: \(expectedCode)"
case let .invitation(connReqInvitation): return connReqInvitation
case let .contactInfo(u, contact, connectionStats, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))")
case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_)))")
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
case let .invitation(u, connReqInvitation): return withUser(u, connReqInvitation)
case .sentConfirmation: return noDetails
case .sentInvitation: return noDetails
case let .contactAlreadyExists(contact): return String(describing: contact)
case let .contactDeleted(contact): return String(describing: contact)
case let .chatCleared(chatInfo): return String(describing: chatInfo)
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
case let .contactDeleted(u, contact): return withUser(u, String(describing: contact))
case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo))
case .userProfileNoChange: return noDetails
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
case let .contactAliasUpdated(toContact): return String(describing: toContact)
case let .connectionAliasUpdated(toConnection): return String(describing: toConnection)
case let .contactPrefsUpdated(fromContact, toContact): return "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))"
case let .userContactLink(contactLink): return contactLink.responseDetails
case let .userContactLinkUpdated(contactLink): return contactLink.responseDetails
case let .userContactLinkCreated(connReq): return connReq
case let .userProfileUpdated(u, _, toProfile): return withUser(u, String(describing: toProfile))
case let .contactAliasUpdated(u, toContact): return withUser(u, String(describing: toContact))
case let .connectionAliasUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
case let .contactPrefsUpdated(u, fromContact, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))")
case let .userContactLink(u, contactLink): return withUser(u, contactLink.responseDetails)
case let .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails)
case let .userContactLinkCreated(u, connReq): return withUser(u, connReq)
case .userContactLinkDeleted: return noDetails
case let .contactConnected(contact, _): return String(describing: contact)
case let .contactConnecting(contact): return String(describing: contact)
case let .receivedContactRequest(contactRequest): return String(describing: contactRequest)
case let .acceptingContactRequest(contact): return String(describing: contact)
case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact))
case let .contactConnecting(u, contact): return withUser(u, String(describing: contact))
case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest))
case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact))
case .contactRequestRejected: return noDetails
case let .contactUpdated(toContact): return String(describing: toContact)
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactSubError(contact, chatError): return "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))"
case let .contactSubSummary(contactSubscriptions): return String(describing: contactSubscriptions)
case let .groupSubscribed(groupInfo): return String(describing: groupInfo)
case let .memberSubErrors(memberSubErrors): return String(describing: memberSubErrors)
case let .groupEmpty(groupInfo): return String(describing: groupInfo)
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
case let .contactsSubscribed(u, server, contactRefs): return withUser(u, "server: \(server)\ncontacts:\n\(String(describing: contactRefs))")
case let .contactsDisconnected(u, server, contactRefs): return withUser(u, "server: \(server)\ncontacts:\n\(String(describing: contactRefs))")
case let .contactSubError(u, contact, chatError): return withUser(u, "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))")
case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions))
case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .memberSubErrors(u, memberSubErrors): return withUser(u, String(describing: memberSubErrors))
case let .groupEmpty(u, groupInfo): return withUser(u, String(describing: groupInfo))
case .userContactLinkSubscribed: return noDetails
case let .newChatItem(chatItem): return String(describing: chatItem)
case let .chatItemStatusUpdated(chatItem): return String(describing: chatItem)
case let .chatItemUpdated(chatItem): return String(describing: chatItem)
case let .chatItemDeleted(deletedChatItem, toChatItem, byUser): return "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)"
case let .contactsList(contacts): return String(describing: contacts)
case let .groupCreated(groupInfo): return String(describing: groupInfo)
case let .sentGroupInvitation(groupInfo, contact, member): return "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)"
case let .userAcceptedGroupSent(groupInfo, hostContact): return "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))"
case let .userDeletedMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
case let .leftMemberUser(groupInfo): return String(describing: groupInfo)
case let .groupMembers(group): return String(describing: group)
case let .receivedGroupInvitation(groupInfo, contact, memberRole): return "groupInfo: \(groupInfo)\ncontact: \(contact)\nmemberRole: \(memberRole)"
case let .groupDeletedUser(groupInfo): return String(describing: groupInfo)
case let .joinedGroupMemberConnecting(groupInfo, hostMember, member): return "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)"
case let .memberRole(groupInfo, byMember, member, fromRole, toRole): return "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)"
case let .memberRoleUser(groupInfo, member, fromRole, toRole): return "groupInfo: \(groupInfo)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)"
case let .deletedMemberUser(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
case let .deletedMember(groupInfo, byMember, deletedMember): return "groupInfo: \(groupInfo)\nbyMember: \(byMember)\ndeletedMember: \(deletedMember)"
case let .leftMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
case let .groupDeleted(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
case let .contactsMerged(intoContact, mergedContact): return "intoContact: \(intoContact)\nmergedContact: \(mergedContact)"
case let .groupInvitation(groupInfo): return String(describing: groupInfo)
case let .userJoinedGroup(groupInfo): return String(describing: groupInfo)
case let .joinedGroupMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
case let .connectedToGroupMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
case let .groupRemoved(groupInfo): return String(describing: groupInfo)
case let .groupUpdated(toGroup): return String(describing: toGroup)
case let .groupLinkCreated(groupInfo, connReqContact): return "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)"
case let .groupLink(groupInfo, connReqContact): return "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)"
case let .groupLinkDeleted(groupInfo): return String(describing: groupInfo)
case let .rcvFileAccepted(chatItem): return String(describing: chatItem)
case let .newChatItem(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemStatusUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemDeleted(u, deletedChatItem, toChatItem, byUser): return withUser(u, "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)")
case let .contactsList(u, contacts): return withUser(u, String(describing: contacts))
case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)")
case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))")
case let .userDeletedMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .groupMembers(u, group): return withUser(u, String(describing: group))
case let .receivedGroupInvitation(u, groupInfo, contact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmemberRole: \(memberRole)")
case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .joinedGroupMemberConnecting(u, groupInfo, hostMember, member): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)")
case let .memberRole(u, groupInfo, byMember, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)")
case let .memberRoleUser(u, groupInfo, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)")
case let .deletedMemberUser(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
case let .deletedMember(u, groupInfo, byMember, deletedMember): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\ndeletedMember: \(deletedMember)")
case let .leftMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
case let .groupDeleted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
case let .contactsMerged(u, intoContact, mergedContact): return withUser(u, "intoContact: \(intoContact)\nmergedContact: \(mergedContact)")
case let .groupInvitation(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .userJoinedGroup(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .joinedGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
case let .connectedToGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
case let .groupRemoved(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
case let .groupLinkCreated(u, groupInfo, connReqContact): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)")
case let .groupLink(u, groupInfo, connReqContact): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)")
case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
case .rcvFileAcceptedSndCancelled: return noDetails
case let .rcvFileStart(chatItem): return String(describing: chatItem)
case let .rcvFileComplete(chatItem): return String(describing: chatItem)
case let .sndFileStart(chatItem, _): return String(describing: chatItem)
case let .sndFileComplete(chatItem, _): return String(describing: chatItem)
case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem))
case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem))
case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileCancelled(chatItem, _): return String(describing: chatItem)
case let .sndFileRcvCancelled(chatItem, _): return String(describing: chatItem)
case let .sndGroupFileCancelled(chatItem, _, _): return String(describing: chatItem)
case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndGroupFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
case let .callInvitation(inv): return String(describing: inv)
case let .callOffer(contact, callType, offer, sharedKey, askConfirmation): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))"
case let .callAnswer(contact, answer): return "contact: \(contact.id)\nanswer: \(String(describing: answer))"
case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))"
case let .callEnded(contact): return "contact: \(contact.id)"
case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))")
case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")
case let .callExtraInfo(u, contact, extraInfo): return withUser(u, "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))")
case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)")
case let .callInvitations(invs): return String(describing: invs)
case let .ntfTokenStatus(status): return String(describing: status)
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
case let .ntfMessages(connEntity, msgTs, ntfMessages): return "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))"
case let .newContactConnection(connection): return String(describing: connection)
case let .contactConnectionDeleted(connection): return String(describing: connection)
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
case let .newContactConnection(u, connection): return withUser(u, String(describing: connection))
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
case .cmdOk: return noDetails
case let .chatCmdError(chatError): return String(describing: chatError)
case let .chatError(chatError): return String(describing: chatError)
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
}
}
}
private var noDetails: String { get { "\(responseType): no details" } }
private func withUser(_ u: User?, _ s: String) -> String {
if let id = u?.userId {
return "userId: \(id)\n\(s)"
}
return s
}
}
public enum ChatPagination {

View File

@@ -38,6 +38,7 @@ public struct WebRTCExtraInfo: Codable {
}
public struct RcvCallInvitation: Decodable {
public var user: User
public var contact: Contact
public var callkitUUID: UUID? = UUID()
public var callType: CallType
@@ -53,6 +54,7 @@ public struct RcvCallInvitation: Decodable {
}
public static let sampleData = RcvCallInvitation(
user: User.sampleData,
contact: Contact.sampleData,
callType: CallType(media: .audio, capabilities: CallCapabilities(encryption: false)),
callTs: .now

View File

@@ -21,7 +21,7 @@ public let appNotificationId = "chat.simplex.app.notification"
let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification")
public func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
public func createContactRequestNtf(_ user: User, _ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
return createNotification(
categoryIdentifier: ntfCategoryContactRequest,
@@ -34,11 +34,11 @@ public func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNM
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName
),
targetContentIdentifier: nil,
userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId]
userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId]
)
}
public func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent {
public func createContactConnectedNtf(_ user: User, _ contact: Contact) -> UNMutableNotificationContent {
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
return createNotification(
categoryIdentifier: ntfCategoryContactConnected,
@@ -50,12 +50,13 @@ public func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificati
NSLocalizedString("You can now send messages to %@", comment: "notification body"),
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName
),
targetContentIdentifier: contact.id
targetContentIdentifier: contact.id,
userInfo: ["userId": user.userId]
// userInfo: ["chatId": contact.id, "contactId": contact.apiId]
)
}
public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
public func createMessageReceivedNtf(_ user: User, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
let previewMode = ntfPreviewModeGroupDefault.get()
var title: String
if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir {
@@ -67,7 +68,8 @@ public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UN
categoryIdentifier: ntfCategoryMessageReceived,
title: title,
body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"),
targetContentIdentifier: cInfo.id
targetContentIdentifier: cInfo.id,
userInfo: ["userId": user.userId]
// userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id]
)
}
@@ -82,11 +84,11 @@ public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutabl
title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):",
body: text,
targetContentIdentifier: nil,
userInfo: ["chatId": invitation.contact.id]
userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId]
)
}
public func createConnectionEventNtf(_ connEntity: ConnectionEntity) -> UNMutableNotificationContent {
public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity) -> UNMutableNotificationContent {
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
var title: String
var body: String? = nil
@@ -115,7 +117,8 @@ public func createConnectionEventNtf(_ connEntity: ConnectionEntity) -> UNMutabl
categoryIdentifier: ntfCategoryConnectionEvent,
title: title,
body: body,
targetContentIdentifier: targetContentIdentifier
targetContentIdentifier: targetContentIdentifier,
userInfo: ["userId": user.userId]
)
}