From 59e1f109089ef9fb704ba058c66b07ccd6507cba Mon Sep 17 00:00:00 2001 From: Peter Iakovlev Date: Tue, 6 Feb 2018 22:31:32 +0400 Subject: [PATCH] no message --- Postbox.xcodeproj/project.pbxproj | 30 + Postbox/Crc32.h | 8 + Postbox/Crc32.m | 7 + Postbox/FileSize.swift | 10 + Postbox/GroupFeedIndexTable.swift | 24 + Postbox/GroupFeedStateTable.swift | 35 +- Postbox/ManagedFile.swift | 13 + Postbox/MediaBox.swift | 449 ++++++++----- Postbox/MediaBoxFile.swift | 822 ++++++++++++++++++++++++ Postbox/Message.swift | 14 +- Postbox/MessageHistoryView.swift | 7 + Postbox/PeerGroupStateView.swift | 35 + Postbox/Postbox.swift | 56 +- Postbox/PostboxPrivate/module.modulemap | 1 + Postbox/PostboxTransaction.swift | 7 +- Postbox/Views.swift | 11 + 16 files changed, 1330 insertions(+), 199 deletions(-) create mode 100644 Postbox/Crc32.h create mode 100644 Postbox/Crc32.m create mode 100644 Postbox/FileSize.swift create mode 100644 Postbox/MediaBoxFile.swift create mode 100644 Postbox/PeerGroupStateView.swift diff --git a/Postbox.xcodeproj/project.pbxproj b/Postbox.xcodeproj/project.pbxproj index 5919ee2cc6..f458412d93 100644 --- a/Postbox.xcodeproj/project.pbxproj +++ b/Postbox.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ D021E0D61DB4FCFC00C6B04F /* ItemCollectionInfoTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0D51DB4FCFC00C6B04F /* ItemCollectionInfoTable.swift */; }; D021E0D81DB4FD1300C6B04F /* ItemCollectionItemTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0D71DB4FD1300C6B04F /* ItemCollectionItemTable.swift */; }; D021E0DC1DB5237C00C6B04F /* ItemCollectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0DB1DB5237C00C6B04F /* ItemCollectionsView.swift */; }; + D021FC262024B83700C34AB7 /* FileSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021FC252024B83700C34AB7 /* FileSize.swift */; }; + D021FC272024B83700C34AB7 /* FileSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021FC252024B83700C34AB7 /* FileSize.swift */; }; D02EB8071D2B07F300D07ED3 /* OrderStatisticTreeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02EB8061D2B07F300D07ED3 /* OrderStatisticTreeTests.swift */; }; D03120F81DA53FF4006A2A60 /* PeerPresenceTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03120F71DA53FF4006A2A60 /* PeerPresenceTable.swift */; }; D03120FA1DA540F0006A2A60 /* CachedPeerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03120F91DA540F0006A2A60 /* CachedPeerData.swift */; }; @@ -343,6 +345,14 @@ D0FA0ACB1E780A26005BB9B7 /* PostboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA0AC91E780A26005BB9B7 /* PostboxView.swift */; }; D0FA0ACD1E781067005BB9B7 /* Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA0ACC1E781067005BB9B7 /* Views.swift */; }; D0FA0ACE1E781067005BB9B7 /* Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA0ACC1E781067005BB9B7 /* Views.swift */; }; + D0FC194A201E8EAF00FEDBB2 /* MediaBoxFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC1949201E8EAF00FEDBB2 /* MediaBoxFile.swift */; }; + D0FC194B201E8EAF00FEDBB2 /* MediaBoxFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC1949201E8EAF00FEDBB2 /* MediaBoxFile.swift */; }; + D0FC195020208E8800FEDBB2 /* Crc32.h in Headers */ = {isa = PBXBuildFile; fileRef = D0FC194E20208E8800FEDBB2 /* Crc32.h */; }; + D0FC195120208E8800FEDBB2 /* Crc32.h in Headers */ = {isa = PBXBuildFile; fileRef = D0FC194E20208E8800FEDBB2 /* Crc32.h */; }; + D0FC195220208E8800FEDBB2 /* Crc32.m in Sources */ = {isa = PBXBuildFile; fileRef = D0FC194F20208E8800FEDBB2 /* Crc32.m */; }; + D0FC195320208E8800FEDBB2 /* Crc32.m in Sources */ = {isa = PBXBuildFile; fileRef = D0FC194F20208E8800FEDBB2 /* Crc32.m */; }; + D0FC19552020CB7700FEDBB2 /* PeerGroupStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC19542020CB7700FEDBB2 /* PeerGroupStateView.swift */; }; + D0FC19562020CB7700FEDBB2 /* PeerGroupStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC19542020CB7700FEDBB2 /* PeerGroupStateView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -375,6 +385,7 @@ D021E0D51DB4FCFC00C6B04F /* ItemCollectionInfoTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollectionInfoTable.swift; sourceTree = ""; }; D021E0D71DB4FD1300C6B04F /* ItemCollectionItemTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollectionItemTable.swift; sourceTree = ""; }; D021E0DB1DB5237C00C6B04F /* ItemCollectionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollectionsView.swift; sourceTree = ""; }; + D021FC252024B83700C34AB7 /* FileSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSize.swift; sourceTree = ""; }; D02EB8061D2B07F300D07ED3 /* OrderStatisticTreeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderStatisticTreeTests.swift; sourceTree = ""; }; D03120F71DA53FF4006A2A60 /* PeerPresenceTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerPresenceTable.swift; sourceTree = ""; }; D03120F91DA540F0006A2A60 /* CachedPeerData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedPeerData.swift; sourceTree = ""; }; @@ -538,6 +549,10 @@ D0FA0AC61E77F0A2005BB9B7 /* ItemCollectionInfosView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollectionInfosView.swift; sourceTree = ""; }; D0FA0AC91E780A26005BB9B7 /* PostboxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxView.swift; sourceTree = ""; }; D0FA0ACC1E781067005BB9B7 /* Views.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Views.swift; sourceTree = ""; }; + D0FC1949201E8EAF00FEDBB2 /* MediaBoxFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaBoxFile.swift; sourceTree = ""; }; + D0FC194E20208E8800FEDBB2 /* Crc32.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Crc32.h; sourceTree = ""; }; + D0FC194F20208E8800FEDBB2 /* Crc32.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Crc32.m; sourceTree = ""; }; + D0FC19542020CB7700FEDBB2 /* PeerGroupStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerGroupStateView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -575,6 +590,7 @@ D05F09A51C9E9F9300BB6F96 /* MediaResourceStatus.swift */, D0CE63F51CA1CCB2002BC462 /* MediaResource.swift */, D09ADF091D2E89F300C8208D /* RandomAccessMediaResourceContext.swift */, + D0FC1949201E8EAF00FEDBB2 /* MediaBoxFile.swift */, ); name = "Media Box"; sourceTree = ""; @@ -754,6 +770,7 @@ D0B1671F1F9EAAA900976B40 /* OrderedList.swift */, D0AA55121FB4C6AB00C2AB58 /* BinarySearch.swift */, D0ECCB8E1FE9EB5500609802 /* PostboxLogging.swift */, + D021FC252024B83700C34AB7 /* FileSize.swift */, ); name = Utils; sourceTree = ""; @@ -812,6 +829,7 @@ D0C26D771FE31CA4004ABF18 /* GroupFeedReadStateSyncOperationsView.swift */, D0C26D801FE41323004ABF18 /* ChatListGroupReferenceUnreadCounters.swift */, D04614322004F2CC00EC0EF2 /* LocalMessageTagsView.swift */, + D0FC19542020CB7700FEDBB2 /* PeerGroupStateView.swift */, ); name = Views; sourceTree = ""; @@ -865,6 +883,8 @@ D0E3A74D1B28A7E300A402D9 /* Supporting Files */ = { isa = PBXGroup; children = ( + D0FC194E20208E8800FEDBB2 /* Crc32.h */, + D0FC194F20208E8800FEDBB2 /* Crc32.m */, D044E1611B2AD667001EE087 /* MurMurHash32.h */, D044E1621B2AD677001EE087 /* MurMurHash32.m */, D0E3A74F1B28A7E300A402D9 /* Postbox.h */, @@ -909,6 +929,7 @@ files = ( D0B418201D7DFDFD004562A4 /* sqlite3ext.h in Headers */, D0B4185D1D7DFE35004562A4 /* IpcNotifier.h in Headers */, + D0FC195120208E8800FEDBB2 /* Crc32.h in Headers */, D050F2651E4A5B4800988324 /* fts3_tokenizer.h in Headers */, D0B4185B1D7DFE2C004562A4 /* MurMurHash32.h in Headers */, D0B4181E1D7DFDF8004562A4 /* SQLite-Bridging.h in Headers */, @@ -923,6 +944,7 @@ files = ( D07516771B2EC90400AE42E0 /* fts3_tokenizer.h in Headers */, D07516451B2D9CEF00AE42E0 /* sqlite3.h in Headers */, + D0FC195020208E8800FEDBB2 /* Crc32.h in Headers */, D0D511041D64D91C00A97B8A /* IpcNotifier.h in Headers */, D07516781B2EC90400AE42E0 /* SQLite-Bridging.h in Headers */, D0E3A7501B28A7E300A402D9 /* Postbox.h in Headers */, @@ -1064,6 +1086,7 @@ C20EB2A31F7179DC00DD3A57 /* PeerNotificationSettingsView.swift in Sources */, C25B56FE1F431C3300581D02 /* MessageHistoryTagsSummaryTable.swift in Sources */, D050F2661E4A5B5A00988324 /* MessageGloballyUniqueIdTable.swift in Sources */, + D0FC195320208E8800FEDBB2 /* Crc32.m in Sources */, D03229EF1E6B33FD0000AF9C /* SqliteInterface.swift in Sources */, D0AA55141FB4C6AB00C2AB58 /* BinarySearch.swift in Sources */, D01C7F081EFC1ED3008305F1 /* UnorderedItemListTable.swift in Sources */, @@ -1106,6 +1129,7 @@ D0B418591D7DFE29004562A4 /* PostboxTransaction.swift in Sources */, D0F7B1D21E045C6A007EB8A5 /* PeerNotificationSettingsTable.swift in Sources */, D0C26D701FE2E737004ABF18 /* GroupFeedReadState.swift in Sources */, + D0FC19562020CB7700FEDBB2 /* PeerGroupStateView.swift in Sources */, D0E23DE31E808A9400B9B6D2 /* ItemCollectionIdsView.swift in Sources */, D0B4181D1D7DFDF4004562A4 /* sqlite3.c in Sources */, D0F7B1C91E045C6A007EB8A5 /* MessageHistoryTagsTable.swift in Sources */, @@ -1135,9 +1159,11 @@ D0FA0ACE1E781067005BB9B7 /* Views.swift in Sources */, D0B418501D7DFE20004562A4 /* UnsentMessageIndicesView.swift in Sources */, D0BEAF641E54B2FA00BD963D /* AccountManager.swift in Sources */, + D021FC272024B83700C34AB7 /* FileSize.swift in Sources */, D073CE7F1DCBF3B4007511FD /* ItemCollection.swift in Sources */, D07047B21F3DE40400F6A8D4 /* PendingMessageActionsSummaryView.swift in Sources */, D0F7B1D91E045C6A007EB8A5 /* OrderStatisticTable.swift in Sources */, + D0FC194B201E8EAF00FEDBB2 /* MediaBoxFile.swift in Sources */, D073CEA01DCBF3C1007511FD /* ItemCollectionsView.swift in Sources */, D0BE383A1E7C1FD4000079AF /* ItemCollectionInfoView.swift in Sources */, D0B418491D7DFE20004562A4 /* ChatListView.swift in Sources */, @@ -1226,6 +1252,7 @@ D0CE8CF61F703B1E00AA2DB0 /* PendingPeerNotificationSettingsIndexTable.swift in Sources */, D03229EE1E6B33FD0000AF9C /* SqliteInterface.swift in Sources */, D01C7F071EFC1ED3008305F1 /* UnorderedItemListTable.swift in Sources */, + D021FC262024B83700C34AB7 /* FileSize.swift in Sources */, D0AA55131FB4C6AB00C2AB58 /* BinarySearch.swift in Sources */, D0F9E8631C579F0200037222 /* MediaCleanupTable.swift in Sources */, D0943AF81FDAC53F001522CC /* ChatLocation.swift in Sources */, @@ -1335,10 +1362,12 @@ D07827C11E0079CB00071108 /* StringIndexTokens.swift in Sources */, D0F3CC721DDE1CDC008148FA /* ItemCacheTable.swift in Sources */, D08C713C1C51283C00779C0F /* MessageHistoryIndexTable.swift in Sources */, + D0FC195220208E8800FEDBB2 /* Crc32.m in Sources */, D019B1CF1E2E770700F80DB3 /* MessageGloballyUniqueIdTable.swift in Sources */, D0F9E86D1C5A0E5D00037222 /* MetadataTable.swift in Sources */, D0DA44481E4C7D1E005FDCA7 /* PostboxAccess.swift in Sources */, D0B167201F9EAAA900976B40 /* OrderedList.swift in Sources */, + D0FC19552020CB7700FEDBB2 /* PeerGroupStateView.swift in Sources */, D0C26D721FE2E7A8004ABF18 /* GroupFeedReadStateTable.swift in Sources */, D0943AF11FD99DCD001522CC /* GroupChatListInclusion.swift in Sources */, D0E3A7841B28AE0900A402D9 /* Peer.swift in Sources */, @@ -1359,6 +1388,7 @@ D04614302004E24600EC0EF2 /* LocalMessageHistoryTagsTable.swift in Sources */, D00EED1E1C81F28D00341DFF /* MessageHistoryTagsTable.swift in Sources */, D044CA2C1C617E2D002160FF /* MessageHistoryMetadataTable.swift in Sources */, + D0FC194A201E8EAF00FEDBB2 /* MediaBoxFile.swift in Sources */, D03120FC1DA55427006A2A60 /* PeerNotificationSettings.swift in Sources */, D0943B021FDB01D8001522CC /* PostboxUpgrade_14to15.swift in Sources */, D0F7AB321DCFAB18009AD9A1 /* PeerChatTopIndexableMessageIds.swift in Sources */, diff --git a/Postbox/Crc32.h b/Postbox/Crc32.h new file mode 100644 index 0000000000..c0c4a42317 --- /dev/null +++ b/Postbox/Crc32.h @@ -0,0 +1,8 @@ +#ifndef Postbox_Crc32_h +#define Postbox_Crc32_h + +#import + +uint32_t Crc32(const void *bytes, int length); + +#endif diff --git a/Postbox/Crc32.m b/Postbox/Crc32.m new file mode 100644 index 0000000000..7dfded40f5 --- /dev/null +++ b/Postbox/Crc32.m @@ -0,0 +1,7 @@ +#import "Crc32.h" + +#import + +uint32_t Crc32(const void *bytes, int length) { + return (uint32_t)crc32(0, bytes, (uInt)length); +} diff --git a/Postbox/FileSize.swift b/Postbox/FileSize.swift new file mode 100644 index 0000000000..20510f7ab7 --- /dev/null +++ b/Postbox/FileSize.swift @@ -0,0 +1,10 @@ +import Foundation + +func fileSize(_ path: String) -> Int? { + var value = stat() + if stat(path, &value) == 0 { + return Int(value.st_size) + } else { + return nil + } +} diff --git a/Postbox/GroupFeedIndexTable.swift b/Postbox/GroupFeedIndexTable.swift index fe6f9f2f7f..01ce98d519 100644 --- a/Postbox/GroupFeedIndexTable.swift +++ b/Postbox/GroupFeedIndexTable.swift @@ -50,6 +50,9 @@ private func writeEntry(_ entry: GroupFeedIndexEntry, to buffer: WriteBuffer) { var stableIdValue: UInt32 = stableId var timestampValue: Int32 = hole.lowerIndex.timestamp + if timestampValue == 0 { + //print("writing 0 hole") + } var idPeerIdValue: Int64 = hole.lowerIndex.id.peerId.toInt64() var idNamespaceValue: Int32 = hole.lowerIndex.id.namespace var idIdValue: Int32 = hole.lowerIndex.id.id @@ -293,6 +296,8 @@ final class GroupFeedIndexTable: Table { var filledUpperBound: MessageIndex? var filledLowerBound: MessageIndex? + //self.debugPrintEntries(groupId: groupId) + var adjustedMainHoleIndex: MessageIndex? do { var upperItem: GroupFeedIndexEntry? @@ -454,9 +459,13 @@ final class GroupFeedIndexTable: Table { self.fillHole(insertMessage: insertMessage, groupId: groupId, index: holeIndex, fillType: currentFillType, messages: holeMessages, addOperation: addOperation) } + //self.debugPrintEntries(groupId: groupId) + for message in remainingMessages { insertMessage(message) } + + //self.debugPrintEntries(groupId: groupId) } private func fillHole(insertMessage: (InternalStoreMessage) -> Void, groupId: PeerGroupId, index: MessageIndex, fillType: HoleFill, messages: [InternalStoreMessage], addOperation: (PeerGroupId, GroupFeedIndexOperation) -> Void) { @@ -714,4 +723,19 @@ final class GroupFeedIndexTable: Table { }, limit: 1) return result } + + private func debugPrintEntries(groupId: PeerGroupId) { + print("-----------------------------") + self.valueBox.range(self.table, start: self.lowerBound(groupId: groupId), end: self.upperBound(groupId: groupId), values: { key, value in + let entry = readEntry(groupId: groupId, key: key, value: value) + switch entry { + case let .message(index): + print("message timestamp: \(index.timestamp), peerId: \(index.id.peerId.id), id: \(index.id.id)") + case let .hole(_, hole): + print("hole upper timestamp: \(hole.upperIndex.timestamp), \(hole.upperIndex.id.peerId.id), \(hole.upperIndex.id.id), lower \(hole.lowerIndex.timestamp), \(hole.lowerIndex.id.peerId.id), \(hole.lowerIndex.id.id)") + } + return true + }, limit: 0) + print("-----------------------------") + } } diff --git a/Postbox/GroupFeedStateTable.swift b/Postbox/GroupFeedStateTable.swift index ca4c2dd9b2..07d095f883 100644 --- a/Postbox/GroupFeedStateTable.swift +++ b/Postbox/GroupFeedStateTable.swift @@ -1,23 +1,23 @@ import Foundation -public final class GroupFeedState { - +public protocol PeerGroupState: PostboxCoding { + func equals(_ other: PeerGroupState) -> Bool } -private struct GroupFeedStateEntry { - let state: GroupFeedState? +private struct PeerGroupStateEntry { + let state: PeerGroupState? - init(_ state: GroupFeedState?) { + init(_ state: PeerGroupState?) { self.state = state } } -final class GroupFeedStateTable: Table { +final class PeerGroupStateTable: Table { static func tableSpec(_ id: Int32) -> ValueBoxTable { return ValueBoxTable(id: id, keyType: .int64) } - private var cachedStates: [PeerGroupId: GroupFeedStateEntry] = [:] + private var cachedStates: [PeerGroupId: PeerGroupStateEntry] = [:] private var updatedGroupIds = Set() private let sharedKey = ValueBoxKey(length: 8) @@ -27,23 +27,22 @@ final class GroupFeedStateTable: Table { return self.sharedKey } - func get(_ id: PeerGroupId) -> GroupFeedState? { + func get(_ id: PeerGroupId) -> PeerGroupState? { if let state = self.cachedStates[id] { return state.state } else { - /*if let value = self.valueBox.get(self.table, key: self.key(id)), let state = PostboxDecoder(buffer: value).decodeRootObject() { - self.cachedPeerChatStates[id] = state + if let value = self.valueBox.get(self.table, key: self.key(id)), let state = PostboxDecoder(buffer: value).decodeRootObject() as? PeerGroupState { + self.cachedStates[id] = PeerGroupStateEntry(state) return state } else { - self.cachedPeerChatStates[id] = nil + self.cachedStates[id] = PeerGroupStateEntry(nil) return nil - }*/ - return nil + } } } - func set(_ id: PeerGroupId, state: GroupFeedState?) { - self.cachedStates[id] = GroupFeedStateEntry(state) + func set(_ id: PeerGroupId, state: PeerGroupState?) { + self.cachedStates[id] = PeerGroupStateEntry(state) self.updatedGroupIds.insert(id) } @@ -55,10 +54,11 @@ final class GroupFeedStateTable: Table { override func beforeCommit() { if !self.updatedGroupIds.isEmpty { for id in self.updatedGroupIds { + let sharedEncoder = PostboxEncoder() if let entry = self.cachedStates[id], let state = entry.state { - /*sharedEncoder.reset() + sharedEncoder.reset() sharedEncoder.encodeRootObject(state) - self.valueBox.set(self.table, key: self.key(id), value: sharedEncoder.readBufferNoCopy())*/ + self.valueBox.set(self.table, key: self.key(id), value: sharedEncoder.readBufferNoCopy()) } else { self.valueBox.remove(self.table, key: self.key(id)) } @@ -68,3 +68,4 @@ final class GroupFeedStateTable: Table { } } + diff --git a/Postbox/ManagedFile.swift b/Postbox/ManagedFile.swift index 61243b496a..0b56f3abbd 100644 --- a/Postbox/ManagedFile.swift +++ b/Postbox/ManagedFile.swift @@ -70,4 +70,17 @@ public final class ManagedFile { public func truncate(count: Int64) { ftruncate(self.fd, count) } + + public func getSize() -> Int? { + var value = stat() + if fstat(self.fd, &value) == 0 { + return Int(value.st_size) + } else { + return nil + } + } + + public func sync() { + fsync(self.fd) + } } diff --git a/Postbox/MediaBox.swift b/Postbox/MediaBox.swift index 4d77896c6b..1336c556c0 100644 --- a/Postbox/MediaBox.swift +++ b/Postbox/MediaBox.swift @@ -8,6 +8,11 @@ import Foundation private final class ResourceStatusContext { var status: MediaResourceStatus? let subscribers = Bag<(MediaResourceStatus) -> Void>() + let disposable: Disposable + + init(disposable: Disposable) { + self.disposable = disposable + } } private final class ResourceDataContext { @@ -24,15 +29,6 @@ private final class ResourceDataContext { } } -private func fileSize(_ path: String) -> Int? { - var value = stat() - if stat(path, &value) == 0 { - return Int(value.st_size) - } else { - return nil - } -} - public enum ResourceDataRangeMode { case complete case incremental @@ -51,32 +47,30 @@ private struct ResourceStorePaths { public struct MediaResourceData { public let path: String + public let offset: Int public let size: Int public let complete: Bool - public init(path: String, size: Int, complete: Bool) { + public init(path: String, offset: Int, size: Int, complete: Bool) { self.path = path + self.offset = offset self.size = size self.complete = complete } } -public enum MediaResourceDataFetchResult { - case dataPart(data: Data, range: Range, complete: Bool) - case replaceHeader(data: Data, range: Range) - case moveLocalFile(path: String) - case reset +public protocol MediaResourceDataFetchCopyLocalItem { + func copyTo(url: URL) -> Bool } -/*public struct MediaResourceDataFetchResult { - public let data: Data - public let complete: Bool - - public init(data: Data, complete: Bool) { - self.data = data - self.complete = complete - } -}*/ +public enum MediaResourceDataFetchResult { + case dataPart(resourceOffset: Int, data: Data, range: Range, complete: Bool) + case resourceSizeUpdated(Int) + case replaceHeader(data: Data, range: Range) + case moveLocalFile(path: String) + case copyLocalItem(MediaResourceDataFetchCopyLocalItem) + case reset +} public struct CachedMediaResourceRepresentationResult { public let temporaryPath: String @@ -119,12 +113,12 @@ public final class MediaBox { private let cacheQueue = Queue() private var statusContexts: [WrappedMediaResourceId: ResourceStatusContext] = [:] - private var dataContexts: [WrappedMediaResourceId: ResourceDataContext] = [:] - private var randomAccessContexts: [WrappedMediaResourceId: RandomAccessMediaResourceContext] = [:] private var cachedRepresentationContexts: [CachedMediaResourceRepresentationKey: CachedMediaResourceRepresentationContext] = [:] - private var wrappedFetchResource = Promise<(MediaResource, Range, MediaResourceFetchTag?) -> Signal>() - public var fetchResource: ((MediaResource, Range, MediaResourceFetchTag?) -> Signal)? { + private var fileContexts: [WrappedMediaResourceId: MediaBoxFileContext] = [:] + + private var wrappedFetchResource = Promise<(MediaResource, Signal, MediaResourceFetchTag?) -> Signal>() + public var fetchResource: ((MediaResource, Signal, MediaResourceFetchTag?) -> Signal)? { didSet { if let fetchResource = self.fetchResource { wrappedFetchResource.set(.single(fetchResource)) @@ -199,12 +193,16 @@ public final class MediaBox { subscriber.putCompletion() } else { self.statusQueue.async { + let resourceId = WrappedMediaResourceId(resource.id) let statusContext: ResourceStatusContext - if let current = self.statusContexts[WrappedMediaResourceId(resource.id)] { + var statusUpdateDisposable: MetaDisposable? + if let current = self.statusContexts[resourceId] { statusContext = current } else { - statusContext = ResourceStatusContext() - self.statusContexts[WrappedMediaResourceId(resource.id)] = statusContext + let statusUpdateDisposableValue = MetaDisposable() + statusContext = ResourceStatusContext(disposable: statusUpdateDisposableValue) + self.statusContexts[resourceId] = statusContext + statusUpdateDisposable = statusUpdateDisposableValue } let index = statusContext.subscribers.add({ status in @@ -213,40 +211,32 @@ public final class MediaBox { if let status = statusContext.status { subscriber.putNext(status) - } else { + } + + if let statusUpdateDisposable = statusUpdateDisposable { + let statusQueue = self.statusQueue self.dataQueue.async { - let status: MediaResourceStatus - - if let _ = fileSize(paths.complete) { - status = .Local - } else { - var fetchingData = false - if let dataContext = self.dataContexts[WrappedMediaResourceId(resource.id)] { - fetchingData = dataContext.fetchDisposable != nil - } - - if fetchingData { - let currentSize = fileSize(paths.partial) ?? 0 - - if let resourceSize = resource.size { - status = .Fetching(isActive: true, progress: Float(currentSize) / Float(resourceSize)) - } else { - status = .Fetching(isActive: true, progress: 0.0) + if let fileContext = self.fileContext(for: resource) { + statusUpdateDisposable.set(fileContext.status(next: { value in + statusQueue.async { + if let context = self.statusContexts[resourceId], context.status != value { + context.status = value + for subscriber in statusContext.subscribers.copyItems() { + subscriber(value) + } + } } - - } else { - status = .Remote - } - } - - self.statusQueue.async { - if let statusContext = self.statusContexts[WrappedMediaResourceId(resource.id)] , statusContext.status == nil { - statusContext.status = status - - for subscriber in statusContext.subscribers.copyItems() { - subscriber(status) + }, completed: { + statusQueue.async { + if let context = self.statusContexts[resourceId] { + context.subscribers.remove(index) + if context.subscribers.isEmpty { + self.statusContexts.removeValue(forKey: resourceId) + context.disposable.dispose() + } + } } - } + }, size: resource.size.flatMap(Int32.init))) } } } @@ -257,6 +247,7 @@ public final class MediaBox { current.subscribers.remove(index) if current.subscribers.isEmpty { self.statusContexts.removeValue(forKey: WrappedMediaResourceId(resource.id)) + current.disposable.dispose() } } } @@ -297,23 +288,53 @@ public final class MediaBox { if fileSize(symlinkPath) == nil { let _ = try? FileManager.default.createSymbolicLink(atPath: symlinkPath, withDestinationPath: URL(fileURLWithPath: paths.complete).lastPathComponent) } - subscriber.putNext(MediaResourceData(path: symlinkPath, size: completeSize, complete: true)) + subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: completeSize, complete: true)) subscriber.putCompletion() } else { - subscriber.putNext(MediaResourceData(path: paths.complete, size: completeSize, complete: true)) + subscriber.putNext(MediaResourceData(path: paths.complete, offset: 0, size: completeSize, complete: true)) subscriber.putCompletion() } } else { self.dataQueue.async { - let resourceId = WrappedMediaResourceId(resource.id) - let currentContext: ResourceDataContext? = self.dataContexts[resourceId] + if let fileContext = self.fileContext(for: resource) { + let waitUntilAfterInitialFetch: Bool + switch option { + case let .complete(waitUntilFetchStatus): + waitUntilAfterInitialFetch = waitUntilFetchStatus + case let .incremental(waitUntilFetchStatus): + waitUntilAfterInitialFetch = waitUntilFetchStatus + } + let dataDisposable = fileContext.data(range: 0 ..< Int32.max, waitUntilAfterInitialFetch: waitUntilAfterInitialFetch, next: { value in + self.dataQueue.async { + if value.complete { + if let pathExtension = pathExtension { + let symlinkPath = paths.complete + ".\(pathExtension)" + if fileSize(symlinkPath) == nil { + let _ = try? FileManager.default.createSymbolicLink(atPath: symlinkPath, withDestinationPath: URL(fileURLWithPath: paths.complete).lastPathComponent) + } + subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: value.size, complete: true)) + } else { + subscriber.putNext(value) + } + subscriber.putCompletion() + } else { + subscriber.putNext(value) + } + } + }) + disposable.set(ActionDisposable { + dataDisposable.dispose() + }) + } + + /*let currentContext: ResourceDataContext? = self.dataContexts[resourceId] if let currentContext = currentContext, currentContext.data.complete { if let pathExtension = pathExtension { let symlinkPath = paths.complete + ".\(pathExtension)" if fileSize(symlinkPath) == nil { let _ = try? FileManager.default.createSymbolicLink(atPath: symlinkPath, withDestinationPath: URL(fileURLWithPath: paths.complete).lastPathComponent) } - subscriber.putNext(MediaResourceData(path: symlinkPath, size: currentContext.data.size, complete: currentContext.data.complete)) + subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: currentContext.data.size, complete: currentContext.data.complete)) subscriber.putCompletion() } else { subscriber.putNext(currentContext.data) @@ -325,10 +346,10 @@ public final class MediaBox { if fileSize(symlinkPath) == nil { let _ = try? FileManager.default.createSymbolicLink(atPath: symlinkPath, withDestinationPath: URL(fileURLWithPath: paths.complete).lastPathComponent) } - subscriber.putNext(MediaResourceData(path: symlinkPath, size: completeSize, complete: true)) + subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: completeSize, complete: true)) subscriber.putCompletion() } else { - subscriber.putNext(MediaResourceData(path: paths.complete, size: completeSize, complete: true)) + subscriber.putNext(MediaResourceData(path: paths.complete, offset: 0, size: completeSize, complete: true)) subscriber.putCompletion() } } else { @@ -337,7 +358,7 @@ public final class MediaBox { dataContext = currentContext } else { let partialSize = fileSize(paths.partial) ?? 0 - dataContext = ResourceDataContext(data: MediaResourceData(path: paths.partial, size: partialSize, complete: false)) + dataContext = ResourceDataContext(data: MediaResourceData(path: paths.partial, offset: 0, size: partialSize, complete: false)) self.dataContexts[resourceId] = dataContext } @@ -350,7 +371,7 @@ public final class MediaBox { if fileSize(symlinkPath) == nil { let _ = try? FileManager.default.createSymbolicLink(atPath: symlinkPath, withDestinationPath: URL(fileURLWithPath: paths.complete).lastPathComponent) } - subscriber.putNext(MediaResourceData(path: symlinkPath, size: data.size, complete: data.complete)) + subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: data.size, complete: data.complete)) if data.complete { subscriber.putCompletion() } @@ -360,7 +381,7 @@ public final class MediaBox { } })) if !waitUntilFetchStatus || dataContext.processedFetch { - subscriber.putNext(MediaResourceData(path: dataContext.data.path, size: 0, complete: false)) + subscriber.putNext(MediaResourceData(path: dataContext.data.path, offset: 0, size: 0, complete: false)) } case let .incremental(waitUntilFetchStatus): index = dataContext.progresiveDataSubscribers.add((waitUntilFetchStatus, { data in @@ -369,7 +390,7 @@ public final class MediaBox { if fileSize(symlinkPath) == nil { let _ = try? FileManager.default.createSymbolicLink(atPath: symlinkPath, withDestinationPath: URL(fileURLWithPath: paths.complete).lastPathComponent) } - subscriber.putNext(MediaResourceData(path: symlinkPath, size: data.size, complete: data.complete)) + subscriber.putNext(MediaResourceData(path: symlinkPath, offset: 0, size: data.size, complete: data.complete)) subscriber.putCompletion() } else { subscriber.putNext(data) @@ -399,7 +420,7 @@ public final class MediaBox { } } }) - } + }*/ } } } @@ -408,72 +429,42 @@ public final class MediaBox { } } - private func randomAccessContext(for resource: MediaResource, size: Int, tag: MediaResourceFetchTag?) -> RandomAccessMediaResourceContext { + private func fileContext(for resource: MediaResource) -> MediaBoxFileContext? { assert(self.dataQueue.isCurrent()) let resourceId = WrappedMediaResourceId(resource.id) - let dataContext: RandomAccessMediaResourceContext - if let current = self.randomAccessContexts[resourceId] { - dataContext = current + if let current = self.fileContexts[resourceId] { + return current } else { - let path = self.pathForId(resource.id) + ".random" - dataContext = RandomAccessMediaResourceContext(path: path, size: size, fetchRange: { [weak self] range in - let disposable = MetaDisposable() - - if let strongSelf = self { - strongSelf.dataQueue.async { - let fetch = strongSelf.wrappedFetchResource.get() |> take(1) |> mapToSignal { fetch -> Signal in - return fetch(resource, range, tag) - } - var offset = 0 - disposable.set(fetch.start(next: { [weak strongSelf] result in - if let strongSelf = strongSelf { - strongSelf.dataQueue.async { - if let dataContext = strongSelf.randomAccessContexts[resourceId] { - switch result { - case let .dataPart(data, dataRange, _): - let storeRange = RandomAccessResourceStoreRange(offset: range.lowerBound + offset, data: data.subdata(in: dataRange)) - offset += data.count - dataContext.storeRanges([storeRange]) - default: - assertionFailure() - } - } - } - } - })) - } - } - - return disposable - }) - self.randomAccessContexts[resourceId] = dataContext + let paths = self.storePathsForId(resource.id) + if let fileContext = MediaBoxFileContext(queue: self.dataQueue, path: paths.complete, partialPath: paths.partial) { + self.fileContexts[resourceId] = fileContext + return fileContext + } else { + return nil + } } - return dataContext } - public func fetchedResourceData(_ resource: MediaResource, size: Int, in range: Range, tag: MediaResourceFetchTag?) -> Signal { + public func fetchedResourceData(_ resource: MediaResource, in range: Range, tag: MediaResourceFetchTag?) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.dataQueue.async { - let resourceId = WrappedMediaResourceId(resource.id) - let dataContext = self.randomAccessContext(for: resource, size: size, tag: tag) + let fileContext = self.fileContext(for: resource) - let listener = dataContext.addListenerForFetchedData(in: range) - - disposable.set(ActionDisposable { [weak self] in - if let strongSelf = self { - strongSelf.dataQueue.async { - if let dataContext = strongSelf.randomAccessContexts[resourceId] { - dataContext.removeListenerForFetchedData(listener) - if !dataContext.hasDataListeners() { - //let _ = strongSelf.randomAccessContexts.removeValue(forKey: resourceId) - } - } - } + let fetchResource = self.wrappedFetchResource.get() + let fetchedDisposable = fileContext?.fetched(range: Int32(range.lowerBound) ..< Int32(range.upperBound), fetch: { ranges in + return fetchResource |> mapToSignal { fetch in + return fetch(resource, ranges, tag) } + }, completed: { + subscriber.putCompletion() + }) + + disposable.set(ActionDisposable { + fetchedDisposable?.dispose() }) } @@ -481,55 +472,56 @@ public final class MediaBox { } } - public func resourceData(_ resource: MediaResource, size: Int, in range: Range, tag: MediaResourceFetchTag?, mode: ResourceDataRangeMode = .complete) -> Signal { + public func resourceData(_ resource: MediaResource, size: Int, in range: Range, mode: ResourceDataRangeMode = .complete) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.dataQueue.async { - let resourceId = WrappedMediaResourceId(resource.id) - let dataContext = self.randomAccessContext(for: resource, size: size, tag: tag) + let fileContext = self.fileContext(for: resource) - let listenerMode: RandomAccessResourceDataRangeMode - switch mode { - case .complete: - listenerMode = .Complete - case .incremental: - listenerMode = .Incremental - case .partial: - listenerMode = .Partial - } - - var offset = 0 - - let listener = dataContext.addListenerForData(in: range, mode: listenerMode, updated: { [weak self] data in - if let strongSelf = self { - strongSelf.dataQueue.async { - subscriber.putNext(data) - + let dataDisposable = fileContext?.data(range: Int32(range.lowerBound) ..< Int32(range.upperBound), waitUntilAfterInitialFetch: false, next: { result in + if let data = try? Data(contentsOf: URL(fileURLWithPath: result.path), options: .mappedRead) { + if result.complete { + let resultData = data.subdata(in: result.offset ..< (result.offset + result.size)) + subscriber.putNext(resultData) + subscriber.putCompletion() + } else { switch mode { - case .complete, .partial: - offset = max(offset, data.count) + case .complete: + break case .incremental: - offset += data.count - } - if offset == range.count { - subscriber.putCompletion() + break + case .partial: + break } } } }) - disposable.set(ActionDisposable { [weak self] in - if let strongSelf = self { - strongSelf.dataQueue.async { - if let dataContext = strongSelf.randomAccessContexts[resourceId] { - dataContext.removeListenerForData(listener) - if !dataContext.hasDataListeners() { - //let _ = strongSelf.randomAccessContexts.removeValue(forKey: resourceId) - } - } - } - } + disposable.set(ActionDisposable { + dataDisposable?.dispose() + }) + } + + return disposable + } + } + + public func resourceRangesStatus(_ resource: MediaResource) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + self.dataQueue.async { + let fileContext = self.fileContext(for: resource) + + let statusDisposable = fileContext?.rangeStatus(next: { result in + subscriber.putNext(result) + }, completed: { + subscriber.putCompletion() + }) + + disposable.set(ActionDisposable { + statusDisposable?.dispose() }) } @@ -542,7 +534,6 @@ public final class MediaBox { let disposable = MetaDisposable() self.dataQueue.async { - let resourceId = WrappedMediaResourceId(resource.id) let paths = self.storePathsForId(resource.id) if let _ = fileSize(paths.complete) { @@ -551,12 +542,27 @@ public final class MediaBox { } subscriber.putCompletion() } else { + if let fileContext = self.fileContext(for: resource) { + let fetchResource = self.wrappedFetchResource.get() + let fetchedDisposable = fileContext.fetchedFullRange(fetch: { ranges in + return fetchResource |> mapToSignal { fetch in + return fetch(resource, ranges, tag) + } + }, completed: { + if implNext { + subscriber.putNext(.remote) + } + subscriber.putCompletion() + }) + disposable.set(fetchedDisposable) + } + /* let currentSize = fileSize(paths.partial) ?? 0 let dataContext: ResourceDataContext if let current = self.dataContexts[resourceId] { dataContext = current } else { - dataContext = ResourceDataContext(data: MediaResourceData(path: paths.partial, size: currentSize, complete: false)) + dataContext = ResourceDataContext(data: MediaResourceData(path: paths.partial, offset: 0, size: currentSize, complete: false)) self.dataContexts[resourceId] = dataContext } @@ -582,7 +588,9 @@ public final class MediaBox { let file = Atomic(value: nil) let dataQueue = self.dataQueue dataContext.fetchDisposable = ((self.wrappedFetchResource.get() |> take(1) |> mapToSignal { fetch -> Signal in - return fetch(resource, currentSize ..< Int.max, tag) + var ranges = IndexSet() + ranges.insert(integersIn: currentSize ..< Int.max) + return fetch(resource, .single(ranges), tag) }) |> afterDisposed { dataQueue.async { let _ = file.modify { current in @@ -594,7 +602,9 @@ public final class MediaBox { let _ = self.ensureDirectoryCreated switch resultOption { - case let .dataPart(data, dataRange, complete): + case .resourceSizeUpdated: + break + case let .dataPart(_, data, dataRange, complete): var currentFile: ManagedFile? let _ = file.modify { current in if let current = current { @@ -624,9 +634,9 @@ public final class MediaBox { if complete { let linkResult = link(paths.partial, paths.complete) //assert(linkResult == 0) - updatedData = MediaResourceData(path: paths.complete, size: updatedSize, complete: true) + updatedData = MediaResourceData(path: paths.complete, offset: 0, size: updatedSize, complete: true) } else { - updatedData = MediaResourceData(path: paths.partial, size: updatedSize, complete: false) + updatedData = MediaResourceData(path: paths.partial, offset: 0, size: updatedSize, complete: false) } dataContext.data = updatedData @@ -716,7 +726,7 @@ public final class MediaBox { let updatedSize = offset let updatedData: MediaResourceData - updatedData = MediaResourceData(path: paths.partial, size: updatedSize, complete: false) + updatedData = MediaResourceData(path: paths.partial, offset: 0, size: updatedSize, complete: false) dataContext.data = updatedData @@ -779,7 +789,7 @@ public final class MediaBox { let updatedData: MediaResourceData let linkResult = link(paths.partial, paths.complete) assert(linkResult == 0) - updatedData = MediaResourceData(path: paths.complete, size: updatedSize, complete: true) + updatedData = MediaResourceData(path: paths.complete, offset: 0, size: updatedSize, complete: true) dataContext.data = updatedData @@ -857,7 +867,7 @@ public final class MediaBox { } } } - }) + })*/ } } @@ -867,7 +877,10 @@ public final class MediaBox { public func cancelInteractiveResourceFetch(_ resource: MediaResource) { self.dataQueue.async { - let resourceId = WrappedMediaResourceId(resource.id) + if let fileContext = self.fileContext(for: resource) { + fileContext.cancelFullRangeFetches() + } + /*let resourceId = WrappedMediaResourceId(resource.id) if let dataContext = self.dataContexts[resourceId], dataContext.fetchDisposable != nil { dataContext.fetchDisposable?.dispose() dataContext.fetchDisposable = nil @@ -887,7 +900,7 @@ public final class MediaBox { } } } - } + }*/ } } @@ -897,7 +910,7 @@ public final class MediaBox { self.concurrentQueue.async { let path = self.cachedRepresentationPathForId(resource.id, representation: representation) if let size = fileSize(path) { - subscriber.putNext(MediaResourceData(path: path, size: size, complete: true)) + subscriber.putNext(MediaResourceData(path: path, offset: 0, size: size, complete: true)) subscriber.putCompletion() } else { self.dataQueue.async { @@ -961,7 +974,7 @@ public final class MediaBox { if let strongSelf = self, let context = strongSelf.cachedRepresentationContexts[key] { strongSelf.cachedRepresentationContexts.removeValue(forKey: key) if let size = fileSize(path) { - let data = MediaResourceData(path: path, size: size, complete: true) + let data = MediaResourceData(path: path, offset: 0, size: size, complete: true) context.currentData = data for subscriber in context.dataSubscribers.copyItems() { subscriber(data) @@ -970,7 +983,7 @@ public final class MediaBox { } } else { if let strongSelf = self, let context = strongSelf.cachedRepresentationContexts[key] { - let data = MediaResourceData(path: path, size: 0, complete: false) + let data = MediaResourceData(path: path, offset: 0, size: 0, complete: false) context.currentData = data for subscriber in context.dataSubscribers.copyItems() { subscriber(data) @@ -1006,6 +1019,87 @@ public final class MediaBox { } } + public func collectOtherResourceUsage(excludeIds: Set) -> Signal<(Int64, [String], Int64), NoError> { + return Signal { subscriber in + self.dataQueue.async { + var result: Int64 = 0 + + var excludeNames = Set() + for id in excludeIds { + let partial = "\(self.fileNameForId(id.id))_partial" + let meta = "\(self.fileNameForId(id.id))_meta" + let complete = self.fileNameForId(id.id) + + excludeNames.insert(meta) + excludeNames.insert(partial) + excludeNames.insert(complete) + } + + var fileIds = Set() + + var paths: [String] = [] + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath), includingPropertiesForKeys: [.fileSizeKey, .fileResourceIdentifierKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if excludeNames.contains(url.lastPathComponent) { + continue loop + } + + if let fileId = (try? url.resourceValues(forKeys: Set([.fileResourceIdentifierKey])))?.fileResourceIdentifier as? Data { + if fileIds.contains(fileId) { + paths.append(url.lastPathComponent) + continue loop + } + + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + fileIds.insert(fileId) + paths.append(url.lastPathComponent) + result += Int64(value) + } + } + } + } + } + + var cacheResult: Int64 = 0 + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/cache"), includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + if let value = (try? url.resourceValues(forKeys: Set([.fileSizeKey])))?.fileSize, value != 0 { + cacheResult += Int64(value) + } + } + } + } + + subscriber.putNext((result, paths, cacheResult)) + subscriber.putCompletion() + } + return EmptyDisposable + } + } + + public func removeOtherCachedResources(paths: [String]) -> Signal { + return Signal { subscriber in + self.dataQueue.async { + for path in paths { + unlink(self.basePath + "/" + path) + } + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: self.basePath + "/cache"), includingPropertiesForKeys: [], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + loop: for url in enumerator { + if let url = url as? URL { + unlink(url.path) + } + } + } + subscriber.putCompletion() + } + return EmptyDisposable + } + } + public func removeCachedResources(_ ids: Set) -> Signal { return Signal { subscriber in self.dataQueue.async { @@ -1014,6 +1108,7 @@ public final class MediaBox { unlink(paths.complete) unlink(paths.partial) } + subscriber.putCompletion() } return EmptyDisposable } diff --git a/Postbox/MediaBoxFile.swift b/Postbox/MediaBoxFile.swift new file mode 100644 index 0000000000..7ec67bd063 --- /dev/null +++ b/Postbox/MediaBoxFile.swift @@ -0,0 +1,822 @@ +import Foundation +#if os(iOS) +import SwiftSignalKit +#else +import SwiftSignalKitMac +#endif + +import sqlcipher + +private final class MediaBoxFileMap { + fileprivate(set) var sum: Int32 + private(set) var ranges: IndexSet + private(set) var truncationSize: Int32? + + init() { + self.sum = 0 + self.ranges = IndexSet() + self.truncationSize = nil + } + + init?(fd: ManagedFile) { + guard let length = fd.getSize() else { + return nil + } + + var crc: UInt32 = 0 + var count: Int32 = 0 + var sum: Int32 = 0 + var ranges: IndexSet = IndexSet() + + guard fd.read(&crc, 4) == 4 else { + return nil + } + guard fd.read(&count, 4) == 4 else { + return nil + } + + if count < 0 { + return nil + } + + if count < 0 || length < 4 + 4 + count * 2 * 4 { + return nil + } + + var truncationSizeValue: Int32 = 0 + + var data = Data(count: Int(4 + count * 2 * 4)) + if !(data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Bool in + guard fd.read(bytes, data.count) == data.count else { + return false + } + + memcpy(&truncationSizeValue, bytes, 4) + + let calculatedCrc = Crc32(bytes, Int32(data.count)) + if calculatedCrc != crc { + return false + } + + var offset = 4 + for _ in 0 ..< count { + var intervalOffset: Int32 = 0 + var intervalLength: Int32 = 0 + memcpy(&intervalOffset, bytes.advanced(by: offset), 4) + memcpy(&intervalLength, bytes.advanced(by: offset + 4), 4) + offset += 8 + + ranges.insert(integersIn: Int(intervalOffset) ..< Int(intervalOffset + intervalLength)) + + sum += intervalLength + } + + return true + }) { + return nil + } + + self.sum = sum + self.ranges = ranges + if truncationSizeValue == -1 { + self.truncationSize = nil + } else { + self.truncationSize = truncationSizeValue + } + } + + func serialize(to file: ManagedFile) { + file.seek(position: 0) + let buffer = WriteBuffer() + var zero: Int32 = 0 + buffer.write(&zero, offset: 0, length: 4) + + let rangeView = self.ranges.rangeView + var count: Int32 = Int32(rangeView.count) + buffer.write(&count, offset: 0, length: 4) + + var truncationSizeValue: Int32 = self.truncationSize ?? -1 + buffer.write(&truncationSizeValue, offset: 0, length: 4) + + for range in rangeView { + var intervalOffset = Int32(range.lowerBound) + var intervalLength = Int32(range.count) + buffer.write(&intervalOffset, offset: 0, length: 4) + buffer.write(&intervalLength, offset: 0, length: 4) + } + var crc: UInt32 = Crc32(buffer.memory.advanced(by: 4 * 2), Int32(buffer.length - 4 * 2)) + memcpy(buffer.memory, &crc, 4) + let written = file.write(buffer.memory, count: buffer.length) + assert(written == buffer.length) + } + + fileprivate func fill(_ range: Range) { + let intRange = Range(Int(range.lowerBound) ..< Int(range.upperBound)) + let previousCount = self.ranges.count(in: intRange) + self.ranges.insert(integersIn: intRange) + self.sum += Int32(range.count - previousCount) + } + + fileprivate func truncate(_ size: Int32) { + self.truncationSize = size + } + + fileprivate func reset() { + self.truncationSize = nil + self.ranges.removeAll() + self.sum = 0 + } + + fileprivate func contains(_ range: Range) -> Bool { + let maxValue: Int + if let truncationSize = self.truncationSize { + maxValue = Int(truncationSize) + } else { + maxValue = Int.max + } + let intRange = Range(Int(range.lowerBound) ..< min(maxValue, Int(range.upperBound))) + return self.ranges.contains(integersIn: intRange) + } +} + +private class MediaBoxPartialFileDataRequest { + let range: Range + var waitingUntilAfterInitialFetch: Bool + let completion: (MediaResourceData) -> Void + + init(range: Range, waitingUntilAfterInitialFetch: Bool, completion: @escaping (MediaResourceData) -> Void) { + self.range = range + self.waitingUntilAfterInitialFetch = waitingUntilAfterInitialFetch + self.completion = completion + } +} + +final class MediaBoxPartialFile { + private let queue: Queue + private let path: String + private let completePath: String + private let completed: (Int32) -> Void + private let metadataFd: ManagedFile + private let fd: ManagedFile + fileprivate let fileMap: MediaBoxFileMap + private var dataRequests = Bag() + private let missingRanges: MediaBoxFileMissingRanges + private let rangeStatusRequests = Bag<((IndexSet) -> Void, () -> Void)>() + private let statusRequests = Bag<((MediaResourceStatus) -> Void, Int32?)>() + + private let fullRangeRequests = Bag() + + private var currentFetch: (Promise, Disposable)? + private var processedAtLeastOneFetch: Bool = false + + init?(queue: Queue, path: String, completePath: String, completed: @escaping (Int32) -> Void) { + assert(queue.isCurrent()) + if let metadataFd = ManagedFile(queue: queue, path: path + ".meta", mode: .readwrite), let fd = ManagedFile(queue: queue, path: path, mode: .readwrite) { + self.queue = queue + self.path = path + self.completePath = completePath + self.completed = completed + self.metadataFd = metadataFd + self.fd = fd + if let fileMap = MediaBoxFileMap(fd: self.metadataFd) { + self.fileMap = fileMap + } else { + self.fileMap = MediaBoxFileMap() + } + self.missingRanges = MediaBoxFileMissingRanges() + } else { + return nil + } + } + + deinit { + self.currentFetch?.1.dispose() + } + + var storedSize: Int32 { + assert(self.queue.isCurrent()) + return self.fileMap.sum + } + + func reset() { + assert(self.queue.isCurrent()) + + self.fileMap.reset() + self.fileMap.serialize(to: self.metadataFd) + + for request in self.dataRequests.copyItems() { + request.completion(MediaResourceData(path: self.path, offset: Int(request.range.lowerBound), size: 0, complete: false)) + } + + if let updatedRanges = self.missingRanges.reset(fileMap: self.fileMap) { + self.updateRequestRanges(updatedRanges, fetch: nil) + } + + if !self.rangeStatusRequests.isEmpty { + let ranges = self.fileMap.ranges + for (f, _) in self.rangeStatusRequests.copyItems() { + f(ranges) + } + } + + self.updateStatuses() + } + + func moveLocalFile(tempPath: String) { + assert(self.queue.isCurrent()) + + do { + try FileManager.default.moveItem(atPath: tempPath, toPath: self.completePath) + + if let size = fileSize(self.completePath) { + unlink(self.path) + unlink(self.path + ".meta") + + for completion in self.missingRanges.clear() { + completion() + } + + if let (_, disposable) = self.currentFetch { + self.currentFetch = nil + disposable.dispose() + } + + for request in self.dataRequests.copyItems() { + request.completion(MediaResourceData(path: self.completePath, offset: Int(request.range.lowerBound), size: max(0, size - Int(request.range.lowerBound)), complete: true)) + } + self.dataRequests.removeAll() + + for statusRequest in self.statusRequests.copyItems() { + statusRequest.0(.Local) + } + self.statusRequests.removeAll() + + self.completed(self.fileMap.sum) + } else { + assertionFailure() + } + } catch { + assertionFailure() + } + } + + func copyLocalItem(_ item: MediaResourceDataFetchCopyLocalItem) { + assert(self.queue.isCurrent()) + + do { + if item.copyTo(url: URL(fileURLWithPath: self.completePath)) { + + } else { + return + } + + if let size = fileSize(self.completePath) { + unlink(self.path) + unlink(self.path + ".meta") + + for completion in self.missingRanges.clear() { + completion() + } + + if let (_, disposable) = self.currentFetch { + self.currentFetch = nil + disposable.dispose() + } + + for request in self.dataRequests.copyItems() { + request.completion(MediaResourceData(path: self.completePath, offset: Int(request.range.lowerBound), size: max(0, size - Int(request.range.lowerBound)), complete: true)) + } + self.dataRequests.removeAll() + + for statusRequest in self.statusRequests.copyItems() { + statusRequest.0(.Local) + } + self.statusRequests.removeAll() + + self.completed(self.fileMap.sum) + } else { + assertionFailure() + } + } catch { + assertionFailure() + } + } + + func truncate(_ size: Int32) { + assert(self.queue.isCurrent()) + + let range: Range = size ..< Int32.max + + self.fileMap.truncate(size) + self.fileMap.serialize(to: self.metadataFd) + + self.checkDataRequestsAfterFill(range: range) + } + + func write(offset: Int32, data: Data, dataRange: Range) { + assert(self.queue.isCurrent()) + + self.fd.seek(position: Int64(offset)) + let written = data.withUnsafeBytes { (bytes: UnsafePointer) -> Int in + return self.fd.write(bytes.advanced(by: dataRange.lowerBound), count: dataRange.count) + } + assert(written == dataRange.count) + let range: Range = offset ..< (offset + Int32(dataRange.count)) + self.fileMap.fill(range) + self.fileMap.serialize(to: self.metadataFd) + + self.checkDataRequestsAfterFill(range: range) + } + + func checkDataRequestsAfterFill(range: Range) { + var removeIndices: [(Int, MediaBoxPartialFileDataRequest)] = [] + for (index, request) in self.dataRequests.copyItemsWithIndices() { + if request.range.overlaps(range) { + var maxValue = request.range.upperBound + if let truncationSize = self.fileMap.truncationSize { + maxValue = truncationSize + } + if request.range.lowerBound > maxValue { + assertionFailure() + removeIndices.append((index, request)) + } else { + let intRange = Range(Int(request.range.lowerBound) ..< Int(maxValue)) + if self.fileMap.ranges.contains(integersIn: intRange) { + removeIndices.append((index, request)) + } + } + } + } + if !removeIndices.isEmpty { + for (index, request) in removeIndices { + self.dataRequests.remove(index) + var maxValue = request.range.upperBound + if let truncationSize = self.fileMap.truncationSize { + maxValue = truncationSize + } + request.completion(MediaResourceData(path: self.path, offset: Int(request.range.lowerBound), size: Int(maxValue) - Int(request.range.lowerBound), complete: true)) + } + } + + var isCompleted = false + if let truncationSize = self.fileMap.truncationSize, self.fileMap.contains(0 ..< truncationSize) { + isCompleted = true + } + + if isCompleted { + for completion in self.missingRanges.clear() { + completion() + } + } else { + if let (updatedRanges, completions) = self.missingRanges.fill(range) { + self.updateRequestRanges(updatedRanges, fetch: nil) + completions.forEach({ $0() }) + } + } + + if !self.rangeStatusRequests.isEmpty { + let ranges = self.fileMap.ranges + for (f, completed) in self.rangeStatusRequests.copyItems() { + f(ranges) + if isCompleted { + completed() + } + } + if isCompleted { + self.rangeStatusRequests.removeAll() + } + } + + self.updateStatuses() + + if isCompleted { + for statusRequest in self.statusRequests.copyItems() { + statusRequest.0(.Local) + } + self.statusRequests.removeAll() + self.fd.sync() + let linkResult = link(self.path, self.completePath) + assert(linkResult == 0) + self.completed(self.fileMap.sum) + } + } + + func read(range: Range) -> Data? { + assert(self.queue.isCurrent()) + + if self.fileMap.contains(range) { + self.fd.seek(position: Int64(range.lowerBound)) + var data = Data(count: range.count) + let readBytes = data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Int in + return self.fd.read(bytes, data.count) + } + if readBytes == data.count { + return data + } else { + return nil + } + } else { + return nil + } + } + + func data(range: Range, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable { + assert(self.queue.isCurrent()) + + if self.fileMap.contains(range) { + next(MediaResourceData(path: self.path, offset: Int(range.lowerBound), size: range.count, complete: true)) + return EmptyDisposable + } + + var waitingUntilAfterInitialFetch = false + if waitUntilAfterInitialFetch && !self.processedAtLeastOneFetch { + waitingUntilAfterInitialFetch = true + } else { + next(MediaResourceData(path: self.path, offset: Int(range.lowerBound), size: 0, complete: false)) + } + + let index = self.dataRequests.add(MediaBoxPartialFileDataRequest(range: range, waitingUntilAfterInitialFetch: waitingUntilAfterInitialFetch, completion: { data in + next(data) + })) + + let queue = self.queue + return ActionDisposable { [weak self] in + queue.async { + if let strongSelf = self { + strongSelf.dataRequests.remove(index) + } + } + } + } + + func fetched(range: Range, fetch: @escaping (Signal) -> Signal, completed: @escaping () -> Void) -> Disposable { + assert(self.queue.isCurrent()) + + if self.fileMap.contains(range) { + completed() + return EmptyDisposable + } + + let (index, updatedRanges) = self.missingRanges.addRequest(fileMap: self.fileMap, range: range, completion: { + completed() + }) + if let updatedRanges = updatedRanges { + self.updateRequestRanges(updatedRanges, fetch: fetch) + } + + let queue = self.queue + return ActionDisposable { [weak self] in + queue.async { + if let strongSelf = self { + if let updatedRanges = strongSelf.missingRanges.removeRequest(fileMap: strongSelf.fileMap, index: index) { + strongSelf.updateRequestRanges(updatedRanges, fetch: nil) + } + } + } + } + } + + func fetchedFullRange(fetch: @escaping (Signal) -> Signal, completed: @escaping () -> Void) -> Disposable { + let queue = self.queue + let disposable = MetaDisposable() + + let index = self.fullRangeRequests.add(disposable) + self.updateStatuses() + + disposable.set(self.fetched(range: 0 ..< Int32.max, fetch: fetch, completed: { [weak self] in + queue.async { + if let strongSelf = self { + strongSelf.fullRangeRequests.remove(index) + if strongSelf.fullRangeRequests.isEmpty { + strongSelf.updateStatuses() + } + } + completed() + } + })) + + return ActionDisposable { [weak self] in + queue.async { + if let strongSelf = self { + strongSelf.fullRangeRequests.remove(index) + disposable.dispose() + if strongSelf.fullRangeRequests.isEmpty { + strongSelf.updateStatuses() + } + } + } + } + } + + func cancelFullRangeFetches() { + self.fullRangeRequests.copyItems().forEach({ $0.dispose() }) + self.fullRangeRequests.removeAll() + + self.updateStatuses() + } + + private func updateStatuses() { + if !self.statusRequests.isEmpty { + for (f, size) in self.statusRequests.copyItems() { + let status = self.immediateStatus(size: size) + f(status) + } + } + } + + func rangeStatus(next: @escaping (IndexSet) -> Void, completed: @escaping () -> Void) -> Disposable { + assert(self.queue.isCurrent()) + + next(self.fileMap.ranges) + if let truncationSize = self.fileMap.truncationSize, self.fileMap.contains(0 ..< truncationSize) { + completed() + return EmptyDisposable + } + + let index = self.rangeStatusRequests.add((next, completed)) + + let queue = self.queue + return ActionDisposable { [weak self] in + queue.async { + if let strongSelf = self { + strongSelf.rangeStatusRequests.remove(index) + } + } + } + } + + private func immediateStatus(size: Int32?) -> MediaResourceStatus { + let status: MediaResourceStatus + if self.fullRangeRequests.isEmpty { + status = .Remote + } else { + let progress: Float + if let truncationSize = self.fileMap.truncationSize, truncationSize != 0 { + progress = Float(self.fileMap.sum) / Float(truncationSize) + } else if let size = size { + progress = Float(self.fileMap.sum) / Float(size) + } else { + progress = 0.0 + } + status = .Fetching(isActive: true, progress: progress) + } + return status + } + + func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int32?) -> Disposable { + let index = self.statusRequests.add((next, size)) + + let value = self.immediateStatus(size: size) + next(value) + if case .Local = value { + completed() + return EmptyDisposable + } else { + let queue = self.queue + return ActionDisposable { [weak self] in + queue.async { + if let strongSelf = self { + strongSelf.statusRequests.remove(index) + } + } + } + } + } + + private func updateRequestRanges(_ ranges: IndexSet, fetch: ((Signal) -> Signal)?) { + assert(self.queue.isCurrent()) + + if ranges.isEmpty { + if let (_, disposable) = self.currentFetch { + self.currentFetch = nil + disposable.dispose() + } + } else { + if let (promise, _) = self.currentFetch { + promise.set(.single(ranges)) + } else if let fetch = fetch { + let promise = Promise() + let disposable = MetaDisposable() + self.currentFetch = (promise, disposable) + disposable.set((fetch(promise.get()) |> deliverOn(self.queue)).start(next: { [weak self] data in + if let strongSelf = self { + switch data { + case .reset: + if !strongSelf.fileMap.ranges.isEmpty { + strongSelf.reset() + } + case let .resourceSizeUpdated(size): + strongSelf.truncate(Int32(size)) + case let .dataPart(resourceOffset, data, range, complete): + if !data.isEmpty { + strongSelf.write(offset: Int32(resourceOffset), data: data, dataRange: range) + } + if complete { + if let maxOffset = strongSelf.fileMap.ranges.max() { + let maxValue = max(resourceOffset + range.count, maxOffset) + strongSelf.truncate(Int32(maxValue)) + } + } + case let .replaceHeader(data, range): + strongSelf.write(offset: 0, data: data, dataRange: range) + case let .moveLocalFile(path): + strongSelf.moveLocalFile(tempPath: path) + case let .copyLocalItem(item): + strongSelf.copyLocalItem(item) + } + if !strongSelf.processedAtLeastOneFetch { + strongSelf.processedAtLeastOneFetch = true + for request in strongSelf.dataRequests.copyItems() { + if request.waitingUntilAfterInitialFetch { + request.waitingUntilAfterInitialFetch = false + + if strongSelf.fileMap.contains(request.range) { + request.completion(MediaResourceData(path: strongSelf.path, offset: Int(request.range.lowerBound), size: request.range.count, complete: true)) + } else { + request.completion(MediaResourceData(path: strongSelf.path, offset: Int(request.range.lowerBound), size: 0, complete: false)) + } + } + } + } + } + })) + promise.set(.single(ranges)) + } + } + } +} + +private final class MediaBoxFileMissingRange { + var range: Range + var remainingRanges: IndexSet + let completion: () -> Void + + init(range: Range, completion: @escaping () -> Void) { + self.range = range + let intRange = Range(Int(range.lowerBound) ..< Int(range.upperBound)) + self.remainingRanges = IndexSet(integersIn: intRange) + self.completion = completion + } +} + +private final class MediaBoxFileMissingRanges { + private var requestedRanges = Bag() + + private var missingRanges = IndexSet() + + func clear() -> [() -> Void] { + let completions = self.requestedRanges.copyItems().map({ $0.completion }) + self.requestedRanges.removeAll() + return completions + } + + func reset(fileMap: MediaBoxFileMap) -> IndexSet? { + return self.update(fileMap: fileMap) + } + + func fill(_ range: Range) -> (IndexSet, [() -> Void])? { + let intRange = Range(Int(range.lowerBound) ..< Int(range.upperBound)) + if self.missingRanges.intersects(integersIn: intRange) { + self.missingRanges.remove(integersIn: intRange) + var completions: [() -> Void] = [] + for (index, item) in self.requestedRanges.copyItemsWithIndices() { + if item.range.overlaps(range) { + item.remainingRanges.remove(integersIn: intRange) + if item.remainingRanges.isEmpty { + self.requestedRanges.remove(index) + completions.append(item.completion) + } + } + } + return (self.missingRanges, completions) + } else { + return nil + } + } + + func addRequest(fileMap: MediaBoxFileMap, range: Range, completion: @escaping () -> Void) -> (Int, IndexSet?) { + let index = self.requestedRanges.add(MediaBoxFileMissingRange(range: range, completion: completion)) + + return (index, self.update(fileMap: fileMap)) + } + + func removeRequest(fileMap: MediaBoxFileMap, index: Int) -> IndexSet? { + self.requestedRanges.remove(index) + return self.update(fileMap: fileMap) + } + + private func update(fileMap: MediaBoxFileMap) -> IndexSet? { + var requested = IndexSet() + for (item) in self.requestedRanges.copyItems() { + let intRange = Range(Int(item.range.lowerBound) ..< Int(item.range.upperBound)) + requested.insert(integersIn: intRange) + } + requested.subtract(fileMap.ranges) + if requested != self.missingRanges { + self.missingRanges = requested + return requested + } + return nil + } +} + +private enum MediaBoxFileContent { + case complete(String, Int) + case partial(MediaBoxPartialFile) +} + +final class MediaBoxFileContext { + private let queue: Queue + private let path: String + private let partialPath: String + + private var content: MediaBoxFileContent + + init?(queue: Queue, path: String, partialPath: String) { + assert(queue.isCurrent()) + + self.queue = queue + self.path = path + self.partialPath = partialPath + + var completeImpl: ((Int32) -> Void)? + if let size = fileSize(path) { + self.content = .complete(path, size) + } else if let file = MediaBoxPartialFile(queue: queue, path: partialPath, completePath: path, completed: { size in + completeImpl?(size) + }) { + self.content = .partial(file) + completeImpl = { [weak self] size in + queue.async { + if let strongSelf = self { + strongSelf.content = .complete(path, Int(size)) + } + } + } + } else { + return nil + } + } + + deinit { + assert(self.queue.isCurrent()) + } + + func data(range: Range, waitUntilAfterInitialFetch: Bool, next: @escaping (MediaResourceData) -> Void) -> Disposable { + switch self.content { + case let .complete(path, size): + next(MediaResourceData(path: path, offset: Int(range.lowerBound), size: min(Int(range.upperBound), size), complete: true)) + return EmptyDisposable + case let .partial(file): + return file.data(range: range, waitUntilAfterInitialFetch: waitUntilAfterInitialFetch, next: next) + } + } + + func fetched(range: Range, fetch: @escaping (Signal) -> Signal, completed: @escaping () -> Void) -> Disposable { + switch self.content { + case .complete: + return EmptyDisposable + case let .partial(file): + return file.fetched(range: range, fetch: fetch, completed: completed) + } + } + + func fetchedFullRange(fetch: @escaping (Signal) -> Signal, completed: @escaping () -> Void) -> Disposable { + switch self.content { + case .complete: + return EmptyDisposable + case let .partial(file): + return file.fetchedFullRange(fetch: fetch, completed: completed) + } + } + + func cancelFullRangeFetches() { + switch self.content { + case .complete: + break + case let .partial(file): + file.cancelFullRangeFetches() + } + } + + func rangeStatus(next: @escaping (IndexSet) -> Void, completed: @escaping () -> Void) -> Disposable { + switch self.content { + case let .complete(_, size): + next(IndexSet(0 ..< size)) + completed() + return EmptyDisposable + case let .partial(file): + return file.rangeStatus(next: next, completed: completed) + } + } + + func status(next: @escaping (MediaResourceStatus) -> Void, completed: @escaping () -> Void, size: Int32?) -> Disposable { + switch self.content { + case .complete: + next(.Local) + return EmptyDisposable + case let .partial(file): + return file.status(next: next, completed: completed, size: size) + } + } +} diff --git a/Postbox/Message.swift b/Postbox/Message.swift index 3b92b05594..6cb7286b52 100644 --- a/Postbox/Message.swift +++ b/Postbox/Message.swift @@ -651,12 +651,24 @@ public final class StoreMessage { } } + public func withUpdatedFlags(_ flags: StoreMessageFlags) -> StoreMessage { + if flags == self.flags { + return self + } else { + return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, timestamp: self.timestamp, flags: flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: attributes, media: self.media) + } + } + public func withUpdatedAttributes(_ attributes: [MessageAttribute]) -> StoreMessage { return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: attributes, media: self.media) } public func withUpdatedLocalTags(_ localTags: LocalMessageTags) -> StoreMessage { - return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: self.attributes, media: self.media) + if localTags == self.localTags { + return self + } else { + return StoreMessage(id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, timestamp: self.timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: localTags, forwardInfo: self.forwardInfo, authorId: self.authorId, text: self.text, attributes: self.attributes, media: self.media) + } } } diff --git a/Postbox/MessageHistoryView.swift b/Postbox/MessageHistoryView.swift index ff1a4de322..d77f554198 100644 --- a/Postbox/MessageHistoryView.swift +++ b/Postbox/MessageHistoryView.swift @@ -13,6 +13,7 @@ public enum AdditionalMessageHistoryViewData { case cachedPeerData(PeerId) case cachedPeerDataMessages(PeerId) case peerChatState(PeerId) + case peerGroupState(PeerGroupId) case totalUnreadCount case peerNotificationSettings(PeerId) } @@ -21,6 +22,7 @@ public enum AdditionalMessageHistoryViewDataEntry { case cachedPeerData(PeerId, CachedPeerData?) case cachedPeerDataMessages(PeerId, [MessageId: Message]?) case peerChatState(PeerId, PeerChatState?) + case peerGroupState(PeerGroupId, PeerGroupState?) case totalUnreadCount(Int32) case peerNotificationSettings(PeerNotificationSettings?) } @@ -1001,6 +1003,11 @@ final class MutableMessageHistoryView { self.additionalDatas[i] = .peerChatState(peerId, postbox.peerChatStateTable.get(peerId) as? PeerChatState) hasChanges = true } + case let .peerGroupState(groupId, _): + if transaction.currentUpdatedPeerGroupStates.contains(groupId) { + self.additionalDatas[i] = .peerGroupState(groupId, postbox.peerGroupStateTable.get(groupId)) + hasChanges = true + } case .totalUnreadCount: break case .peerNotificationSettings: diff --git a/Postbox/PeerGroupStateView.swift b/Postbox/PeerGroupStateView.swift new file mode 100644 index 0000000000..a0c63daf9a --- /dev/null +++ b/Postbox/PeerGroupStateView.swift @@ -0,0 +1,35 @@ +import Foundation + +final class MutablePeerGroupStateView: MutablePostboxView { + let groupId: PeerGroupId + var state: PeerGroupState? + + init(postbox: Postbox, groupId: PeerGroupId) { + self.groupId = groupId + self.state = postbox.peerGroupStateTable.get(groupId) + } + + func replay(postbox: Postbox, transaction: PostboxTransaction) -> Bool { + if transaction.currentUpdatedPeerGroupStates.contains(self.groupId) { + self.state = postbox.peerGroupStateTable.get(self.groupId) + return true + } else { + return false + } + } + + func immutableView() -> PostboxView { + return PeerGroupStateView(self) + } +} + +public final class PeerGroupStateView: PostboxView { + public let groupId: PeerGroupId + public let state: PeerGroupState? + + init(_ view: MutablePeerGroupStateView) { + self.groupId = view.groupId + self.state = view.state + } +} + diff --git a/Postbox/Postbox.swift b/Postbox/Postbox.swift index 69f397891a..b3689e0ced 100644 --- a/Postbox/Postbox.swift +++ b/Postbox/Postbox.swift @@ -62,6 +62,16 @@ public final class Modifier { self.postbox?.addFeedHoleFromLatestEntries(groupId: groupId) } + public func addMessagesToGroupFeedIndex(groupId: PeerGroupId, ids: [MessageId]) { + assert(!self.disposed) + self.postbox?.addMessagesToGroupFeedIndex(groupId: groupId, ids: ids) + } + + public func removeMessagesFromGroupFeedIndex(groupId: PeerGroupId, ids: [MessageId]) { + assert(!self.disposed) + self.postbox?.removeMessagesFromGroupFeedIndex(groupId: groupId, ids: ids) + } + public func replaceChatListHole(groupId: PeerGroupId?, index: MessageIndex, hole: ChatListHole?) { assert(!self.disposed) self.postbox?.replaceChatListHole(groupId: groupId, index: index, hole: hole) @@ -187,6 +197,16 @@ public final class Modifier { self.postbox?.setPeerChatState(id, state: state) } + public func getPeerGroupState(_ id: PeerGroupId) -> PeerGroupState? { + assert(!self.disposed) + return self.postbox?.peerGroupStateTable.get(id) + } + + public func setPeerGroupState(_ id: PeerGroupId, state: PeerGroupState) { + assert(!self.disposed) + self.postbox?.setPeerGroupState(id, state: state) + } + public func getPeerChatInterfaceState(_ id: PeerId) -> PeerChatInterfaceState? { assert(!self.disposed) return self.postbox?.peerChatInterfaceStateTable.get(id) @@ -832,10 +852,10 @@ public func openPostbox(basePath: String, globalMessageIdsNamespace: MessageId.N return Signal { subscriber in queue.async { let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil) - + #if DEBUG //debugSaveState(basePath: basePath, name: "previous") - debugRestoreState(basePath: basePath, name: "previous") + //debugRestoreState(basePath: basePath, name: "previous") #endif loop: while true { @@ -919,6 +939,7 @@ public final class Postbox { private var currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]] = [:] private var currentItemCollectionInfosOperations: [ItemCollectionInfosOperation] = [] private var currentUpdatedPeerChatStates = Set() + private var currentUpdatedPeerGroupStates = Set() private var currentUpdatedAccessChallengeData: PostboxAccessChallengeData? private var currentPendingMessageActionsOperations: [PendingMessageActionsOperation] = [] private var currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32] = [:] @@ -967,6 +988,7 @@ public final class Postbox { let globalMessageHistoryTagsTable: GlobalMessageHistoryTagsTable let localMessageHistoryTagsTable: LocalMessageHistoryTagsTable let peerChatStateTable: PeerChatStateTable + let peerGroupStateTable: PeerGroupStateTable let readStateTable: MessageHistoryReadStateTable let synchronizeReadStateTable: MessageHistorySynchronizeReadStateTable let contactsTable: ContactTable @@ -1057,6 +1079,7 @@ public final class Postbox { self.groupAssociationTable = PeerGroupAssociationTable(valueBox: self.valueBox, table: PeerGroupAssociationTable.tableSpec(49)) self.messageHistoryTable = MessageHistoryTable(valueBox: self.valueBox, table: MessageHistoryTable.tableSpec(7), messageHistoryIndexTable: self.messageHistoryIndexTable, messageMediaTable: self.mediaTable, historyMetadataTable: self.messageHistoryMetadataTable, globallyUniqueMessageIdsTable: self.globallyUniqueMessageIdsTable, unsentTable: self.messageHistoryUnsentTable, tagsTable: self.messageHistoryTagsTable, globalTagsTable: self.globalMessageHistoryTagsTable, localTagsTable: self.localMessageHistoryTagsTable, readStateTable: self.readStateTable, synchronizeReadStateTable: self.synchronizeReadStateTable, textIndexTable: self.textIndexTable, summaryTable: self.messageHistoryTagsSummaryTable, pendingActionsTable: self.pendingMessageActionsTable, groupAssociationTable: self.groupAssociationTable, groupFeedIndexTable: self.groupFeedIndexTable) self.peerChatStateTable = PeerChatStateTable(valueBox: self.valueBox, table: PeerChatStateTable.tableSpec(13)) + self.peerGroupStateTable = PeerGroupStateTable(valueBox: self.valueBox, table: PeerGroupStateTable.tableSpec(53)) self.peerNameTokenIndexTable = ReverseIndexReferenceTable(valueBox: self.valueBox, table: ReverseIndexReferenceTable.tableSpec(26)) self.peerNameIndexTable = PeerNameIndexTable(valueBox: self.valueBox, table: PeerNameIndexTable.tableSpec(27), peerTable: self.peerTable, peerNameTokenIndexTable: self.peerNameTokenIndexTable) self.contactsTable = ContactTable(valueBox: self.valueBox, table: ContactTable.tableSpec(16), peerNameIndexTable: self.peerNameIndexTable) @@ -1103,6 +1126,7 @@ public final class Postbox { tables.append(self.chatListTable) tables.append(self.groupAssociationTable) tables.append(self.peerChatStateTable) + tables.append(self.peerGroupStateTable) tables.append(self.contactsTable) tables.append(self.peerRatingTable) tables.append(self.peerNotificationSettingsTable) @@ -1388,6 +1412,24 @@ public final class Postbox { self.groupFeedIndexTable.addHoleFromLatestEntries(groupId: groupId, messageHistoryTable: self.messageHistoryTable, operations: &self.currentGroupFeedOperations) } + fileprivate func addMessagesToGroupFeedIndex(groupId: PeerGroupId, ids: [MessageId]) { + for id in ids { + if let entry = self.messageHistoryIndexTable.get(id), case let .Message(index) = entry { + if let message = self.messageHistoryTable.getMessage(index) { + self.groupFeedIndexTable.add(groupId: groupId, message: message, operations: &self.currentGroupFeedOperations) + } + } + } + } + + fileprivate func removeMessagesFromGroupFeedIndex(groupId: PeerGroupId, ids: [MessageId]) { + for id in ids { + if let entry = self.messageHistoryIndexTable.get(id), case let .Message(index) = entry { + self.groupFeedIndexTable.remove(groupId: groupId, messageIndex: index, operations: &self.currentGroupFeedOperations) + } + } + } + fileprivate func replaceChatListHole(groupId: PeerGroupId?, index: MessageIndex, hole: ChatListHole?) { self.chatListTable.replaceHole(groupId: groupId, index: index, hole: hole, operations: &self.currentChatListOperations) } @@ -1670,7 +1712,7 @@ public final class Postbox { return self.peerTable.get(peerId) }, updatedTotalUnreadCount: &self.currentUpdatedTotalUnreadCount) - let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentOperationsByPeerId: self.currentOperationsByPeerId, currentGroupFeedOperations: self.currentGroupFeedOperations, peerIdsWithFilledHoles: self.currentFilledHolesByPeerId, removedHolesByPeerId: self.currentRemovedHolesByPeerId, groupFeedIdsWithFilledHoles: self.currentGroupFeedIdsWithFilledHoles, removedHolesByPeerGroupId: self.currentRemovedHolesByPeerGroupId, chatListOperations: self.currentChatListOperations, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadCount: self.currentUpdatedTotalUnreadCount, peerIdsWithUpdatedUnreadCounts: Set(transactionUnreadCountDeltas.keys), peerIdsWithUpdatedCombinedReadStates: peerIdsWithUpdatedCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, updatedAccessChallengeData: self.currentUpdatedAccessChallengeData, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, currentGroupFeedReadStateContext: self.currentGroupFeedReadStateContext, currentInitialPeerGroupIdsBeforeUpdate: self.currentInitialPeerGroupIdsBeforeUpdate, currentUpdatedMasterClientId: currentUpdatedMasterClientId) + let transaction = PostboxTransaction(currentUpdatedState: self.currentUpdatedState, currentOperationsByPeerId: self.currentOperationsByPeerId, currentGroupFeedOperations: self.currentGroupFeedOperations, peerIdsWithFilledHoles: self.currentFilledHolesByPeerId, removedHolesByPeerId: self.currentRemovedHolesByPeerId, groupFeedIdsWithFilledHoles: self.currentGroupFeedIdsWithFilledHoles, removedHolesByPeerGroupId: self.currentRemovedHolesByPeerGroupId, chatListOperations: self.currentChatListOperations, currentUpdatedPeers: self.currentUpdatedPeers, currentUpdatedPeerNotificationSettings: self.currentUpdatedPeerNotificationSettings, currentUpdatedCachedPeerData: self.currentUpdatedCachedPeerData, currentUpdatedPeerPresences: currentUpdatedPeerPresences, currentUpdatedPeerChatListEmbeddedStates: self.currentUpdatedPeerChatListEmbeddedStates, currentUpdatedTotalUnreadCount: self.currentUpdatedTotalUnreadCount, peerIdsWithUpdatedUnreadCounts: Set(transactionUnreadCountDeltas.keys), peerIdsWithUpdatedCombinedReadStates: peerIdsWithUpdatedCombinedReadStates, currentPeerMergedOperationLogOperations: self.currentPeerMergedOperationLogOperations, currentTimestampBasedMessageAttributesOperations: self.currentTimestampBasedMessageAttributesOperations, unsentMessageOperations: self.currentUnsentOperations, updatedSynchronizePeerReadStateOperations: self.currentUpdatedSynchronizeReadStateOperations, currentPreferencesOperations: self.currentPreferencesOperations, currentOrderedItemListOperations: self.currentOrderedItemListOperations, currentItemCollectionItemsOperations: self.currentItemCollectionItemsOperations, currentItemCollectionInfosOperations: self.currentItemCollectionInfosOperations, currentUpdatedPeerChatStates: self.currentUpdatedPeerChatStates, currentUpdatedPeerGroupStates: self.currentUpdatedPeerGroupStates, updatedAccessChallengeData: self.currentUpdatedAccessChallengeData, currentGlobalTagsOperations: self.currentGlobalTagsOperations, currentLocalTagsOperations: self.currentLocalTagsOperations, updatedMedia: self.currentUpdatedMedia, replaceRemoteContactCount: self.currentReplaceRemoteContactCount, replaceContactPeerIds: self.currentReplacedContactPeerIds, currentPendingMessageActionsOperations: self.currentPendingMessageActionsOperations, currentUpdatedMessageActionsSummaries: self.currentUpdatedMessageActionsSummaries, currentUpdatedMessageTagSummaries: self.currentUpdatedMessageTagSummaries, currentInvalidateMessageTagSummaries: self.currentInvalidateMessageTagSummaries, currentUpdatedPendingPeerNotificationSettings: self.currentUpdatedPendingPeerNotificationSettings, currentGroupFeedReadStateContext: self.currentGroupFeedReadStateContext, currentInitialPeerGroupIdsBeforeUpdate: self.currentInitialPeerGroupIdsBeforeUpdate, currentUpdatedMasterClientId: currentUpdatedMasterClientId) var updatedTransactionState: Int64? var updatedMasterClientId: Int64? if !transaction.isEmpty { @@ -1714,6 +1756,7 @@ public final class Postbox { self.currentItemCollectionItemsOperations.removeAll() self.currentItemCollectionInfosOperations.removeAll() self.currentUpdatedPeerChatStates.removeAll() + self.currentUpdatedPeerGroupStates.removeAll() self.currentUpdatedAccessChallengeData = nil self.currentPendingMessageActionsOperations.removeAll() self.currentUpdatedMessageActionsSummaries.removeAll() @@ -1870,6 +1913,11 @@ public final class Postbox { self.currentUpdatedPeerChatStates.insert(id) } + fileprivate func setPeerGroupState(_ id: PeerGroupId, state: PeerGroupState) { + self.peerGroupStateTable.set(id, state: state) + self.currentUpdatedPeerGroupStates.insert(id) + } + fileprivate func updatePeerChatInterfaceState(_ id: PeerId, update: (PeerChatInterfaceState?) -> (PeerChatInterfaceState?)) { let updatedState = update(self.peerChatInterfaceStateTable.get(id)) let (_, updatedEmbeddedState) = self.peerChatInterfaceStateTable.set(id, state: updatedState) @@ -2254,6 +2302,8 @@ public final class Postbox { additionalDataEntries.append(.cachedPeerDataMessages(peerId, messages)) case let .peerChatState(peerId): additionalDataEntries.append(.peerChatState(peerId, self.peerChatStateTable.get(peerId) as? PeerChatState)) + case let .peerGroupState(groupId): + additionalDataEntries.append(.peerGroupState(groupId, self.peerGroupStateTable.get(groupId))) case .totalUnreadCount: additionalDataEntries.append(.totalUnreadCount(self.messageHistoryMetadataTable.getChatListTotalUnreadCount())) case let .peerNotificationSettings(peerId): diff --git a/Postbox/PostboxPrivate/module.modulemap b/Postbox/PostboxPrivate/module.modulemap index b4494ba2ae..d2a187ee15 100644 --- a/Postbox/PostboxPrivate/module.modulemap +++ b/Postbox/PostboxPrivate/module.modulemap @@ -3,5 +3,6 @@ module sqlcipher { header "sqlcipher/sqlite3ext.h" header "sqlcipher/SQLite-Bridging.h" header "sqlcipher/fts3_tokenizer.h" + header "../Crc32.h" export * } diff --git a/Postbox/PostboxTransaction.swift b/Postbox/PostboxTransaction.swift index 20d71d56b6..cd62f063a0 100644 --- a/Postbox/PostboxTransaction.swift +++ b/Postbox/PostboxTransaction.swift @@ -24,6 +24,7 @@ final class PostboxTransaction { let currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]] let currentItemCollectionInfosOperations: [ItemCollectionInfosOperation] let currentUpdatedPeerChatStates: Set + let currentUpdatedPeerGroupStates: Set let updatedAccessChallengeData: PostboxAccessChallengeData? let currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation] let currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation] @@ -130,6 +131,9 @@ final class PostboxTransaction { if !currentUpdatedPeerChatStates.isEmpty { return false } + if !currentUpdatedPeerGroupStates.isEmpty { + return false + } if self.updatedAccessChallengeData != nil { return false } @@ -163,7 +167,7 @@ final class PostboxTransaction { return true } - init(currentUpdatedState: PostboxCoding?, currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], currentGroupFeedOperations: [PeerGroupId : [GroupFeedIndexOperation]], peerIdsWithFilledHoles: [PeerId: [MessageIndex: HoleFillDirection]], removedHolesByPeerId: [PeerId: [MessageIndex: HoleFillDirection]], groupFeedIdsWithFilledHoles: [PeerGroupId: [MessageIndex: HoleFillDirection]], removedHolesByPeerGroupId: [PeerGroupId: [MessageIndex: HoleFillDirection]], chatListOperations: [WrappedPeerGroupId: [ChatListOperation]], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: PeerNotificationSettings], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?], currentUpdatedTotalUnreadCount: Int32?, peerIdsWithUpdatedUnreadCounts: Set, peerIdsWithUpdatedCombinedReadStates: Set, currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set, updatedAccessChallengeData: PostboxAccessChallengeData?, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set, currentGroupFeedReadStateContext: GroupFeedReadStateUpdateContext, currentInitialPeerGroupIdsBeforeUpdate: [PeerId: WrappedPeerGroupId], currentUpdatedMasterClientId: Int64?) { + init(currentUpdatedState: PostboxCoding?, currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], currentGroupFeedOperations: [PeerGroupId : [GroupFeedIndexOperation]], peerIdsWithFilledHoles: [PeerId: [MessageIndex: HoleFillDirection]], removedHolesByPeerId: [PeerId: [MessageIndex: HoleFillDirection]], groupFeedIdsWithFilledHoles: [PeerGroupId: [MessageIndex: HoleFillDirection]], removedHolesByPeerGroupId: [PeerGroupId: [MessageIndex: HoleFillDirection]], chatListOperations: [WrappedPeerGroupId: [ChatListOperation]], currentUpdatedPeers: [PeerId: Peer], currentUpdatedPeerNotificationSettings: [PeerId: PeerNotificationSettings], currentUpdatedCachedPeerData: [PeerId: CachedPeerData], currentUpdatedPeerPresences: [PeerId: PeerPresence], currentUpdatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?], currentUpdatedTotalUnreadCount: Int32?, peerIdsWithUpdatedUnreadCounts: Set, peerIdsWithUpdatedCombinedReadStates: Set, currentPeerMergedOperationLogOperations: [PeerMergedOperationLogOperation], currentTimestampBasedMessageAttributesOperations: [TimestampBasedMessageAttributesOperation], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], updatedSynchronizePeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?], currentPreferencesOperations: [PreferencesOperation], currentOrderedItemListOperations: [Int32: [OrderedItemListOperation]], currentItemCollectionItemsOperations: [ItemCollectionId: [ItemCollectionItemsOperation]], currentItemCollectionInfosOperations: [ItemCollectionInfosOperation], currentUpdatedPeerChatStates: Set, currentUpdatedPeerGroupStates: Set, updatedAccessChallengeData: PostboxAccessChallengeData?, currentGlobalTagsOperations: [GlobalMessageHistoryTagsOperation], currentLocalTagsOperations: [IntermediateMessageHistoryLocalTagsOperation], updatedMedia: [MediaId: Media?], replaceRemoteContactCount: Int32?, replaceContactPeerIds: Set?, currentPendingMessageActionsOperations: [PendingMessageActionsOperation], currentUpdatedMessageActionsSummaries: [PendingMessageActionsSummaryKey: Int32], currentUpdatedMessageTagSummaries: [MessageHistoryTagsSummaryKey: MessageHistoryTagNamespaceSummary], currentInvalidateMessageTagSummaries: [InvalidatedMessageHistoryTagsSummaryEntryOperation], currentUpdatedPendingPeerNotificationSettings: Set, currentGroupFeedReadStateContext: GroupFeedReadStateUpdateContext, currentInitialPeerGroupIdsBeforeUpdate: [PeerId: WrappedPeerGroupId], currentUpdatedMasterClientId: Int64?) { self.currentUpdatedState = currentUpdatedState self.currentOperationsByPeerId = currentOperationsByPeerId self.currentGroupFeedOperations = currentGroupFeedOperations @@ -189,6 +193,7 @@ final class PostboxTransaction { self.currentItemCollectionItemsOperations = currentItemCollectionItemsOperations self.currentItemCollectionInfosOperations = currentItemCollectionInfosOperations self.currentUpdatedPeerChatStates = currentUpdatedPeerChatStates + self.currentUpdatedPeerGroupStates = currentUpdatedPeerGroupStates self.updatedAccessChallengeData = updatedAccessChallengeData self.currentGlobalTagsOperations = currentGlobalTagsOperations self.currentLocalTagsOperations = currentLocalTagsOperations diff --git a/Postbox/Views.swift b/Postbox/Views.swift index 163f78d9a2..0203a61807 100644 --- a/Postbox/Views.swift +++ b/Postbox/Views.swift @@ -5,6 +5,7 @@ public enum PostboxViewKey: Hashable { case itemCollectionIds(namespaces: [ItemCollectionId.Namespace]) case itemCollectionInfo(id: ItemCollectionId) case peerChatState(peerId: PeerId) + case peerGroupState(groupId: PeerGroupId) case orderedItemList(id: Int32) case accessChallengeData case preferences(keys: Set) @@ -31,6 +32,8 @@ public enum PostboxViewKey: Hashable { return 1 case let .peerChatState(peerId): return peerId.hashValue + case let .peerGroupState(groupId): + return groupId.hashValue case let .itemCollectionInfo(id): return id.hashValue case let .orderedItemList(id): @@ -96,6 +99,12 @@ public enum PostboxViewKey: Hashable { } else { return false } + case let .peerGroupState(groupId): + if case .peerGroupState(groupId) = rhs { + return true + } else { + return false + } case let .orderedItemList(id): if case .orderedItemList(id) = rhs { return true @@ -212,6 +221,8 @@ func postboxViewForKey(postbox: Postbox, key: PostboxViewKey) -> MutablePostboxV return MutableItemCollectionInfoView(postbox: postbox, id: id) case let .peerChatState(peerId): return MutablePeerChatStateView(postbox: postbox, peerId: peerId) + case let .peerGroupState(groupId): + return MutablePeerGroupStateView(postbox: postbox, groupId: groupId) case let .orderedItemList(id): return MutableOrderedItemListView(postbox: postbox, collectionId: id) case .accessChallengeData: