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:
Evgeny 2024-12-20 11:43:11 +00:00 committed by GitHub
parent 143be1edaf
commit 9adff0bfd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 139 additions and 58 deletions

View file

@ -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)" } }

View file

@ -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 = [:]
}
}
}

View file

@ -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

View file

@ -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
}
}