mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 12:19:54 +00:00
ios: refactor chat state (remove chatItemsChangesListener) (#5858)
This commit is contained in:
parent
ca49167ec6
commit
38b8e0cee6
5 changed files with 104 additions and 109 deletions
|
@ -54,9 +54,7 @@ class ItemsModel: ObservableObject {
|
|||
willSet { publisher.send() }
|
||||
}
|
||||
|
||||
// set listener here that will be notified on every add/delete of a chat item
|
||||
let chatState = ActiveChatState()
|
||||
var chatItemsChangesListener: RecalculatePositions = RecalculatePositions()
|
||||
|
||||
// Publishes directly to `objectWillChange` publisher,
|
||||
// this will cause reversedChatItems to be rendered without throttling
|
||||
|
@ -573,7 +571,7 @@ final class ChatModel: ObservableObject {
|
|||
ci.meta.itemStatus = status
|
||||
}
|
||||
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
||||
im.chatItemsChangesListener.added((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0)
|
||||
im.chatState.itemAdded((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0)
|
||||
im.itemAdded = true
|
||||
ChatItemDummyModel.shared.sendUpdate()
|
||||
return true
|
||||
|
@ -621,7 +619,7 @@ final class ChatModel: ObservableObject {
|
|||
if let i = getChatItemIndex(cItem) {
|
||||
withAnimation {
|
||||
let item = im.reversedChatItems.remove(at: i)
|
||||
im.chatItemsChangesListener.removed([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
|
||||
im.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -709,7 +707,7 @@ final class ChatModel: ObservableObject {
|
|||
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
||||
withAnimation {
|
||||
im.reversedChatItems.insert(cItem, at: 0)
|
||||
im.chatItemsChangesListener.added((cItem.id, cItem.isRcvNew), 0)
|
||||
im.chatState.itemAdded((cItem.id, cItem.isRcvNew), 0)
|
||||
im.itemAdded = true
|
||||
}
|
||||
return cItem
|
||||
|
@ -743,7 +741,7 @@ final class ChatModel: ObservableObject {
|
|||
markChatItemRead_(i)
|
||||
i += 1
|
||||
}
|
||||
im.chatItemsChangesListener.read(nil, im.reversedChatItems.reversed())
|
||||
im.chatState.itemsRead(nil, im.reversedChatItems.reversed())
|
||||
}
|
||||
}
|
||||
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
|
||||
|
@ -767,7 +765,7 @@ final class ChatModel: ObservableObject {
|
|||
if chatId == cInfo.id {
|
||||
chatItemStatuses = [:]
|
||||
im.reversedChatItems = []
|
||||
im.chatItemsChangesListener.cleared()
|
||||
im.chatState.clear()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -785,7 +783,7 @@ final class ChatModel: ObservableObject {
|
|||
}
|
||||
i += 1
|
||||
}
|
||||
im.chatItemsChangesListener.read(unreadItemIds, im.reversedChatItems.reversed())
|
||||
im.chatState.itemsRead(unreadItemIds, im.reversedChatItems.reversed())
|
||||
}
|
||||
self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count, unreadMentions: -mentionsRead)
|
||||
}
|
||||
|
|
|
@ -345,7 +345,7 @@ func loadChat(chatId: ChatId, search: String = "", openAroundItemId: ChatItem.ID
|
|||
m.chatItemStatuses = [:]
|
||||
if clearItems {
|
||||
im.reversedChatItems = []
|
||||
ItemsModel.shared.chatItemsChangesListener.cleared()
|
||||
ItemsModel.shared.chatState.clear()
|
||||
}
|
||||
}
|
||||
await apiLoadMessages(chatId, openAroundItemId != nil ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) : (search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)), im.chatState, search, openAroundItemId, { 0...0 })
|
||||
|
|
|
@ -321,6 +321,101 @@ class ActiveChatState {
|
|||
unreadAfter = 0
|
||||
unreadAfterNewestLoaded = 0
|
||||
}
|
||||
|
||||
func itemsRead(_ itemIds: Set<Int64>?, _ newItems: [ChatItem]) {
|
||||
guard let itemIds else {
|
||||
// special case when the whole chat became read
|
||||
unreadTotal = 0
|
||||
unreadAfter = 0
|
||||
return
|
||||
}
|
||||
var unreadAfterItemIndex: Int = -1
|
||||
// since it's more often that the newest items become read, it's logical to loop from the end of the list to finish it faster
|
||||
var i = newItems.count - 1
|
||||
var ids = itemIds
|
||||
// intermediate variables to prevent re-setting state value a lot of times without reason
|
||||
var newUnreadTotal = unreadTotal
|
||||
var newUnreadAfter = unreadAfter
|
||||
while i >= 0 {
|
||||
let item = newItems[i]
|
||||
if item.id == unreadAfterItemId {
|
||||
unreadAfterItemIndex = i
|
||||
}
|
||||
if ids.contains(item.id) {
|
||||
// was unread, now this item is read
|
||||
if (unreadAfterItemIndex == -1) {
|
||||
newUnreadAfter -= 1
|
||||
}
|
||||
newUnreadTotal -= 1
|
||||
ids.remove(item.id)
|
||||
if ids.isEmpty {
|
||||
break
|
||||
}
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
unreadTotal = newUnreadTotal
|
||||
unreadAfter = newUnreadAfter
|
||||
}
|
||||
|
||||
func itemAdded(_ item: (Int64, Bool), _ index: Int) {
|
||||
if item.1 {
|
||||
unreadAfter += 1
|
||||
unreadTotal += 1
|
||||
}
|
||||
}
|
||||
|
||||
func itemsRemoved(_ itemIds: [(Int64, Int, Bool)], _ newItems: [ChatItem]) {
|
||||
var newSplits: [Int64] = []
|
||||
for split in splits {
|
||||
let index = itemIds.firstIndex(where: { (delId, _, _) in delId == split })
|
||||
// deleted the item that was right before the split between items, find newer item so it will act like the split
|
||||
if let index {
|
||||
let idx = itemIds[index].1 - itemIds.filter { (_, delIndex, _) in delIndex <= index }.count
|
||||
let newSplit = newItems.count > idx && idx >= 0 ? newItems[idx].id : nil
|
||||
// it the whole section is gone and splits overlap, don't add it at all
|
||||
if let newSplit, !newSplits.contains(newSplit) {
|
||||
newSplits.append(newSplit)
|
||||
}
|
||||
} else {
|
||||
newSplits.append(split)
|
||||
}
|
||||
}
|
||||
splits = newSplits
|
||||
|
||||
let index = itemIds.firstIndex(where: { (delId, _, _) in delId == unreadAfterItemId })
|
||||
// unread after item was removed
|
||||
if let index {
|
||||
let idx = itemIds[index].1 - itemIds.filter { (_, delIndex, _) in delIndex <= index }.count
|
||||
var newUnreadAfterItemId = newItems.count > idx && idx >= 0 ? newItems[idx].id : nil
|
||||
let newUnreadAfterItemWasNull = newUnreadAfterItemId == nil
|
||||
if newUnreadAfterItemId == nil {
|
||||
// everything on top (including unread after item) were deleted, take top item as unread after id
|
||||
newUnreadAfterItemId = newItems.first?.id
|
||||
}
|
||||
if let newUnreadAfterItemId {
|
||||
unreadAfterItemId = newUnreadAfterItemId
|
||||
totalAfter -= itemIds.filter { (_, delIndex, _) in delIndex > index }.count
|
||||
unreadTotal -= itemIds.filter { (_, delIndex, isRcvNew) in delIndex <= index && isRcvNew }.count
|
||||
unreadAfter -= itemIds.filter { (_, delIndex, isRcvNew) in delIndex > index && isRcvNew }.count
|
||||
if newUnreadAfterItemWasNull {
|
||||
// since the unread after item was moved one item after initial position, adjust counters accordingly
|
||||
if newItems.first?.isRcvNew == true {
|
||||
unreadTotal += 1
|
||||
unreadAfter -= 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// all items were deleted, 0 items in chatItems
|
||||
unreadAfterItemId = -1
|
||||
totalAfter = 0
|
||||
unreadTotal = 0
|
||||
unreadAfter = 0
|
||||
}
|
||||
} else {
|
||||
totalAfter -= itemIds.count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BoxedValue<T: Hashable>: Equatable, Hashable {
|
||||
|
@ -359,101 +454,3 @@ func visibleItemIndexesNonReversed(_ listState: EndlessScrollView<MergedItem>.Li
|
|||
// visible items mapped to their underlying data structure which is ItemsModel.shared.reversedChatItems.reversed()
|
||||
return range
|
||||
}
|
||||
|
||||
class RecalculatePositions {
|
||||
private var chatState: ActiveChatState { get { ItemsModel.shared.chatState } }
|
||||
|
||||
func read(_ itemIds: Set<Int64>?, _ newItems: [ChatItem]) {
|
||||
guard let itemIds else {
|
||||
// special case when the whole chat became read
|
||||
chatState.unreadTotal = 0
|
||||
chatState.unreadAfter = 0
|
||||
return
|
||||
}
|
||||
var unreadAfterItemIndex: Int = -1
|
||||
// since it's more often that the newest items become read, it's logical to loop from the end of the list to finish it faster
|
||||
var i = newItems.count - 1
|
||||
var ids = itemIds
|
||||
// intermediate variables to prevent re-setting state value a lot of times without reason
|
||||
var newUnreadTotal = chatState.unreadTotal
|
||||
var newUnreadAfter = chatState.unreadAfter
|
||||
while i >= 0 {
|
||||
let item = newItems[i]
|
||||
if item.id == chatState.unreadAfterItemId {
|
||||
unreadAfterItemIndex = i
|
||||
}
|
||||
if ids.contains(item.id) {
|
||||
// was unread, now this item is read
|
||||
if (unreadAfterItemIndex == -1) {
|
||||
newUnreadAfter -= 1
|
||||
}
|
||||
newUnreadTotal -= 1
|
||||
ids.remove(item.id)
|
||||
if ids.isEmpty {
|
||||
break
|
||||
}
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
chatState.unreadTotal = newUnreadTotal
|
||||
chatState.unreadAfter = newUnreadAfter
|
||||
}
|
||||
func added(_ item: (Int64, Bool), _ index: Int) {
|
||||
if item.1 {
|
||||
chatState.unreadAfter += 1
|
||||
chatState.unreadTotal += 1
|
||||
}
|
||||
}
|
||||
func removed(_ itemIds: [(Int64, Int, Bool)], _ newItems: [ChatItem]) {
|
||||
var newSplits: [Int64] = []
|
||||
for split in chatState.splits {
|
||||
let index = itemIds.firstIndex(where: { (delId, _, _) in delId == split })
|
||||
// deleted the item that was right before the split between items, find newer item so it will act like the split
|
||||
if let index {
|
||||
let idx = itemIds[index].1 - itemIds.filter { (_, delIndex, _) in delIndex <= index }.count
|
||||
let newSplit = newItems.count > idx && idx >= 0 ? newItems[idx].id : nil
|
||||
// it the whole section is gone and splits overlap, don't add it at all
|
||||
if let newSplit, !newSplits.contains(newSplit) {
|
||||
newSplits.append(newSplit)
|
||||
}
|
||||
} else {
|
||||
newSplits.append(split)
|
||||
}
|
||||
}
|
||||
chatState.splits = newSplits
|
||||
|
||||
let index = itemIds.firstIndex(where: { (delId, _, _) in delId == chatState.unreadAfterItemId })
|
||||
// unread after item was removed
|
||||
if let index {
|
||||
let idx = itemIds[index].1 - itemIds.filter { (_, delIndex, _) in delIndex <= index }.count
|
||||
var newUnreadAfterItemId = newItems.count > idx && idx >= 0 ? newItems[idx].id : nil
|
||||
let newUnreadAfterItemWasNull = newUnreadAfterItemId == nil
|
||||
if newUnreadAfterItemId == nil {
|
||||
// everything on top (including unread after item) were deleted, take top item as unread after id
|
||||
newUnreadAfterItemId = newItems.first?.id
|
||||
}
|
||||
if let newUnreadAfterItemId {
|
||||
chatState.unreadAfterItemId = newUnreadAfterItemId
|
||||
chatState.totalAfter -= itemIds.filter { (_, delIndex, _) in delIndex > index }.count
|
||||
chatState.unreadTotal -= itemIds.filter { (_, delIndex, isRcvNew) in delIndex <= index && isRcvNew }.count
|
||||
chatState.unreadAfter -= itemIds.filter { (_, delIndex, isRcvNew) in delIndex > index && isRcvNew }.count
|
||||
if newUnreadAfterItemWasNull {
|
||||
// since the unread after item was moved one item after initial position, adjust counters accordingly
|
||||
if newItems.first?.isRcvNew == true {
|
||||
chatState.unreadTotal += 1
|
||||
chatState.unreadAfter -= 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// all items were deleted, 0 items in chatItems
|
||||
chatState.unreadAfterItemId = -1
|
||||
chatState.totalAfter = 0
|
||||
chatState.unreadTotal = 0
|
||||
chatState.unreadAfter = 0
|
||||
}
|
||||
} else {
|
||||
chatState.totalAfter -= itemIds.count
|
||||
}
|
||||
}
|
||||
func cleared() { chatState.clear() }
|
||||
}
|
||||
|
|
|
@ -281,7 +281,7 @@ struct ChatView: View {
|
|||
if chatModel.chatId == nil {
|
||||
chatModel.chatItemStatuses = [:]
|
||||
ItemsModel.shared.reversedChatItems = []
|
||||
ItemsModel.shared.chatItemsChangesListener.cleared()
|
||||
ItemsModel.shared.chatState.clear()
|
||||
chatModel.groupMembers = []
|
||||
chatModel.groupMembersIndexes.removeAll()
|
||||
chatModel.membersLoaded = false
|
||||
|
|
|
@ -65,7 +65,7 @@ struct LocalAuthView: View {
|
|||
// Clear sensitive data on screen just in case app fails to hide its views while new database is created
|
||||
m.chatId = nil
|
||||
ItemsModel.shared.reversedChatItems = []
|
||||
ItemsModel.shared.chatItemsChangesListener.cleared()
|
||||
ItemsModel.shared.chatState.clear()
|
||||
m.updateChats([])
|
||||
m.users = []
|
||||
_ = kcAppPassword.set(password)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue