ios: Multiuser calls (#1800)

* ios: Multiuser calls

* counter update on badge

* padding before profile info in call view

* underline in name

* change after merge

* do not show Simplex Info button if users already created

* unread counter

* do not increase badge counter when chat has disabled notifications

* update incoming call

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko
2023-01-23 13:20:58 +00:00
committed by GitHub
parent bcc80be8e9
commit 4cd396a0d2
12 changed files with 66 additions and 68 deletions

View File

@@ -158,7 +158,7 @@ final class ChatModel: ObservableObject {
addChat(Chat(c), at: i)
}
}
NtfManager.shared.setNtfBadgeCount(totalUnreadCount())
NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers())
}
// func addGroup(_ group: SimpleXChat.Group) {
@@ -172,7 +172,6 @@ final class ChatModel: ObservableObject {
if case .rcvNew = cItem.meta.itemStatus {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
increaseUnreadCounter(user: currentUser!)
NtfManager.shared.incNtfBadgeCount()
}
if i > 0 {
if chatId == nil {
@@ -253,9 +252,6 @@ final class ChatModel: ObservableObject {
// remove from current chat
if chatId == cInfo.id {
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
if reversedChatItems[i].isRcvNew {
NtfManager.shared.decNtfBadgeCount()
}
_ = withAnimation {
self.reversedChatItems.remove(at: i)
}
@@ -304,7 +300,7 @@ final class ChatModel: ObservableObject {
func markChatItemsRead(_ cInfo: ChatInfo) {
// update preview
_updateChat(cInfo.id) { chat in
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
chat.chatStats = ChatStats()
}
// update current chat
@@ -337,7 +333,6 @@ final class ChatModel: ObservableObject {
// update preview
let markedCount = chat.chatStats.unreadCount - unreadBelow
if markedCount > 0 {
NtfManager.shared.decNtfBadgeCount(by: markedCount)
chat.chatStats.unreadCount -= markedCount
self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount)
}
@@ -357,7 +352,7 @@ final class ChatModel: ObservableObject {
func clearChat(_ cInfo: ChatInfo) {
// clear preview
if let chat = getChat(cInfo.id) {
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
chat.chatItems = []
chat.chatStats = ChatStats()
chat.chatInfo = cInfo
@@ -397,20 +392,23 @@ final class ChatModel: ObservableObject {
func increaseUnreadCounter(user: User) {
changeUnreadCounter(user: user, by: 1)
NtfManager.shared.incNtfBadgeCount()
}
func decreaseUnreadCounter(user: User, by: Int = 1) {
changeUnreadCounter(user: user, by: -by)
NtfManager.shared.decNtfBadgeCount(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)
users[i].unreadCount += by
}
}
func totalUnreadCount() -> Int {
chats.reduce(0, { count, chat in count + chat.chatStats.unreadCount })
func totalUnreadCountForAllUsers() -> Int {
chats.filter { $0.chatInfo.ntfsEnabled }.reduce(0, { count, chat in count + chat.chatStats.unreadCount }) +
users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount })
}
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {

View File

@@ -39,10 +39,10 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
if let userId = content.userInfo["userId"] as? Int64,
userId != chatModel.currentUser?.userId {
changeActiveUser(userId)
changeActiveUser(userId)
}
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
let chatId = content.userInfo["chatId"] as? String {
let chatId = content.userInfo["chatId"] as? String {
if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
Task { await acceptContactRequest(contactRequest) }
} else {

View File

@@ -928,7 +928,7 @@ func startChat() throws {
m.users = try listUsers()
if justStarted {
try getUserChatData()
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCount())
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
try refreshCallInvitations()
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
if let token = m.deviceToken {
@@ -1077,7 +1077,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
case let .newChatItem(user, aChatItem):
if !active(user) {
if case .rcvNew = aChatItem.chatItem.meta.itemStatus {
if case .rcvNew = aChatItem.chatItem.meta.itemStatus, aChatItem.chatInfo.ntfsEnabled {
m.increaseUnreadCounter(user: user)
}
return
@@ -1125,7 +1125,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
if !active(user) {
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew {
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
m.decreaseUnreadCounter(user: user)
}
return

View File

@@ -60,7 +60,7 @@ struct SimpleXApp: App {
enteredBackground = ProcessInfo.processInfo.systemUptime
}
doAuthenticate = false
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCount())
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
case .active:
if chatModel.chatRunning == true {
ChatReceiver.shared.start()

View File

@@ -36,7 +36,6 @@ 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

@@ -29,6 +29,10 @@ struct IncomingCallView: View {
private func incomingCall(_ invitation: RcvCallInvitation) -> some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
if m.users.count > 1 {
ProfileImage(imageStr: invitation.user.image, color: .white)
.frame(width: 24, height: 24)
}
Image(systemName: invitation.callType.media == .video ? "video.fill" : "phone.fill").foregroundColor(.green)
Text(invitation.callTypeText)
}
@@ -82,6 +86,8 @@ struct IncomingCallView: View {
struct IncomingCallView_Previews: PreviewProvider {
static var previews: some View {
CallController.shared.activeCallInvitation = RcvCallInvitation.sampleData
return IncomingCallView()
let m = ChatModel()
m.users = [UserInfo.sampleData, UserInfo.sampleData]
return IncomingCallView().environmentObject(m)
}
}

View File

@@ -233,7 +233,6 @@ struct ChatView: View {
if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) {
Task {
await apiMarkChatItemRead(cInfo, ci)
NtfManager.shared.decNtfBadgeCount()
}
}
}

View File

@@ -132,8 +132,8 @@ struct UserPicker: View {
}
}
func unreadCounter(_ unread: Int64) -> some View {
unreadCountText(Int(truncatingIfNeeded: unread))
func unreadCounter(_ unread: Int) -> some View {
unreadCountText(unread)
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 4)

View File

@@ -51,13 +51,17 @@ struct CreateProfile: View {
Spacer()
HStack {
Button {
hideKeyboard()
withAnimation { m.onboardingStage = .step1_SimpleXInfo }
} label: {
HStack {
Image(systemName: "lessthan")
Text("About SimpleX")
if m.users.isEmpty {
Button {
hideKeyboard()
withAnimation {
m.onboardingStage = .step1_SimpleXInfo
}
} label: {
HStack {
Image(systemName: "lessthan")
Text("About SimpleX")
}
}
}

View File

@@ -107,18 +107,16 @@ class NotificationService: UNNotificationServiceExtension {
let encNtfInfo = ntfData["message"] as? String,
let dbStatus = startChat() {
if case .ok = dbStatus,
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()
}
let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
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()
}
}
}
@@ -220,6 +218,9 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotification
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
var cItem = aChatItem.chatItem
if !cInfo.ntfsEnabled {
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
}
if case .image = cItem.content.msgContent {
if let file = cItem.file,
file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV,
@@ -258,15 +259,6 @@ 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 response: \(String(describing: r))")
@@ -300,21 +292,20 @@ func apiSetIncognito(incognito: Bool) throws {
throw r
}
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> [NtfMessages]? {
let users = listUsers()
if users.isEmpty {
logger.debug("no users")
return []
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
guard apiGetActiveUser() != nil else {
logger.debug("no active user")
return nil
}
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)")
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfMessages(user, connEntity, msgTs, ntfMessages) = r, let user = user {
return NtfMessages(user: user, connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages)
} else if case let .chatCmdError(_, error) = r {
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
} else {
logger.debug("apiGetNtfMessage ignored response: \(r.responseType, privacy: .public) \(String.init(describing: r), privacy: .private)")
}
return result
return nil
}
func apiReceiveFile(fileId: Int64, inline: Bool) -> AChatItem? {

View File

@@ -37,7 +37,7 @@ public enum ChatCommand {
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
case apiDeleteToken(token: DeviceToken)
case apiGetNtfMessage(userId: Int64, nonce: String, encNtfInfo: String)
case apiGetNtfMessage(nonce: String, encNtfInfo: String)
case apiNewGroup(userId: Int64, groupProfile: GroupProfile)
case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole)
case apiJoinGroup(groupId: Int64)
@@ -125,7 +125,7 @@ public enum ChatCommand {
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)"
case let .apiGetNtfMessage(userId, nonce, encNtfInfo): return "/_ntf message \(userId) \(nonce) \(encNtfInfo)"
case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)"
case let .apiNewGroup(userId, groupProfile): return "/_group \(userId) \(encodeJSON(groupProfile))"
case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)"
case let .apiJoinGroup(groupId): return "/_join #\(groupId)"
@@ -409,7 +409,7 @@ public enum ChatResponse: Decodable, Error {
case callInvitations(callInvitations: [RcvCallInvitation])
case ntfTokenStatus(status: NtfTknStatus)
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
case ntfMessages(user: User, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
case newContactConnection(user: User, connection: PendingContactConnection)
case contactConnectionDeleted(user: User, connection: PendingContactConnection)
case versionInfo(versionInfo: CoreVersionInfo)
@@ -1078,6 +1078,7 @@ public enum ChatError: Decodable {
public enum ChatErrorType: Decodable {
case noActiveUser
case activeUserExists
case differentActiveUser
case chatNotStarted
case invalidConnReq
case invalidChatMessage(message: String)

View File

@@ -36,9 +36,9 @@ public struct User: Decodable, NamedChat, Identifiable {
public struct UserInfo: Decodable, Identifiable {
public var user: User
public var unreadCount: Int64
public var unreadCount: Int
public init(user: User, unreadCount: Int64) {
public init(user: User, unreadCount: Int) {
self.user = user
self.unreadCount = unreadCount
}