mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-03-14 09:45:42 +00:00
ios: track unread chat lists, avoid scanning when adding and removing chats (#5398)
* ios: track unread chat lists, avoid scanning when adding and removing chats * disable favorites filter when no more favorite chats
This commit is contained in:
parent
143be1edaf
commit
9adff0bfd1
4 changed files with 139 additions and 58 deletions
|
@ -105,27 +105,87 @@ class ChatTagsModel: ObservableObject {
|
|||
|
||||
@Published var userTags: [ChatTag] = []
|
||||
@Published var activeFilter: ActiveFilter? = nil
|
||||
@Published var presetTags: [PresetTag] = []
|
||||
}
|
||||
|
||||
func updatePresetTags(_ chats: [Chat]) {
|
||||
var matches: Set<PresetTag> = []
|
||||
for chat in chats {
|
||||
for tag in PresetTag.allCases {
|
||||
if presetTagMatchesChat(tag, chat) {
|
||||
matches.insert(tag)
|
||||
@Published var presetTags: [PresetTag:Int] = [:]
|
||||
@Published var unreadTags: [Int64:Int] = [:]
|
||||
|
||||
func updateChatTags(_ chats: [Chat]) {
|
||||
let tm = ChatTagsModel.shared
|
||||
var newPresetTags: [PresetTag:Int] = [:]
|
||||
var newUnreadTags: [Int64:Int] = [:]
|
||||
for chat in chats {
|
||||
for tag in PresetTag.allCases {
|
||||
if presetTagMatchesChat(tag, chat.chatInfo) {
|
||||
newPresetTags[tag] = (newPresetTags[tag] ?? 0) + 1
|
||||
}
|
||||
}
|
||||
if chat.isUnread, let tags = chat.chatInfo.chatTags {
|
||||
for tag in tags {
|
||||
newUnreadTags[tag] = (newUnreadTags[tag] ?? 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches.count == PresetTag.allCases.count {
|
||||
break
|
||||
if case let .presetTag(tag) = tm.activeFilter, (newPresetTags[tag] ?? 0) == 0 {
|
||||
activeFilter = nil
|
||||
}
|
||||
presetTags = newPresetTags
|
||||
unreadTags = newUnreadTags
|
||||
}
|
||||
|
||||
func updateChatFavorite(favorite: Bool, wasFavorite: Bool) {
|
||||
let count = presetTags[.favorites]
|
||||
if favorite && !wasFavorite {
|
||||
presetTags[.favorites] = (count ?? 0) + 1
|
||||
} else if !favorite && wasFavorite, let count {
|
||||
presetTags[.favorites] = max(0, count - 1)
|
||||
if case .presetTag(.favorites) = activeFilter, (presetTags[.favorites] ?? 0) == 0 {
|
||||
activeFilter = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addPresetChatTags(_ chatInfo: ChatInfo) {
|
||||
for tag in PresetTag.allCases {
|
||||
if presetTagMatchesChat(tag, chatInfo) {
|
||||
presetTags[tag] = (presetTags[tag] ?? 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removePresetChatTags(_ chatInfo: ChatInfo) {
|
||||
for tag in PresetTag.allCases {
|
||||
if presetTagMatchesChat(tag, chatInfo) {
|
||||
if let count = presetTags[tag] {
|
||||
presetTags[tag] = max(0, count - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tm = ChatTagsModel.shared
|
||||
if case let .presetTag(tag) = tm.activeFilter, !matches.contains(tag) {
|
||||
tm.activeFilter = nil
|
||||
func markChatTagRead(_ chat: Chat) -> Void {
|
||||
if chat.isUnread, let tags = chat.chatInfo.chatTags {
|
||||
markChatTagRead_(chat, tags)
|
||||
}
|
||||
}
|
||||
|
||||
func updateChatTagRead(_ chat: Chat, wasUnread: Bool) -> Void {
|
||||
guard let tags = chat.chatInfo.chatTags else { return }
|
||||
let nowUnread = chat.isUnread
|
||||
if nowUnread && !wasUnread {
|
||||
for tag in tags {
|
||||
unreadTags[tag] = (unreadTags[tag] ?? 0) + 1
|
||||
}
|
||||
} else if !nowUnread && wasUnread {
|
||||
markChatTagRead_(chat, tags)
|
||||
}
|
||||
}
|
||||
|
||||
private func markChatTagRead_(_ chat: Chat, _ tags: [Int64]) -> Void {
|
||||
for tag in tags {
|
||||
if let count = unreadTags[tag] {
|
||||
unreadTags[tag] = max(0, count - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
tm.presetTags = Array(matches).sorted(by: { $0.rawValue < $1.rawValue })
|
||||
}
|
||||
|
||||
class NetworkModel: ObservableObject {
|
||||
|
@ -370,10 +430,9 @@ final class ChatModel: ObservableObject {
|
|||
private func updateChat(_ cInfo: ChatInfo, addMissing: Bool = true) {
|
||||
if hasChat(cInfo.id) {
|
||||
updateChatInfo(cInfo)
|
||||
updatePresetTags(self.chats)
|
||||
} else if addMissing {
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: []))
|
||||
updatePresetTags(self.chats)
|
||||
ChatTagsModel.shared.addPresetChatTags(cInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -596,6 +655,7 @@ final class ChatModel: ObservableObject {
|
|||
_updateChat(cInfo.id) { chat in
|
||||
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
|
||||
self.updateFloatingButtons(unreadCount: 0)
|
||||
ChatTagsModel.shared.markChatTagRead(chat)
|
||||
chat.chatStats = ChatStats()
|
||||
}
|
||||
// update current chat
|
||||
|
@ -634,7 +694,9 @@ final class ChatModel: ObservableObject {
|
|||
// update preview
|
||||
let markedCount = chat.chatStats.unreadCount - unreadBelow
|
||||
if markedCount > 0 {
|
||||
let wasUnread = chat.isUnread
|
||||
chat.chatStats.unreadCount -= markedCount
|
||||
ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: wasUnread)
|
||||
self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount)
|
||||
self.updateFloatingButtons(unreadCount: chat.chatStats.unreadCount)
|
||||
}
|
||||
|
@ -647,7 +709,9 @@ final class ChatModel: ObservableObject {
|
|||
|
||||
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
|
||||
_updateChat(cInfo.id) { chat in
|
||||
let wasUnread = chat.isUnread
|
||||
chat.chatStats.unreadChat = unreadChat
|
||||
ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: wasUnread)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -656,6 +720,7 @@ final class ChatModel: ObservableObject {
|
|||
if let chat = getChat(cInfo.id) {
|
||||
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
|
||||
chat.chatItems = []
|
||||
ChatTagsModel.shared.markChatTagRead(chat)
|
||||
chat.chatStats = ChatStats()
|
||||
chat.chatInfo = cInfo
|
||||
}
|
||||
|
@ -782,7 +847,9 @@ final class ChatModel: ObservableObject {
|
|||
}
|
||||
|
||||
func changeUnreadCounter(_ chatIndex: Int, by count: Int) {
|
||||
let wasUnread = chats[chatIndex].isUnread
|
||||
chats[chatIndex].chatStats.unreadCount = chats[chatIndex].chatStats.unreadCount + count
|
||||
ChatTagsModel.shared.updateChatTagRead(chats[chatIndex], wasUnread: wasUnread)
|
||||
changeUnreadCounter(user: currentUser!, by: count)
|
||||
}
|
||||
|
||||
|
@ -887,8 +954,10 @@ final class ChatModel: ObservableObject {
|
|||
|
||||
func removeChat(_ id: String) {
|
||||
withAnimation {
|
||||
chats.removeAll(where: { $0.id == id })
|
||||
updatePresetTags(chats)
|
||||
if let i = getChatIndex(id) {
|
||||
let removed = chats.remove(at: i)
|
||||
ChatTagsModel.shared.removePresetChatTags(removed.chatInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -986,6 +1055,10 @@ final class Chat: ObservableObject, Identifiable, ChatLike {
|
|||
}
|
||||
}
|
||||
|
||||
var isUnread: Bool {
|
||||
chatStats.unreadCount > 0 || chatStats.unreadChat
|
||||
}
|
||||
|
||||
var id: ChatId { get { chatInfo.id } }
|
||||
|
||||
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
||||
|
|
|
@ -1793,7 +1793,7 @@ func getUserChatData() throws {
|
|||
let tm = ChatTagsModel.shared
|
||||
tm.activeFilter = nil
|
||||
tm.userTags = tags
|
||||
updatePresetTags(m.chats)
|
||||
tm.updateChatTags(m.chats)
|
||||
}
|
||||
|
||||
private func getUserChatDataAsync() async throws {
|
||||
|
@ -1810,7 +1810,7 @@ private func getUserChatDataAsync() async throws {
|
|||
m.updateChats(chats)
|
||||
tm.activeFilter = nil
|
||||
tm.userTags = tags
|
||||
updatePresetTags(m.chats)
|
||||
tm.updateChatTags(m.chats)
|
||||
}
|
||||
} else {
|
||||
await MainActor.run {
|
||||
|
@ -1818,7 +1818,7 @@ private func getUserChatDataAsync() async throws {
|
|||
m.updateChats([])
|
||||
tm.activeFilter = nil
|
||||
tm.userTags = []
|
||||
tm.presetTags = []
|
||||
tm.presetTags = [:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2007,6 +2007,8 @@ func updateChatSettings(_ chat: Chat, chatSettings: ChatSettings) {
|
|||
do {
|
||||
try await apiSetChatSettings(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, chatSettings: chatSettings)
|
||||
await MainActor.run {
|
||||
let wasFavorite = chat.chatInfo.chatSettings?.favorite ?? false
|
||||
ChatTagsModel.shared.updateChatFavorite(favorite: chatSettings.favorite, wasFavorite: wasFavorite)
|
||||
switch chat.chatInfo {
|
||||
case var .direct(contact):
|
||||
contact.chatSettings = chatSettings
|
||||
|
|
|
@ -472,7 +472,7 @@ struct ChatListView: View {
|
|||
|
||||
func filtered(_ chat: Chat) -> Bool {
|
||||
switch chatTagsModel.activeFilter {
|
||||
case let .presetTag(tag): presetTagMatchesChat(tag, chat)
|
||||
case let .presetTag(tag): presetTagMatchesChat(tag, chat.chatInfo)
|
||||
case let .userTag(tag): chat.chatInfo.chatTags?.contains(tag.chatTagId) == true
|
||||
case .unread: chat.chatStats.unreadChat || chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0
|
||||
case .none: true
|
||||
|
@ -665,6 +665,7 @@ struct ChatListSearchBar: View {
|
|||
struct ChatTagsView: View {
|
||||
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var parentSheet: SomeSheet<AnyView>?
|
||||
|
||||
var body: some View {
|
||||
|
@ -681,13 +682,13 @@ struct ChatTagsView: View {
|
|||
collapsedTagsFilterView()
|
||||
}
|
||||
}
|
||||
let selectedTag: ChatTag? = if case let .userTag(tag) = chatTagsModel.activeFilter {
|
||||
tag
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
ForEach(chatTagsModel.userTags, id: \.id) { tag in
|
||||
let current = if case let .userTag(t) = chatTagsModel.activeFilter {
|
||||
t == tag
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
let current = tag == selectedTag
|
||||
let color: Color = current ? .accentColor : .secondary
|
||||
ZStack {
|
||||
HStack(spacing: 4) {
|
||||
|
@ -698,8 +699,9 @@ struct ChatTagsView: View {
|
|||
.foregroundColor(color)
|
||||
}
|
||||
ZStack {
|
||||
Text(tag.chatTagText).fontWeight(.semibold).foregroundColor(.clear)
|
||||
Text(tag.chatTagText).fontWeight(current ? .semibold : .regular).foregroundColor(color)
|
||||
let badge = Text(verbatim: (chatTagsModel.unreadTags[tag.chatTagId] ?? 0) > 0 ? " ●" : "").font(.footnote)
|
||||
(Text(tag.chatTagText).fontWeight(.semibold) + badge).foregroundColor(.clear)
|
||||
Text(tag.chatTagText).fontWeight(current ? .semibold : .regular).foregroundColor(color) + badge.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
|
@ -757,21 +759,23 @@ struct ChatTagsView: View {
|
|||
} else {
|
||||
nil
|
||||
}
|
||||
ForEach(chatTagsModel.presetTags, id: \.id) { tag in
|
||||
let active = tag == selectedPresetTag
|
||||
let (icon, text) = presetTagLabel(tag: tag, active: active)
|
||||
let color: Color = active ? .accentColor : .secondary
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(color)
|
||||
ZStack {
|
||||
Text(text).fontWeight(.semibold).foregroundColor(.clear)
|
||||
Text(text).fontWeight(active ? .semibold : .regular).foregroundColor(color)
|
||||
ForEach(PresetTag.allCases, id: \.id) { tag in
|
||||
if (chatTagsModel.presetTags[tag] ?? 0) > 0 {
|
||||
let active = tag == selectedPresetTag
|
||||
let (icon, text) = presetTagLabel(tag: tag, active: active)
|
||||
let color: Color = active ? .accentColor : .secondary
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(color)
|
||||
ZStack {
|
||||
Text(text).fontWeight(.semibold).foregroundColor(.clear)
|
||||
Text(text).fontWeight(active ? .semibold : .regular).foregroundColor(color)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
setActiveFilter(filter: .presetTag(tag))
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
setActiveFilter(filter: .presetTag(tag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -793,14 +797,16 @@ struct ChatTagsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
ForEach(chatTagsModel.presetTags, id: \.id) { tag in
|
||||
Button {
|
||||
setActiveFilter(filter: .presetTag(tag))
|
||||
} label: {
|
||||
let (systemName, text) = presetTagLabel(tag: tag, active: tag == selectedPresetTag)
|
||||
HStack {
|
||||
Image(systemName: systemName)
|
||||
Text(text)
|
||||
ForEach(PresetTag.allCases, id: \.id) { tag in
|
||||
if (chatTagsModel.presetTags[tag] ?? 0) > 0 {
|
||||
Button {
|
||||
setActiveFilter(filter: .presetTag(tag))
|
||||
} label: {
|
||||
let (systemName, text) = presetTagLabel(tag: tag, active: tag == selectedPresetTag)
|
||||
HStack {
|
||||
Image(systemName: systemName)
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -846,12 +852,12 @@ func chatStoppedIcon() -> some View {
|
|||
}
|
||||
}
|
||||
|
||||
func presetTagMatchesChat(_ tag: PresetTag, _ chat: Chat) -> Bool {
|
||||
func presetTagMatchesChat(_ tag: PresetTag, _ chatInfo: ChatInfo) -> Bool {
|
||||
switch tag {
|
||||
case .favorites:
|
||||
chat.chatInfo.chatSettings?.favorite == true
|
||||
chatInfo.chatSettings?.favorite == true
|
||||
case .contacts:
|
||||
switch chat.chatInfo {
|
||||
switch chatInfo {
|
||||
case let .direct(contact): !(contact.activeConn == nil && contact.profile.contactLink != nil && contact.active) && !contact.chatDeleted
|
||||
case .contactRequest: true
|
||||
case .contactConnection: true
|
||||
|
@ -859,12 +865,12 @@ func presetTagMatchesChat(_ tag: PresetTag, _ chat: Chat) -> Bool {
|
|||
default: false
|
||||
}
|
||||
case .groups:
|
||||
switch chat.chatInfo {
|
||||
switch chatInfo {
|
||||
case let .group(groupInfo): groupInfo.businessChat == nil
|
||||
default: false
|
||||
}
|
||||
case .business:
|
||||
chat.chatInfo.groupInfo?.businessChat?.chatType == .business
|
||||
chatInfo.groupInfo?.businessChat?.chatType == .business
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue