mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +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() }
|
willSet { publisher.send() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// set listener here that will be notified on every add/delete of a chat item
|
|
||||||
let chatState = ActiveChatState()
|
let chatState = ActiveChatState()
|
||||||
var chatItemsChangesListener: RecalculatePositions = RecalculatePositions()
|
|
||||||
|
|
||||||
// Publishes directly to `objectWillChange` publisher,
|
// Publishes directly to `objectWillChange` publisher,
|
||||||
// this will cause reversedChatItems to be rendered without throttling
|
// this will cause reversedChatItems to be rendered without throttling
|
||||||
|
@ -573,7 +571,7 @@ final class ChatModel: ObservableObject {
|
||||||
ci.meta.itemStatus = status
|
ci.meta.itemStatus = status
|
||||||
}
|
}
|
||||||
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
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
|
im.itemAdded = true
|
||||||
ChatItemDummyModel.shared.sendUpdate()
|
ChatItemDummyModel.shared.sendUpdate()
|
||||||
return true
|
return true
|
||||||
|
@ -621,7 +619,7 @@ final class ChatModel: ObservableObject {
|
||||||
if let i = getChatItemIndex(cItem) {
|
if let i = getChatItemIndex(cItem) {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
let item = im.reversedChatItems.remove(at: i)
|
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)
|
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
||||||
withAnimation {
|
withAnimation {
|
||||||
im.reversedChatItems.insert(cItem, at: 0)
|
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
|
im.itemAdded = true
|
||||||
}
|
}
|
||||||
return cItem
|
return cItem
|
||||||
|
@ -743,7 +741,7 @@ final class ChatModel: ObservableObject {
|
||||||
markChatItemRead_(i)
|
markChatItemRead_(i)
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
im.chatItemsChangesListener.read(nil, im.reversedChatItems.reversed())
|
im.chatState.itemsRead(nil, im.reversedChatItems.reversed())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
|
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
|
||||||
|
@ -767,7 +765,7 @@ final class ChatModel: ObservableObject {
|
||||||
if chatId == cInfo.id {
|
if chatId == cInfo.id {
|
||||||
chatItemStatuses = [:]
|
chatItemStatuses = [:]
|
||||||
im.reversedChatItems = []
|
im.reversedChatItems = []
|
||||||
im.chatItemsChangesListener.cleared()
|
im.chatState.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -785,7 +783,7 @@ final class ChatModel: ObservableObject {
|
||||||
}
|
}
|
||||||
i += 1
|
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)
|
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 = [:]
|
m.chatItemStatuses = [:]
|
||||||
if clearItems {
|
if clearItems {
|
||||||
im.reversedChatItems = []
|
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 })
|
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
|
unreadAfter = 0
|
||||||
unreadAfterNewestLoaded = 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 {
|
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()
|
// visible items mapped to their underlying data structure which is ItemsModel.shared.reversedChatItems.reversed()
|
||||||
return range
|
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 {
|
if chatModel.chatId == nil {
|
||||||
chatModel.chatItemStatuses = [:]
|
chatModel.chatItemStatuses = [:]
|
||||||
ItemsModel.shared.reversedChatItems = []
|
ItemsModel.shared.reversedChatItems = []
|
||||||
ItemsModel.shared.chatItemsChangesListener.cleared()
|
ItemsModel.shared.chatState.clear()
|
||||||
chatModel.groupMembers = []
|
chatModel.groupMembers = []
|
||||||
chatModel.groupMembersIndexes.removeAll()
|
chatModel.groupMembersIndexes.removeAll()
|
||||||
chatModel.membersLoaded = false
|
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
|
// Clear sensitive data on screen just in case app fails to hide its views while new database is created
|
||||||
m.chatId = nil
|
m.chatId = nil
|
||||||
ItemsModel.shared.reversedChatItems = []
|
ItemsModel.shared.reversedChatItems = []
|
||||||
ItemsModel.shared.chatItemsChangesListener.cleared()
|
ItemsModel.shared.chatState.clear()
|
||||||
m.updateChats([])
|
m.updateChats([])
|
||||||
m.users = []
|
m.users = []
|
||||||
_ = kcAppPassword.set(password)
|
_ = kcAppPassword.set(password)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue