From 93b8ccac9d6ec7c7506e7a40787a3e5e17e0ba1a Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 18 Apr 2016 01:03:09 +0300 Subject: [PATCH] no message --- Postbox.xcodeproj/project.pbxproj | 147 ++++++++--- Postbox/ChatListTable.swift | 4 +- Postbox/ChatListView.swift | 40 ++- Postbox/InvalidatedPeerReadStatesView.swift | 30 +++ Postbox/LmdbValueBox.swift | 2 +- Postbox/MediaBox.swift | 50 +++- Postbox/Message.swift | 1 - Postbox/MessageHistoryIndexTable.swift | 71 ++++- ...sageHistoryInvalidatedReadStateTable.swift | 66 +++++ Postbox/MessageHistoryReadStateTable.swift | 242 ++++++++++++++++++ Postbox/MessageHistoryTable.swift | 95 ++++++- Postbox/MessageHistoryView.swift | 25 +- Postbox/PeerChatStateTable.swift | 2 +- Postbox/PeerReadState.swift | 20 ++ Postbox/Postbox.swift | 77 ++++-- Postbox/SqliteValueBox.swift | 44 +++- Postbox/ValueBox.swift | 2 +- Postbox/ViewTracker.swift | 44 +++- PostboxTests/ChatListTableTests.swift | 6 +- PostboxTests/MessageHistoryTableTests.swift | 6 +- 20 files changed, 859 insertions(+), 115 deletions(-) create mode 100644 Postbox/InvalidatedPeerReadStatesView.swift create mode 100644 Postbox/MessageHistoryInvalidatedReadStateTable.swift create mode 100644 Postbox/MessageHistoryReadStateTable.swift create mode 100644 Postbox/PeerReadState.swift diff --git a/Postbox.xcodeproj/project.pbxproj b/Postbox.xcodeproj/project.pbxproj index 56baf99ef5..751e452158 100644 --- a/Postbox.xcodeproj/project.pbxproj +++ b/Postbox.xcodeproj/project.pbxproj @@ -8,13 +8,14 @@ /* Begin PBXBuildFile section */ D003E4E61B38DBDB00C22CBC /* MessageHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D003E4E51B38DBDB00C22CBC /* MessageHistoryView.swift */; }; - D00E0FB31B84CEDA002E4EB5 /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D00E0FB21B84CEDA002E4EB5 /* Display.framework */; }; D00E0FB81B85D192002E4EB5 /* lmdb.h in Headers */ = {isa = PBXBuildFile; fileRef = D00E0FB41B85D192002E4EB5 /* lmdb.h */; }; - D00E0FB91B85D192002E4EB5 /* mdb.c in Sources */ = {isa = PBXBuildFile; fileRef = D00E0FB51B85D192002E4EB5 /* mdb.c */; }; - D00E0FBA1B85D192002E4EB5 /* midl.c in Sources */ = {isa = PBXBuildFile; fileRef = D00E0FB61B85D192002E4EB5 /* midl.c */; }; + D00E0FB91B85D192002E4EB5 /* mdb.c in Sources */ = {isa = PBXBuildFile; fileRef = D00E0FB51B85D192002E4EB5 /* mdb.c */; settings = {COMPILER_FLAGS = "-Wno-conversion -Wno-unreachable-code -Wno-conditional-uninitialized -Wno-format-extra-args -Wno-macro-redefined"; }; }; + D00E0FBA1B85D192002E4EB5 /* midl.c in Sources */ = {isa = PBXBuildFile; fileRef = D00E0FB61B85D192002E4EB5 /* midl.c */; settings = {COMPILER_FLAGS = "-Wno-conversion"; }; }; D00E0FBB1B85D192002E4EB5 /* midl.h in Headers */ = {isa = PBXBuildFile; fileRef = D00E0FB71B85D192002E4EB5 /* midl.h */; }; D00E0FBE1B85D1B5002E4EB5 /* LmdbValueBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00E0FBD1B85D1B5002E4EB5 /* LmdbValueBox.swift */; }; D00EED1E1C81F28D00341DFF /* MessageHistoryTagsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00EED1D1C81F28D00341DFF /* MessageHistoryTagsTable.swift */; }; + D01F7D9B1CBEC390008765C9 /* MessageHistoryInvalidatedReadStateTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F7D9A1CBEC390008765C9 /* MessageHistoryInvalidatedReadStateTable.swift */; }; + D01F7D9D1CBF8586008765C9 /* InvalidatedPeerReadStatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F7D9C1CBF8586008765C9 /* InvalidatedPeerReadStatesView.swift */; }; D033A6F71C73D512006A2EAB /* MessageHistoryUnsentTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033A6F61C73D512006A2EAB /* MessageHistoryUnsentTable.swift */; }; D033A6F91C73E440006A2EAB /* UnsentMessageHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D033A6F81C73E440006A2EAB /* UnsentMessageHistoryView.swift */; }; D03BCCF81C73561C0097A291 /* Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03BCCF71C73561C0097A291 /* Table.swift */; }; @@ -26,14 +27,10 @@ D044E1641B2AD718001EE087 /* MurMurHash32.h in Headers */ = {isa = PBXBuildFile; fileRef = D044E1611B2AD667001EE087 /* MurMurHash32.h */; settings = {ATTRIBUTES = (Public, ); }; }; D055BD331B7D3D2D00F06C0A /* MediaBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D055BD321B7D3D2D00F06C0A /* MediaBox.swift */; }; D05F09A61C9E9F9300BB6F96 /* MediaResourceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05F09A51C9E9F9300BB6F96 /* MediaResourceStatus.swift */; }; - D07516441B2D9CEF00AE42E0 /* sqlite3.c in Sources */ = {isa = PBXBuildFile; fileRef = D07516401B2D9CEF00AE42E0 /* sqlite3.c */; }; + D07516441B2D9CEF00AE42E0 /* sqlite3.c in Sources */ = {isa = PBXBuildFile; fileRef = D07516401B2D9CEF00AE42E0 /* sqlite3.c */; settings = {COMPILER_FLAGS = "-Wno-conversion -Wno-ambiguous-macro -Wno-conditional-uninitialized -Wno-unused-const-variable -Wno-unused-function -Wno-unreachable-code"; }; }; D07516451B2D9CEF00AE42E0 /* sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = D07516411B2D9CEF00AE42E0 /* sqlite3.h */; }; D07516461B2D9CEF00AE42E0 /* sqlite3ext.h in Headers */ = {isa = PBXBuildFile; fileRef = D07516421B2D9CEF00AE42E0 /* sqlite3ext.h */; }; - D075165C1B2EC5B000AE42E0 /* module.private.modulemap in Sources */ = {isa = PBXBuildFile; fileRef = D075165B1B2EC5B000AE42E0 /* module.private.modulemap */; }; - D075165E1B2EC5B500AE42E0 /* module.modulemap in Sources */ = {isa = PBXBuildFile; fileRef = D075165D1B2EC5B500AE42E0 /* module.modulemap */; }; D075166A1B2EC7FE00AE42E0 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07516611B2EC7FE00AE42E0 /* Database.swift */; }; - D07516711B2EC7FE00AE42E0 /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07516681B2EC7FE00AE42E0 /* Statement.swift */; }; - D07516721B2EC7FE00AE42E0 /* Value.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07516691B2EC7FE00AE42E0 /* Value.swift */; }; D07516771B2EC90400AE42E0 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = D07516741B2EC90400AE42E0 /* fts3_tokenizer.h */; }; D07516781B2EC90400AE42E0 /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = D07516751B2EC90400AE42E0 /* SQLite-Bridging.h */; }; D07516791B2EC90400AE42E0 /* SQLite-Bridging.m in Sources */ = {isa = PBXBuildFile; fileRef = D07516761B2EC90400AE42E0 /* SQLite-Bridging.m */; }; @@ -46,11 +43,12 @@ D0A7D9451C556CFE0016A115 /* MessageHistoryIndexTableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A7D9441C556CFE0016A115 /* MessageHistoryIndexTableTests.swift */; }; D0B76BE71B66639F0095CF45 /* DeferredString.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B76BE61B66639F0095CF45 /* DeferredString.swift */; }; D0C07F6A1B67DB4800966E43 /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C07F691B67DB4800966E43 /* SwiftSignalKit.framework */; }; + D0C674C81CBB11C600183765 /* MessageHistoryReadStateTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C674C71CBB11C600183765 /* MessageHistoryReadStateTable.swift */; }; + D0C674CC1CBB14A700183765 /* PeerReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C674CB1CBB14A700183765 /* PeerReadState.swift */; }; D0C735281C864DF300BB3149 /* PeerChatStateTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C735271C864DF300BB3149 /* PeerChatStateTable.swift */; }; D0C8FCB71C5C2D200028C27F /* MessageHistoryViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C8FCB61C5C2D200028C27F /* MessageHistoryViewTests.swift */; }; D0C9DA391C65782500855278 /* SimpleSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C9DA381C65782500855278 /* SimpleSet.swift */; }; D0CE63F61CA1CCB2002BC462 /* MediaResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CE63F51CA1CCB2002BC462 /* MediaResource.swift */; }; - D0D224F21B4D6ABD0085E26D /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D224ED1B4D6ABD0085E26D /* Functions.swift */; }; D0E1DE151C5E1C6900C7826E /* ViewTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1DE141C5E1C6900C7826E /* ViewTracker.swift */; }; D0E3A7501B28A7E300A402D9 /* Postbox.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E3A74F1B28A7E300A402D9 /* Postbox.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0E3A7561B28A7E300A402D9 /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E3A74A1B28A7E300A402D9 /* Postbox.framework */; }; @@ -86,13 +84,14 @@ /* Begin PBXFileReference section */ D003E4E51B38DBDB00C22CBC /* MessageHistoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryView.swift; sourceTree = ""; }; - D00E0FB21B84CEDA002E4EB5 /* Display.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Display.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-gbpsmqzuwcmmxadrqcwyrluaftwp/Build/Products/Debug-iphoneos/Display.framework"; sourceTree = ""; }; D00E0FB41B85D192002E4EB5 /* lmdb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = lmdb.h; path = submodules/lmdb/libraries/liblmdb/lmdb.h; sourceTree = SOURCE_ROOT; }; D00E0FB51B85D192002E4EB5 /* mdb.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = mdb.c; path = submodules/lmdb/libraries/liblmdb/mdb.c; sourceTree = SOURCE_ROOT; }; D00E0FB61B85D192002E4EB5 /* midl.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = midl.c; path = submodules/lmdb/libraries/liblmdb/midl.c; sourceTree = SOURCE_ROOT; }; D00E0FB71B85D192002E4EB5 /* midl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = midl.h; path = submodules/lmdb/libraries/liblmdb/midl.h; sourceTree = SOURCE_ROOT; }; D00E0FBD1B85D1B5002E4EB5 /* LmdbValueBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LmdbValueBox.swift; sourceTree = ""; }; D00EED1D1C81F28D00341DFF /* MessageHistoryTagsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryTagsTable.swift; sourceTree = ""; }; + D01F7D9A1CBEC390008765C9 /* MessageHistoryInvalidatedReadStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryInvalidatedReadStateTable.swift; sourceTree = ""; }; + D01F7D9C1CBF8586008765C9 /* InvalidatedPeerReadStatesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvalidatedPeerReadStatesView.swift; sourceTree = ""; }; D033A6F61C73D512006A2EAB /* MessageHistoryUnsentTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryUnsentTable.swift; sourceTree = ""; }; D033A6F81C73E440006A2EAB /* UnsentMessageHistoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsentMessageHistoryView.swift; sourceTree = ""; }; D03BCCF71C73561C0097A291 /* Table.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Table.swift; sourceTree = ""; }; @@ -108,11 +107,7 @@ D07516411B2D9CEF00AE42E0 /* sqlite3.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sqlite3.h; sourceTree = ""; }; D07516421B2D9CEF00AE42E0 /* sqlite3ext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sqlite3ext.h; sourceTree = ""; }; D07516491B2D9E2500AE42E0 /* Postbox.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Postbox.xcconfig; path = Postbox/Config/Postbox.xcconfig; sourceTree = ""; }; - D075165B1B2EC5B000AE42E0 /* module.private.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = module.private.modulemap; sourceTree = ""; }; - D075165D1B2EC5B500AE42E0 /* module.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D07516611B2EC7FE00AE42E0 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Database.swift; path = submodules/sqlite.swift/SQLite/Database.swift; sourceTree = SOURCE_ROOT; }; - D07516681B2EC7FE00AE42E0 /* Statement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Statement.swift; path = submodules/sqlite.swift/SQLite/Statement.swift; sourceTree = SOURCE_ROOT; }; - D07516691B2EC7FE00AE42E0 /* Value.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Value.swift; path = submodules/sqlite.swift/SQLite/Value.swift; sourceTree = SOURCE_ROOT; }; D07516741B2EC90400AE42E0 /* fts3_tokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fts3_tokenizer.h; sourceTree = ""; }; D07516751B2EC90400AE42E0 /* SQLite-Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SQLite-Bridging.h"; sourceTree = ""; }; D07516761B2EC90400AE42E0 /* SQLite-Bridging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SQLite-Bridging.m"; sourceTree = ""; }; @@ -125,11 +120,12 @@ D0A7D9441C556CFE0016A115 /* MessageHistoryIndexTableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryIndexTableTests.swift; sourceTree = ""; }; D0B76BE61B66639F0095CF45 /* DeferredString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeferredString.swift; sourceTree = ""; }; D0C07F691B67DB4800966E43 /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSignalKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-gbpsmqzuwcmmxadrqcwyrluaftwp/Build/Products/Debug-iphoneos/SwiftSignalKit.framework"; sourceTree = ""; }; + D0C674C71CBB11C600183765 /* MessageHistoryReadStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryReadStateTable.swift; sourceTree = ""; }; + D0C674CB1CBB14A700183765 /* PeerReadState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerReadState.swift; sourceTree = ""; }; D0C735271C864DF300BB3149 /* PeerChatStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerChatStateTable.swift; sourceTree = ""; }; D0C8FCB61C5C2D200028C27F /* MessageHistoryViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryViewTests.swift; sourceTree = ""; }; D0C9DA381C65782500855278 /* SimpleSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleSet.swift; sourceTree = ""; }; D0CE63F51CA1CCB2002BC462 /* MediaResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaResource.swift; sourceTree = ""; }; - D0D224ED1B4D6ABD0085E26D /* Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Functions.swift; path = submodules/sqlite.swift/SQLite/Functions.swift; sourceTree = SOURCE_ROOT; }; D0E1DE141C5E1C6900C7826E /* ViewTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTracker.swift; sourceTree = ""; }; D0E3A74A1B28A7E300A402D9 /* Postbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D0E3A74E1B28A7E300A402D9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -161,7 +157,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D00E0FB31B84CEDA002E4EB5 /* Display.framework in Frameworks */, D0C07F6A1B67DB4800966E43 /* SwiftSignalKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -211,7 +206,6 @@ D075163D1B2D9CEF00AE42E0 /* PostboxPrivate */ = { isa = PBXGroup; children = ( - D075165D1B2EC5B500AE42E0 /* module.modulemap */, D075163F1B2D9CEF00AE42E0 /* sqlcipher */, ); path = PostboxPrivate; @@ -233,10 +227,7 @@ D07516731B2EC8C700AE42E0 /* sqlite.swift */ = { isa = PBXGroup; children = ( - D0D224ED1B4D6ABD0085E26D /* Functions.swift */, D07516611B2EC7FE00AE42E0 /* Database.swift */, - D07516681B2EC7FE00AE42E0 /* Statement.swift */, - D07516691B2EC7FE00AE42E0 /* Value.swift */, ); name = sqlite.swift; sourceTree = ""; @@ -259,6 +250,8 @@ D0F9E86E1C5A0E7600037222 /* KeychainTable.swift */, D0F9E8701C5A0E9B00037222 /* PeerTable.swift */, D0C735271C864DF300BB3149 /* PeerChatStateTable.swift */, + D0C674C71CBB11C600183765 /* MessageHistoryReadStateTable.swift */, + D01F7D9A1CBEC390008765C9 /* MessageHistoryInvalidatedReadStateTable.swift */, ); name = Tables; sourceTree = ""; @@ -271,6 +264,7 @@ D0E3A7A11B28B7DC00A402D9 /* Media.swift */, D08C71391C501F0700779C0F /* MessageHistoryHole.swift */, D044CA2D1C618373002160FF /* ChatListHole.swift */, + D0C674CB1CBB14A700183765 /* PeerReadState.swift */, ); name = Objects; sourceTree = ""; @@ -303,6 +297,7 @@ D003E4E51B38DBDB00C22CBC /* MessageHistoryView.swift */, D0F9E86A1C59719800037222 /* ChatListView.swift */, D033A6F81C73E440006A2EAB /* UnsentMessageHistoryView.swift */, + D01F7D9C1CBF8586008765C9 /* InvalidatedPeerReadStatesView.swift */, ); name = Views; sourceTree = ""; @@ -310,7 +305,6 @@ D0E3A7401B28A7E300A402D9 = { isa = PBXGroup; children = ( - D00E0FB21B84CEDA002E4EB5 /* Display.framework */, D0C07F691B67DB4800966E43 /* SwiftSignalKit.framework */, D07516491B2D9E2500AE42E0 /* Postbox.xcconfig */, D0E3A74C1B28A7E300A402D9 /* Postbox */, @@ -331,7 +325,6 @@ D0E3A74C1B28A7E300A402D9 /* Postbox */ = { isa = PBXGroup; children = ( - D075165B1B2EC5B000AE42E0 /* module.private.modulemap */, D075163D1B2D9CEF00AE42E0 /* PostboxPrivate */, D07515FC1B2C44A200AE42E0 /* thirdparty */, D0E1DE181C5EB09300C7826E /* Utils */, @@ -497,16 +490,16 @@ buildActionMask = 2147483647; files = ( D0F9E8631C579F0200037222 /* MediaCleanupTable.swift in Sources */, - D075165C1B2EC5B000AE42E0 /* module.private.modulemap in Sources */, D0F9E86F1C5A0E7600037222 /* KeychainTable.swift in Sources */, D0E3A7821B28ADD000A402D9 /* Postbox.swift in Sources */, D0E3A79E1B28B50400A402D9 /* Message.swift in Sources */, + D0C674C81CBB11C600183765 /* MessageHistoryReadStateTable.swift in Sources */, D044CA2E1C618373002160FF /* ChatListHole.swift in Sources */, D044E1631B2AD677001EE087 /* MurMurHash32.m in Sources */, D00E0FBE1B85D1B5002E4EB5 /* LmdbValueBox.swift in Sources */, - D07516721B2EC7FE00AE42E0 /* Value.swift in Sources */, D08C713A1C501F0700779C0F /* MessageHistoryHole.swift in Sources */, D044CA2A1C617D39002160FF /* SeedConfiguration.swift in Sources */, + D01F7D9D1CBF8586008765C9 /* InvalidatedPeerReadStatesView.swift in Sources */, D0F9E85B1C565EBB00037222 /* MessageMediaTable.swift in Sources */, D0E1DE151C5E1C6900C7826E /* ViewTracker.swift in Sources */, D0F9E8751C5A334100037222 /* SimpleDictionary.swift in Sources */, @@ -521,16 +514,13 @@ D0CE63F61CA1CCB2002BC462 /* MediaResource.swift in Sources */, D0F9E86B1C59719800037222 /* ChatListView.swift in Sources */, D003E4E61B38DBDB00C22CBC /* MessageHistoryView.swift in Sources */, - D075165E1B2EC5B500AE42E0 /* module.modulemap in Sources */, D07516791B2EC90400AE42E0 /* SQLite-Bridging.m in Sources */, D0977FA01B8244D7009994B2 /* SqliteValueBox.swift in Sources */, - D0D224F21B4D6ABD0085E26D /* Functions.swift in Sources */, D00E0FBA1B85D192002E4EB5 /* midl.c in Sources */, D0F9E8651C58CB7F00037222 /* ChatListTable.swift in Sources */, D0F9E8711C5A0E9B00037222 /* PeerTable.swift in Sources */, D05F09A61C9E9F9300BB6F96 /* MediaResourceStatus.swift in Sources */, D033A6F71C73D512006A2EAB /* MessageHistoryUnsentTable.swift in Sources */, - D07516711B2EC7FE00AE42E0 /* Statement.swift in Sources */, D08C713C1C51283C00779C0F /* MessageHistoryIndexTable.swift in Sources */, D0FC1A3C1CA3FB310056AE9A /* MediaResourceData.swift in Sources */, D0F9E86D1C5A0E5D00037222 /* MetadataTable.swift in Sources */, @@ -540,7 +530,9 @@ D044CA2C1C617E2D002160FF /* MessageHistoryMetadataTable.swift in Sources */, D03BCCF81C73561C0097A291 /* Table.swift in Sources */, D0C735281C864DF300BB3149 /* PeerChatStateTable.swift in Sources */, + D01F7D9B1CBEC390008765C9 /* MessageHistoryInvalidatedReadStateTable.swift in Sources */, D055BD331B7D3D2D00F06C0A /* MediaBox.swift in Sources */, + D0C674CC1CBB14A700183765 /* PeerReadState.swift in Sources */, D0977F9C1B822DB4009994B2 /* ValueBox.swift in Sources */, D0B76BE71B66639F0095CF45 /* DeferredString.swift in Sources */, D075166A1B2EC7FE00AE42E0 /* Database.swift in Sources */, @@ -571,6 +563,90 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + D086A5711CC0116A00F08284 /* Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D07516491B2D9E2500AE42E0 /* Postbox.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Hockeyapp; + }; + D086A5721CC0116A00F08284 /* Hockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = Postbox/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_CFLAGS = ( + "-DSQLITE_TEMP_STORE=2", + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS3", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + }; + name = Hockeyapp; + }; + D086A5731CC0116A00F08284 /* Hockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "/Users/peter/Documents/PostBoxTest/submodules/SSignalKit/build/Debug-iphoneos", + ); + INFOPLIST_FILE = PostboxTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Hockeyapp; + }; D0E3A75E1B28A7E300A402D9 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = D07516491B2D9E2500AE42E0 /* Postbox.xcconfig */; @@ -672,11 +748,8 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "/Users/peter/Documents/PostBoxTest/submodules/SSignalKit/build/Debug-iphoneos", - ); + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Postbox/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -702,11 +775,8 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "/Users/peter/Documents/PostBoxTest/submodules/SSignalKit/build/Debug-iphoneos", - ); + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Postbox/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; @@ -767,6 +837,7 @@ buildConfigurations = ( D0E3A75E1B28A7E300A402D9 /* Debug */, D0E3A75F1B28A7E300A402D9 /* Release */, + D086A5711CC0116A00F08284 /* Hockeyapp */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -776,6 +847,7 @@ buildConfigurations = ( D0E3A7611B28A7E300A402D9 /* Debug */, D0E3A7621B28A7E300A402D9 /* Release */, + D086A5721CC0116A00F08284 /* Hockeyapp */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -785,6 +857,7 @@ buildConfigurations = ( D0E3A7641B28A7E300A402D9 /* Debug */, D0E3A7651B28A7E300A402D9 /* Release */, + D086A5731CC0116A00F08284 /* Hockeyapp */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Postbox/ChatListTable.swift b/Postbox/ChatListTable.swift index 8267731e8b..578a2d6079 100644 --- a/Postbox/ChatListTable.swift +++ b/Postbox/ChatListTable.swift @@ -1,7 +1,7 @@ import Foundation enum ChatListOperation { - case InsertMessage(IntermediateMessage) + case InsertMessage(IntermediateMessage, CombinedPeerReadState?) case InsertHole(ChatListHole) case InsertNothing(MessageIndex) case RemoveMessage([MessageIndex]) @@ -91,7 +91,7 @@ final class ChatListTable: Table { } self.indexTable.set(index) self.justInsertMessage(index) - operations.append(.InsertMessage(topMessage)) + operations.append(.InsertMessage(topMessage, messageHistoryTable.readStateTable.getCombinedState(peerId))) } else { if let currentIndex = currentIndex { operations.append(.RemoveMessage([currentIndex])) diff --git a/Postbox/ChatListView.swift b/Postbox/ChatListView.swift index ad76989417..45acfb3662 100644 --- a/Postbox/ChatListView.swift +++ b/Postbox/ChatListView.swift @@ -1,13 +1,13 @@ import Foundation public enum ChatListEntry: Comparable { - case MessageEntry(Message) + case MessageEntry(Message, Int) case HoleEntry(ChatListHole) case Nothing(MessageIndex) public var index: MessageIndex { switch self { - case let .MessageEntry(message): + case let .MessageEntry(message, _): return MessageIndex(message) case let .HoleEntry(hole): return hole.index @@ -18,7 +18,15 @@ public enum ChatListEntry: Comparable { } public func ==(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool { - return lhs.index == rhs.index + if lhs.index == rhs.index { + if case let .MessageEntry(_, lhsCount) = lhs, case let .MessageEntry(_, rhsCount) = rhs { + if lhsCount != rhsCount { + return false + } + } + return true + } + return false } public func <(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool { @@ -26,16 +34,16 @@ public func <(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool { } enum MutableChatListEntry: Equatable { - case IntermediateMessageEntry(IntermediateMessage) - case MessageEntry(Message) + case IntermediateMessageEntry(IntermediateMessage, CombinedPeerReadState?) + case MessageEntry(Message, CombinedPeerReadState?) case HoleEntry(ChatListHole) case Nothing(MessageIndex) var index: MessageIndex { switch self { - case let .IntermediateMessageEntry(message): + case let .IntermediateMessageEntry(message, _): return MessageIndex(id: message.id, timestamp: message.timestamp) - case let .MessageEntry(message): + case let .MessageEntry(message, _): return MessageIndex(message) case let .HoleEntry(hole): return hole.index @@ -109,8 +117,8 @@ final class MutableChatListView { var hasChanges = false for operation in operations { switch operation { - case let .InsertMessage(message): - if self.add(.IntermediateMessageEntry(message)) { + case let .InsertMessage(message, combinedReadState): + if self.add(.IntermediateMessageEntry(message, combinedReadState)) { hasChanges = true } case let .InsertNothing(index): @@ -368,8 +376,8 @@ final class MutableChatListView { func render(renderMessage: IntermediateMessage -> Message) { for i in 0 ..< self.entries.count { - if case let .IntermediateMessageEntry(message) = self.entries[i] { - self.entries[i] = .MessageEntry(renderMessage(message)) + if case let .IntermediateMessageEntry(message, combinedReadState) = self.entries[i] { + self.entries[i] = .MessageEntry(renderMessage(message), combinedReadState) } } } @@ -384,8 +392,14 @@ public final class ChatListView { var entries: [ChatListEntry] = [] for entry in mutableView.entries { switch entry { - case let .MessageEntry(message): - entries.append(.MessageEntry(message)) + case let .MessageEntry(message, combinedReadState): + var unreadCount: Int32 = 0 + if let combinedReadState = combinedReadState { + for (_, state) in combinedReadState.states { + unreadCount += state.count + } + } + entries.append(.MessageEntry(message, Int(unreadCount))) case let .Nothing(index): entries.append(.Nothing(index)) case let .HoleEntry(hole): diff --git a/Postbox/InvalidatedPeerReadStatesView.swift b/Postbox/InvalidatedPeerReadStatesView.swift new file mode 100644 index 0000000000..ee44dea37f --- /dev/null +++ b/Postbox/InvalidatedPeerReadStatesView.swift @@ -0,0 +1,30 @@ +import Foundation + +final class InvalidatedPeerReadStatesView { + var peerIds = Set() + + init(peerIds: [PeerId]) { + for peerId in peerIds { + self.peerIds.insert(peerId) + } + } + + func replay(operations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) -> Bool { + var updated = false + for operation in operations { + switch operation { + case let .Insert(peerId): + if !self.peerIds.contains(peerId) { + self.peerIds.insert(peerId) + updated = true + } + case let .Remove(peerId): + if let _ = self.peerIds.remove(peerId) { + updated = true + } + } + } + + return updated + } +} diff --git a/Postbox/LmdbValueBox.swift b/Postbox/LmdbValueBox.swift index 1922a4b9d9..ec578ff300 100644 --- a/Postbox/LmdbValueBox.swift +++ b/Postbox/LmdbValueBox.swift @@ -312,7 +312,7 @@ public final class LmdbValueBox: ValueBox { } } - public func range(table: Int32, start: ValueBoxKey, end: ValueBoxKey, keys: ValueBoxKey -> Bool, limit: Int) { + public func range(table: Int32, start: ValueBoxKey, end: ValueBoxKey, @noescape keys: ValueBoxKey -> Bool, limit: Int) { self.range(table, start: start, end: end, values: { key, _ in return keys(key) }, limit: limit) diff --git a/Postbox/MediaBox.swift b/Postbox/MediaBox.swift index 2e12b545c2..52addd013e 100644 --- a/Postbox/MediaBox.swift +++ b/Postbox/MediaBox.swift @@ -1,6 +1,5 @@ import Foundation import SwiftSignalKit -import Display private final class ResourceStatusContext { var status: MediaResourceStatus? @@ -9,7 +8,8 @@ private final class ResourceStatusContext { private final class ResourceDataContext { var data: MediaResourceData - let dataSubscribers = Bag Void>() + let progresiveDataSubscribers = Bag Void>() + let completeDataSubscribers = Bag Void>() var fetchDisposable: Disposable? let fetchSubscribers = Bag() @@ -126,7 +126,7 @@ public final class MediaBox { } } - public func resourceData(resource: MediaResource) -> Signal { + public func resourceData(resource: MediaResource, complete: Bool = true) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() @@ -146,21 +146,39 @@ public final class MediaBox { self.dataContexts[resource.id] = dataContext } - let index = dataContext.dataSubscribers.add { data in - subscriber.putNext(data) - if data.size >= resource.size { - subscriber.putCompletion() + let index: Bag Void>.Index + if complete { + index = dataContext.completeDataSubscribers.add { data in + subscriber.putNext(data) + if data.size >= resource.size { + subscriber.putCompletion() + } } + if dataContext.data.size >= resource.size { + subscriber.putNext(dataContext.data) + } else { + subscriber.putNext(MediaResourceData(path: dataContext.data.path, size: 0)) + } + } else { + index = dataContext.progresiveDataSubscribers.add { data in + subscriber.putNext(data) + if data.size >= resource.size { + subscriber.putCompletion() + } + } + subscriber.putNext(dataContext.data) } - subscriber.putNext(dataContext.data) - disposable.set(ActionDisposable { self.dataQueue.dispatch { if let dataContext = self.dataContexts[resource.id] { - dataContext.dataSubscribers.remove(index) + if complete { + dataContext.completeDataSubscribers.remove(index) + } else { + dataContext.progresiveDataSubscribers.remove(index) + } - if dataContext.dataSubscribers.isEmpty && dataContext.fetchSubscribers.isEmpty { + if dataContext.progresiveDataSubscribers.isEmpty && dataContext.completeDataSubscribers.isEmpty && dataContext.fetchSubscribers.isEmpty { self.dataContexts.removeValueForKey(resource.id) } } @@ -230,10 +248,16 @@ public final class MediaBox { offset += data.length let updatedSize = offset - for subscriber in dataContext.dataSubscribers.copyItems() { + for subscriber in dataContext.progresiveDataSubscribers.copyItems() { subscriber(MediaResourceData(path: path, size: updatedSize)) } + if updatedSize >= resource.size { + for subscriber in dataContext.completeDataSubscribers.copyItems() { + subscriber(MediaResourceData(path: path, size: updatedSize)) + } + } + let status: MediaResourceStatus if updatedSize >= resource.size { status = .Local @@ -281,7 +305,7 @@ public final class MediaBox { } } - if dataContext.dataSubscribers.isEmpty && dataContext.fetchSubscribers.isEmpty { + if dataContext.completeDataSubscribers.isEmpty && dataContext.progresiveDataSubscribers.isEmpty && dataContext.fetchSubscribers.isEmpty { self.dataContexts.removeValueForKey(resource.id) } } diff --git a/Postbox/Message.swift b/Postbox/Message.swift index 6862f00a30..c3745d7b00 100644 --- a/Postbox/Message.swift +++ b/Postbox/Message.swift @@ -27,7 +27,6 @@ public struct MessageId: Hashable, Comparable, CustomStringConvertible { } public init(_ buffer: ReadBuffer) { - var peerIdNamespaceValue: Int32 = 0 memcpy(&peerIdNamespaceValue, buffer.memory + buffer.offset, 4) var peerIdIdValue: Int32 = 0 diff --git a/Postbox/MessageHistoryIndexTable.swift b/Postbox/MessageHistoryIndexTable.swift index c3b467e79a..c7e4b030a8 100644 --- a/Postbox/MessageHistoryIndexTable.swift +++ b/Postbox/MessageHistoryIndexTable.swift @@ -32,14 +32,19 @@ enum MessageHistoryIndexOperation { case Update(MessageIndex, InternalStoreMessage) } +private let HistoryEntryTypeMask: Int8 = 1 +private let HistoryEntryTypeMessage: Int8 = 0 +private let HistoryEntryTypeHole: Int8 = 1 +private let HistoryEntryMessageFlagIncoming: Int8 = 1 << 1 + private func readHistoryIndexEntry(peerId: PeerId, namespace: MessageId.Namespace, key: ValueBoxKey, value: ReadBuffer) -> HistoryIndexEntry { - var type: Int8 = 0 - value.read(&type, offset: 0, length: 1) + var flags: Int8 = 0 + value.read(&flags, offset: 0, length: 1) var timestamp: Int32 = 0 value.read(×tamp, offset: 0, length: 4) let index = MessageIndex(id: MessageId(peerId: peerId, namespace: namespace, id: key.getInt32(8 + 4)), timestamp: timestamp) - if type == 0 { + if (flags & HistoryEntryTypeMask) == 0 { return .Message(index) } else { var stableId: UInt32 = 0 @@ -462,11 +467,11 @@ final class MessageHistoryIndexTable: Table { private func justInsertHole(hole: MessageHistoryHole, inout operations: [MessageHistoryIndexOperation]) { let value = WriteBuffer() - var type: Int8 = 1 + var flags: Int8 = HistoryEntryTypeHole var timestamp: Int32 = hole.maxIndex.timestamp var min: Int32 = hole.min var tags: UInt32 = hole.tags - value.write(&type, offset: 0, length: 1) + value.write(&flags, offset: 0, length: 1) value.write(×tamp, offset: 0, length: 4) var stableId: UInt32 = hole.stableId value.write(&stableId, offset: 0, length: 4) @@ -481,9 +486,12 @@ final class MessageHistoryIndexTable: Table { let index = MessageIndex(id: message.id, timestamp: message.timestamp) let value = WriteBuffer() - var type: Int8 = 0 + var flags: Int8 = HistoryEntryTypeMessage + if message.flags.contains(.Incoming) { + flags |= HistoryEntryMessageFlagIncoming + } var timestamp: Int32 = index.timestamp - value.write(&type, offset: 0, length: 1) + value.write(&flags, offset: 0, length: 1) value.write(×tamp, offset: 0, length: 4) self.valueBox.set(self.tableId, key: self.key(index.id), value: value) @@ -535,6 +543,55 @@ final class MessageHistoryIndexTable: Table { return self.valueBox.exists(self.tableId, key: self.key(id)) } + func incomingMessageCountInRange(peerId: PeerId, namespace: MessageId.Namespace, minId: MessageId.Id, maxId: MessageId.Id) -> (Int, Bool) { + var count = 0 + var holes = false + + self.valueBox.range(self.tableId, start: self.key(MessageId(peerId: peerId, namespace: namespace, id: minId)).predecessor, end: self.key(MessageId(peerId: peerId, namespace: namespace, id: maxId)).successor, values: { _, value in + var flags: Int8 = 0 + value.read(&flags, offset: 0, length: 1) + if (flags & HistoryEntryTypeMask) == HistoryEntryTypeMessage { + if (flags & HistoryEntryMessageFlagIncoming) != 0 { + count += 1 + } + } else { + holes = true + } + return true + }, limit: 0) + + return (count, holes) + } + + func incomingMessageCountInIds(peerId: PeerId, namespace: MessageId.Namespace, ids: [MessageId.Id]) -> (Int, Bool) { + var count = 0 + var holes = false + + for id in ids { + self.valueBox.range(self.tableId, start: self.key(MessageId(peerId: peerId, namespace: namespace, id: id)).predecessor, end: self.upperBound(peerId, namespace: namespace), values: { key, value in + let entryId = key.getInt32(8 + 4) + var flags: Int8 = 0 + value.read(&flags, offset: 0, length: 1) + + if entryId == id { + if (flags & HistoryEntryTypeMask) == HistoryEntryTypeMessage { + if (flags & HistoryEntryMessageFlagIncoming) != 0 { + count += 1 + } + } else { + holes = true + } + } else if (flags & HistoryEntryTypeMask) == HistoryEntryTypeHole { + holes = true + } + + return true + }, limit: 1) + } + + return (count, holes) + } + func debugList(peerId: PeerId, namespace: MessageId.Namespace) -> [HistoryIndexEntry] { var list: [HistoryIndexEntry] = [] self.valueBox.range(self.tableId, start: self.lowerBound(peerId, namespace: namespace), end: self.upperBound(peerId, namespace: namespace), values: { key, value in diff --git a/Postbox/MessageHistoryInvalidatedReadStateTable.swift b/Postbox/MessageHistoryInvalidatedReadStateTable.swift new file mode 100644 index 0000000000..cd5b1be7a1 --- /dev/null +++ b/Postbox/MessageHistoryInvalidatedReadStateTable.swift @@ -0,0 +1,66 @@ +import Foundation + +enum IntermediateMessageHistoryInvalidatedReadStateOperation { + case Insert(PeerId) + case Remove(PeerId) +} + +final class MessageHistoryInvalidatedReadStateTable: Table { + private let sharedKey = ValueBoxKey(length: 8) + + private func key(peerId: PeerId) -> ValueBoxKey { + self.sharedKey.setInt64(0, value: peerId.toInt64()) + return self.sharedKey + } + + private var updatedPeerIds: [PeerId: Bool] = [:] + + private func lowerBound() -> ValueBoxKey { + let key = ValueBoxKey(length: 1) + key.setInt8(0, value: 0) + return key + } + + private func upperBound() -> ValueBoxKey { + let key = ValueBoxKey(length: 8) + memset(key.memory, 0xff, key.length) + return key + } + + func add(peerId: PeerId, inout operations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + if !self.valueBox.exists(self.tableId, key: self.key(peerId)) { + self.valueBox.set(self.tableId, key: self.key(peerId), value: MemoryBuffer()) + operations.append(.Insert(peerId)) + } + } + + func remove(peerId: PeerId, inout operations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + if self.valueBox.exists(self.tableId, key: self.key(peerId)) { + self.valueBox.remove(self.tableId, key: self.key(peerId)) + operations.append(.Remove(peerId)) + } + } + + func get() -> [PeerId] { + var peerIds: [PeerId] = [] + self.valueBox.range(self.tableId, start: self.lowerBound(), end: self.upperBound(), keys: { key in + peerIds.append(PeerId(key.getInt64(0))) + return true + }, limit: 0) + return peerIds + } + + override func beforeCommit() { + let key = ValueBoxKey(length: 8) + let buffer = MemoryBuffer() + for (peerId, invalidated) in self.updatedPeerIds { + key.setInt64(0, value: peerId.toInt64()) + if invalidated { + self.valueBox.set(self.tableId, key: key, value: buffer) + } else { + self.valueBox.remove(self.tableId, key: key) + } + } + self.updatedPeerIds.removeAll() + } +} diff --git a/Postbox/MessageHistoryReadStateTable.swift b/Postbox/MessageHistoryReadStateTable.swift new file mode 100644 index 0000000000..3793491890 --- /dev/null +++ b/Postbox/MessageHistoryReadStateTable.swift @@ -0,0 +1,242 @@ +import Foundation + +private final class InternalPeerReadStates { + var namespaces: [MessageId.Namespace: PeerReadState] + + init(namespaces: [MessageId.Namespace: PeerReadState]) { + self.namespaces = namespaces + } +} + +final class MessageHistoryReadStateTable: Table { + private var cachedPeerReadStates: [PeerId: InternalPeerReadStates?] = [:] + private var updatedPeerIds = Set() + + private let sharedKey = ValueBoxKey(length: 8) + + private func key(id: PeerId) -> ValueBoxKey { + self.sharedKey.setInt64(0, value: id.toInt64()) + return self.sharedKey + } + + override init(valueBox: ValueBox, tableId: Int32) { + super.init(valueBox: valueBox, tableId: tableId) + } + + private func get(id: PeerId) -> InternalPeerReadStates? { + if let states = self.cachedPeerReadStates[id] { + return states + } else { + if let value = self.valueBox.get(self.tableId, key: self.key(id)) { + var count: Int32 = 0 + value.read(&count, offset: 0, length: 4) + var stateByNamespace: [MessageId.Namespace: PeerReadState] = [:] + for _ in 0 ..< count { + var namespaceId: Int32 = 0 + var maxReadId: Int32 = 0 + var maxKnownId: Int32 = 0 + var count: Int32 = 0 + value.read(&namespaceId, offset: 0, length: 4) + value.read(&maxReadId, offset: 0, length: 4) + value.read(&maxKnownId, offset: 0, length: 4) + value.read(&count, offset: 0, length: 4) + let state = PeerReadState(maxReadId: maxReadId, maxKnownId: maxKnownId, count: count) + stateByNamespace[namespaceId] = state + } + let states = InternalPeerReadStates(namespaces: stateByNamespace) + self.cachedPeerReadStates[id] = states + return states + } else { + self.cachedPeerReadStates[id] = nil + return nil + } + } + } + + func getCombinedState(peerId: PeerId) -> CombinedPeerReadState? { + if let states = self.get(peerId) { + return CombinedPeerReadState(states: states.namespaces.map({$0})) + } + return nil + } + + func resetStates(peerId: PeerId, namespaces: [MessageId.Namespace: PeerReadState]) -> CombinedPeerReadState? { + self.updatedPeerIds.insert(peerId) + + if let states = self.get(peerId) { + var updated = false + for (namespace, state) in namespaces { + if states.namespaces[namespace] == nil || states.namespaces[namespace]! != state { + updated = true + } + states.namespaces[namespace] = state + } + if updated { + self.updatedPeerIds.insert(peerId) + return CombinedPeerReadState(states: states.namespaces.map({$0})) + } else { + return nil + } + } else { + self.updatedPeerIds.insert(peerId) + let states = InternalPeerReadStates(namespaces: namespaces) + self.cachedPeerReadStates[peerId] = states + return CombinedPeerReadState(states: states.namespaces.map({$0})) + } + } + + func addIncomingMessages(peerId: PeerId, ids: [MessageId]) -> (CombinedPeerReadState?, Bool) { + var idsByNamespace: [MessageId.Namespace: [MessageId.Id]] = [:] + for id in ids { + if idsByNamespace[id.namespace] != nil { + idsByNamespace[id.namespace]!.append(id.id) + } else { + idsByNamespace[id.namespace] = [id.id] + } + } + + if let states = self.get(peerId) { + var updated = false + var invalidated = false + for (namespace, ids) in idsByNamespace { + if let currentState = states.namespaces[namespace] { + var addedUnreadCount: Int32 = 0 + var maxIncomingId: Int32 = 0 + for id in ids { + if id > currentState.maxKnownId { + addedUnreadCount += 1 + maxIncomingId = max(id, maxIncomingId) + } + } + + if addedUnreadCount != 0 { + states.namespaces[namespace] = PeerReadState(maxReadId: currentState.maxReadId, maxKnownId: currentState.maxKnownId, count: currentState.count + addedUnreadCount) + updated = true + } + } + } + + if updated { + self.updatedPeerIds.insert(peerId) + } + + return (updated ? CombinedPeerReadState(states: states.namespaces.map({$0})) : nil, invalidated) + } else { + return (nil, true) + } + + return (nil, false) + } + + func deleteMessages(peerId: PeerId, ids: [MessageId], incomingStatsInIds: (PeerId, MessageId.Namespace, [MessageId.Id]) -> (Int, Bool)) -> (CombinedPeerReadState?, Bool) { + var idsByNamespace: [MessageId.Namespace: [MessageId.Id]] = [:] + for id in ids { + if idsByNamespace[id.namespace] != nil { + idsByNamespace[id.namespace]!.append(id.id) + } else { + idsByNamespace[id.namespace] = [id.id] + } + } + + if let states = self.get(peerId) { + var updated = false + var invalidate = false + for (namespace, ids) in idsByNamespace { + if let currentState = states.namespaces[namespace] { + var unreadIds: [MessageId.Id] = [] + for id in ids { + if id > currentState.maxReadId { + unreadIds.append(id) + } + } + + let (knownCount, holes) = incomingStatsInIds(peerId, namespace, unreadIds) + if holes { + invalidate = true + } + + states.namespaces[namespace] = PeerReadState(maxReadId: currentState.maxReadId, maxKnownId: currentState.maxKnownId, count: currentState.count - knownCount) + updated = true + } else { + invalidate = true + } + } + + if updated { + self.updatedPeerIds.insert(peerId) + } + + return (updated ? CombinedPeerReadState(states: states.namespaces.map({$0})) : nil, invalidate) + } else { + return (nil, true) + } + + return (nil, false) + } + + func applyMaxReadId(peerId: PeerId, namespace: MessageId.Namespace, maxReadId: MessageId.Id, maxKnownId: MessageId.Id, incomingStatsInRange: (MessageId.Id, MessageId.Id) -> (count: Int, holes: Bool)) -> (CombinedPeerReadState?, Bool) { + if let states = self.get(peerId), state = states.namespaces[namespace] { + if state.maxReadId < maxReadId { + let (deltaCount, holes) = incomingStatsInRange(state.maxReadId + 1, maxReadId) + + states.namespaces[namespace] = PeerReadState(maxReadId: maxReadId, maxKnownId: max(state.maxKnownId, maxReadId), count: state.count - Int32(deltaCount)) + self.updatedPeerIds.insert(peerId) + return (CombinedPeerReadState(states: states.namespaces.map({$0})), holes) + } + } else { + return (nil, true) + } + + return (nil, false) + } + + func clearUnreadLocally(peerId: PeerId, topId: (PeerId, MessageId.Namespace) -> MessageId.Id?) -> CombinedPeerReadState? { + if let states = self.get(peerId) { + var updatedNamespaces: [MessageId.Namespace: PeerReadState] = [:] + var updated = false + for (namespace, state) in states.namespaces { + if let topMessageId = topId(peerId, namespace) { + let updatedState = PeerReadState(maxReadId: topMessageId, maxKnownId: topMessageId, count: 0) + if updatedState != state { + updated = true + } + updatedNamespaces[namespace] = updatedState + } else { + let updatedState = PeerReadState(maxReadId: state.maxReadId, maxKnownId: state.maxKnownId, count: 0) + updated = true + } + } + if updated { + self.updatedPeerIds.insert(peerId) + return CombinedPeerReadState(states: states.namespaces.map({$0})) + } + } + + return nil + } + + override func beforeCommit() { + let sharedBuffer = WriteBuffer() + for id in self.updatedPeerIds { + if let wrappedStates = self.cachedPeerReadStates[id], states = wrappedStates { + sharedBuffer.reset() + var count: Int32 = Int32(states.namespaces.count) + sharedBuffer.write(&count, offset: 0, length: 4) + for (namespace, state) in states.namespaces { + var namespaceId: Int32 = namespace + var maxReadId: Int32 = state.maxReadId + var maxKnownId: Int32 = state.maxKnownId + var count: Int32 = state.count + sharedBuffer.write(&namespaceId, offset: 0, length: 4) + sharedBuffer.write(&maxReadId, offset: 0, length: 4) + sharedBuffer.write(&maxKnownId, offset: 0, length: 4) + sharedBuffer.write(&count, offset: 0, length: 4) + } + self.valueBox.set(self.tableId, key: self.key(id), value: sharedBuffer) + } else { + self.valueBox.remove(self.tableId, key: self.key(id)) + } + } + self.updatedPeerIds.removeAll() + } +} diff --git a/Postbox/MessageHistoryTable.swift b/Postbox/MessageHistoryTable.swift index 08c74c737d..4cc285612c 100644 --- a/Postbox/MessageHistoryTable.swift +++ b/Postbox/MessageHistoryTable.swift @@ -4,6 +4,7 @@ enum MessageHistoryOperation { case InsertMessage(IntermediateMessage) case InsertHole(MessageHistoryHole) case Remove([MessageIndex]) + case UpdateReadState(CombinedPeerReadState) } enum IntermediateMessageHistoryEntry { @@ -40,13 +41,17 @@ final class MessageHistoryTable: Table { let historyMetadataTable: MessageHistoryMetadataTable let unsentTable: MessageHistoryUnsentTable let tagsTable: MessageHistoryTagsTable + let readStateTable: MessageHistoryReadStateTable + let invalidatedReadStateTable: MessageHistoryInvalidatedReadStateTable - init(valueBox: ValueBox, tableId: Int32, messageHistoryIndexTable: MessageHistoryIndexTable, messageMediaTable: MessageMediaTable, historyMetadataTable: MessageHistoryMetadataTable, unsentTable: MessageHistoryUnsentTable, tagsTable: MessageHistoryTagsTable) { + init(valueBox: ValueBox, tableId: Int32, messageHistoryIndexTable: MessageHistoryIndexTable, messageMediaTable: MessageMediaTable, historyMetadataTable: MessageHistoryMetadataTable, unsentTable: MessageHistoryUnsentTable, tagsTable: MessageHistoryTagsTable, readStateTable: MessageHistoryReadStateTable, invalidatedReadStateTable: MessageHistoryInvalidatedReadStateTable) { self.messageHistoryIndexTable = messageHistoryIndexTable self.messageMediaTable = messageMediaTable self.historyMetadataTable = historyMetadataTable self.unsentTable = unsentTable self.tagsTable = tagsTable + self.readStateTable = readStateTable + self.invalidatedReadStateTable = invalidatedReadStateTable super.init(valueBox: valueBox, tableId: tableId) } @@ -101,13 +106,15 @@ final class MessageHistoryTable: Table { return dict } - private func processIndexOperations(peerId: PeerId, operations: [MessageHistoryIndexOperation], inout processedOperationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation]) { + private func processIndexOperations(peerId: PeerId, operations: [MessageHistoryIndexOperation], inout processedOperationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { let sharedKey = self.key(MessageIndex(id: MessageId(peerId: PeerId(namespace: 0, id: 0), namespace: 0, id: 0), timestamp: 0)) let sharedBuffer = WriteBuffer() let sharedEncoder = Encoder() var outputOperations: [MessageHistoryOperation] = [] var accumulatedRemoveIndices: [MessageIndex] = [] + var addedIncomingMessageIds: [MessageId] = [] + var removedMessageIds: [MessageId] = [] for operation in operations { switch operation { case let .InsertHole(hole): @@ -154,6 +161,9 @@ final class MessageHistoryTable: Table { } } } + if message.flags.contains(.Incoming) { + addedIncomingMessageIds.append(message.id) + } case let .Remove(index): self.justRemove(index, unsentMessageOperations: &unsentMessageOperations) accumulatedRemoveIndices.append(index) @@ -172,6 +182,16 @@ final class MessageHistoryTable: Table { outputOperations.append(.Remove(accumulatedRemoveIndices)) } + if !addedIncomingMessageIds.isEmpty { + let (combinedState, invalidate) = self.readStateTable.addIncomingMessages(peerId, ids: addedIncomingMessageIds) + if let combinedState = combinedState { + outputOperations.append(.UpdateReadState(combinedState)) + } + if invalidate { + self.invalidatedReadStateTable.add(peerId, operations: &invalidatedReadStateOperations) + } + } + if processedOperationsByPeerId[peerId] == nil { processedOperationsByPeerId[peerId] = outputOperations } else { @@ -193,45 +213,96 @@ final class MessageHistoryTable: Table { return internalStoreMessages } - func addMessages(messages: [StoreMessage], location: AddMessagesLocation, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation]) { + func addMessages(messages: [StoreMessage], location: AddMessagesLocation, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { let messagesByPeerId = self.messagesGroupedByPeerId(messages) for (peerId, peerMessages) in messagesByPeerId { var operations: [MessageHistoryIndexOperation] = [] self.messageHistoryIndexTable.addMessages(self.internalStoreMessages(peerMessages), location: location, operations: &operations) - self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, invalidatedReadStateOperations: &invalidatedReadStateOperations) } } - func addHoles(messageIds: [MessageId], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation]) { + func addHoles(messageIds: [MessageId], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { for (peerId, messageIds) in self.messageIdsByPeerId(messageIds) { var operations: [MessageHistoryIndexOperation] = [] for id in messageIds { self.messageHistoryIndexTable.addHole(id, operations: &operations) } - self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, invalidatedReadStateOperations: &invalidatedReadStateOperations) } } - func removeMessages(messageIds: [MessageId], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation]) { + func removeMessages(messageIds: [MessageId], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { for (peerId, messageIds) in self.messageIdsByPeerId(messageIds) { var operations: [MessageHistoryIndexOperation] = [] + + let (combinedState, invalidate) = self.readStateTable.deleteMessages(peerId, ids: messageIds, incomingStatsInIds: { peerId, namespace, ids in + return self.messageHistoryIndexTable.incomingMessageCountInIds(peerId, namespace: namespace, ids: ids) + }) + for id in messageIds { self.messageHistoryIndexTable.removeMessage(id, operations: &operations) } - self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + self.processIndexOperations(peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, invalidatedReadStateOperations: &invalidatedReadStateOperations) + + if let combinedState = combinedState { + var outputOperations: [MessageHistoryOperation] = [] + outputOperations.append(.UpdateReadState(combinedState)) + + if operationsByPeerId[peerId] == nil { + operationsByPeerId[peerId] = outputOperations + } else { + operationsByPeerId[peerId]!.appendContentsOf(outputOperations) + } + } + + if invalidate { + self.invalidatedReadStateTable.add(peerId, operations: &invalidatedReadStateOperations) + } } } - func fillHole(id: MessageId, fillType: HoleFillType, tagMask: MessageTags?, messages: [StoreMessage], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation]) { + func fillHole(id: MessageId, fillType: HoleFillType, tagMask: MessageTags?, messages: [StoreMessage], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { var operations: [MessageHistoryIndexOperation] = [] self.messageHistoryIndexTable.fillHole(id, fillType: fillType, tagMask: tagMask, messages: self.internalStoreMessages(messages), operations: &operations) - self.processIndexOperations(id.peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + self.processIndexOperations(id.peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, invalidatedReadStateOperations: &invalidatedReadStateOperations) } - func updateMessage(id: MessageId, message: StoreMessage, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation]) { + func updateMessage(id: MessageId, message: StoreMessage, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { var operations: [MessageHistoryIndexOperation] = [] self.messageHistoryIndexTable.updateMessage(id, message: self.internalStoreMessages([message]).first!, operations: &operations) - self.processIndexOperations(id.peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations) + self.processIndexOperations(id.peerId, operations: operations, processedOperationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, invalidatedReadStateOperations: &invalidatedReadStateOperations) + } + + func resetIncomingReadStates(states: [PeerId: [MessageId.Namespace: PeerReadState]], inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + for (peerId, namespaces) in states { + if let combinedState = self.readStateTable.resetStates(peerId, namespaces: namespaces) { + if operationsByPeerId[peerId] == nil { + operationsByPeerId[peerId] = [.UpdateReadState(combinedState)] + } else { + operationsByPeerId[peerId]!.append(.UpdateReadState(combinedState)) + } + } + self.invalidatedReadStateTable.remove(peerId, operations: &invalidatedReadStateOperations) + } + } + + func applyIncomingReadMaxId(messageId: MessageId, inout operationsByPeerId: [PeerId: [MessageHistoryOperation]], inout invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { + let (combinedState, invalidated) = self.readStateTable.applyMaxReadId(messageId.peerId, namespace: messageId.namespace, maxReadId: messageId.id, maxKnownId: messageId.id, incomingStatsInRange: { fromId, toId in + return self.messageHistoryIndexTable.incomingMessageCountInRange(messageId.peerId, namespace: messageId.namespace, minId: fromId, maxId: toId) + }) + + if let combinedState = combinedState { + if operationsByPeerId[messageId.peerId] == nil { + operationsByPeerId[messageId.peerId] = [.UpdateReadState(combinedState)] + } else { + operationsByPeerId[messageId.peerId]!.append(.UpdateReadState(combinedState)) + } + } + + if invalidated { + self.invalidatedReadStateTable.add(messageId.peerId, operations: &invalidatedReadStateOperations) + } } func topMessage(peerId: PeerId) -> IntermediateMessage? { diff --git a/Postbox/MessageHistoryView.swift b/Postbox/MessageHistoryView.swift index ddc3a1d804..12e2b49ff9 100644 --- a/Postbox/MessageHistoryView.swift +++ b/Postbox/MessageHistoryView.swift @@ -69,12 +69,14 @@ final class MutableMessageHistoryViewReplayContext { final class MutableMessageHistoryView { let tagMask: MessageTags? + private var combinedReadState: CombinedPeerReadState? private let count: Int private var earlier: MutableMessageHistoryEntry? private var later: MutableMessageHistoryEntry? private var entries: [MutableMessageHistoryEntry] - init(earlier: MutableMessageHistoryEntry?, entries: [MutableMessageHistoryEntry], later: MutableMessageHistoryEntry?, tagMask: MessageTags?, count: Int) { + init(combinedReadState: CombinedPeerReadState?, earlier: MutableMessageHistoryEntry?, entries: [MutableMessageHistoryEntry], later: MutableMessageHistoryEntry?, tagMask: MessageTags?, count: Int) { + self.combinedReadState = combinedReadState self.earlier = earlier self.entries = entries self.later = later @@ -105,8 +107,12 @@ final class MutableMessageHistoryView { if self.remove(Set(indices), context: context) { hasChanges = true } + case let .UpdateReadState(combinedReadState): + hasChanges = true + self.combinedReadState = combinedReadState } } + return hasChanges } @@ -322,6 +328,7 @@ public final class MessageHistoryView { public let earlierId: MessageIndex? public let laterId: MessageIndex? public let entries: [MessageHistoryEntry] + public let maxReadIndex: MessageIndex? init(_ mutableView: MutableMessageHistoryView) { var entries: [MessageHistoryEntry] = [] @@ -339,5 +346,21 @@ public final class MessageHistoryView { self.earlierId = mutableView.earlier?.index self.laterId = mutableView.later?.index + + if let combinedReadState = mutableView.combinedReadState { + var maxIndex: MessageIndex? + for (namespace, state) in combinedReadState.states { + for entry in entries { + if entry.index.id.namespace == namespace { + if maxIndex == nil || maxIndex! < entry.index { + maxIndex = entry.index + } + } + } + } + self.maxReadIndex = maxIndex + } else { + self.maxReadIndex = nil + } } } diff --git a/Postbox/PeerChatStateTable.swift b/Postbox/PeerChatStateTable.swift index 721d889461..8354705477 100644 --- a/Postbox/PeerChatStateTable.swift +++ b/Postbox/PeerChatStateTable.swift @@ -43,4 +43,4 @@ final class PeerChatStateTable: Table { } self.updatedPeerIds.removeAll() } -} \ No newline at end of file +} diff --git a/Postbox/PeerReadState.swift b/Postbox/PeerReadState.swift new file mode 100644 index 0000000000..a8a66588c4 --- /dev/null +++ b/Postbox/PeerReadState.swift @@ -0,0 +1,20 @@ + +public struct PeerReadState: Equatable { + public let maxReadId: MessageId.Id + public let maxKnownId: MessageId.Id + public let count: Int32 + + public init(maxReadId: MessageId.Id, maxKnownId: MessageId.Id, count: Int32) { + self.maxReadId = maxReadId + self.maxKnownId = maxKnownId + self.count = count + } +} + +public func ==(lhs: PeerReadState, rhs: PeerReadState) -> Bool { + return lhs.maxReadId == rhs.maxReadId && lhs.maxKnownId == rhs.maxKnownId && lhs.count == rhs.count +} + +struct CombinedPeerReadState { + let states: [(MessageId.Namespace, PeerReadState)] +} diff --git a/Postbox/Postbox.swift b/Postbox/Postbox.swift index 4169d232eb..190a3a50db 100644 --- a/Postbox/Postbox.swift +++ b/Postbox/Postbox.swift @@ -40,6 +40,18 @@ public final class Modifier { } } + public func resetIncomingReadStates(states: [PeerId: [MessageId.Namespace: PeerReadState]]) { + self.postbox?.resetIncomingReadStates(states) + } + + public func applyIncomingReadMaxId(messageId: MessageId) { + self.postbox?.applyIncomingReadMaxId(messageId) + } + + public func validateIncomingReadState(peerId: PeerId) { + self.postbox?.validateIncomingReadState(peerId) + } + public func getState() -> Coding? { return self.postbox?.getState() } @@ -90,6 +102,7 @@ public final class Postbox { private var currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] private var currentUnsentOperations: [IntermediateMessageHistoryUnsentOperation] = [] + private var currentInvalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation] = [] private var currentFilledHolesByPeerId = Set() private var currentUpdatedPeers: [PeerId: Peer] = [:] @@ -112,6 +125,11 @@ public final class Postbox { self.sendUnsentMessageImpl.set(single(sendUnsentMessage, NoError.self)) } + private let validatePeerReadStateImpl = Promise Signal>() + public func setValidatePeerReadState(validatePeerReadState: PeerId -> Signal) { + self.validatePeerReadStateImpl.set(.single(validatePeerReadState)) + } + public let mediaBox: MediaBox var tables: [Table] = [] @@ -130,11 +148,16 @@ public final class Postbox { var messageHistoryUnsentTable: MessageHistoryUnsentTable! var messageHistoryTagsTable: MessageHistoryTagsTable! var peerChatStateTable: PeerChatStateTable! + var readStateTable: MessageHistoryReadStateTable! + var invalidatedReadStateTable: MessageHistoryInvalidatedReadStateTable! public init(basePath: String, globalMessageIdsNamespace: MessageId.Namespace, seedConfiguration: SeedConfiguration) { self.basePath = basePath self.globalMessageIdsNamespace = globalMessageIdsNamespace self.seedConfiguration = seedConfiguration + + let _ = try? NSFileManager.defaultManager().removeItemAtPath(self.basePath + "/media") + self.mediaBox = MediaBox(basePath: self.basePath + "/media") self.openDatabase() } @@ -175,12 +198,13 @@ public final class Postbox { //let _ = try? NSFileManager.defaultManager().removeItemAtPath(self.basePath + "/media") //self.debugSaveState("beforeGetDiff") //self.debugRestoreState("beforeGetDiff") + //self.debugRestoreState("clean") self.valueBox = SqliteValueBox(basePath: self.basePath + "/db") self.metadataTable = MetadataTable(valueBox: self.valueBox, tableId: 0) let userVersion: Int32? = self.metadataTable.userVersion() - let currentUserVersion: Int32 = 18 + let currentUserVersion: Int32 = 1 if userVersion != currentUserVersion { self.valueBox.drop() @@ -196,7 +220,9 @@ public final class Postbox { self.messageHistoryIndexTable = MessageHistoryIndexTable(valueBox: self.valueBox, tableId: 4, globalMessageIdsTable: self.globalMessageIdsTable, metadataTable: self.messageHistoryMetadataTable, seedConfiguration: self.seedConfiguration) self.mediaCleanupTable = MediaCleanupTable(valueBox: self.valueBox, tableId: 5) self.mediaTable = MessageMediaTable(valueBox: self.valueBox, tableId: 6, mediaCleanupTable: self.mediaCleanupTable) - self.messageHistoryTable = MessageHistoryTable(valueBox: self.valueBox, tableId: 7, messageHistoryIndexTable: self.messageHistoryIndexTable, messageMediaTable: self.mediaTable, historyMetadataTable: self.messageHistoryMetadataTable, unsentTable: self.messageHistoryUnsentTable!, tagsTable: self.messageHistoryTagsTable) + self.readStateTable = MessageHistoryReadStateTable(valueBox: self.valueBox, tableId: 14) + self.invalidatedReadStateTable = MessageHistoryInvalidatedReadStateTable(valueBox: self.valueBox, tableId: 15) + self.messageHistoryTable = MessageHistoryTable(valueBox: self.valueBox, tableId: 7, messageHistoryIndexTable: self.messageHistoryIndexTable, messageMediaTable: self.mediaTable, historyMetadataTable: self.messageHistoryMetadataTable, unsentTable: self.messageHistoryUnsentTable!, tagsTable: self.messageHistoryTagsTable, readStateTable: self.readStateTable, invalidatedReadStateTable: self.invalidatedReadStateTable!) self.chatListIndexTable = ChatListIndexTable(valueBox: self.valueBox, tableId: 8) self.chatListTable = ChatListTable(valueBox: self.valueBox, tableId: 9, indexTable: self.chatListIndexTable, metadataTable: self.messageHistoryMetadataTable, seedConfiguration: self.seedConfiguration) self.peerChatStateTable = PeerChatStateTable(valueBox: self.valueBox, tableId: 13) @@ -212,8 +238,10 @@ public final class Postbox { self.tables.append(self.chatListIndexTable) self.tables.append(self.chatListTable) self.tables.append(self.peerChatStateTable) + self.tables.append(self.readStateTable) + self.tables.append(self.invalidatedReadStateTable) - self.viewTracker = ViewTracker(queue: self.queue, fetchEarlierHistoryEntries: self.fetchEarlierHistoryEntries, fetchLaterHistoryEntries: self.fetchLaterHistoryEntries, fetchEarlierChatEntries: self.fetchEarlierChatEntries, fetchLaterChatEntries: self.fetchLaterChatEntries, renderMessage: self.renderIntermediateMessage, fetchChatListHole: self.fetchChatListHoleWrapper, fetchMessageHistoryHole: self.fetchMessageHistoryHoleWrapper, sendUnsentMessage: self.sendUnsentMessageWrapper, unsentMessageIndices: self.messageHistoryUnsentTable!.get()) + self.viewTracker = ViewTracker(queue: self.queue, fetchEarlierHistoryEntries: self.fetchEarlierHistoryEntries, fetchLaterHistoryEntries: self.fetchLaterHistoryEntries, fetchEarlierChatEntries: self.fetchEarlierChatEntries, fetchLaterChatEntries: self.fetchLaterChatEntries, renderMessage: self.renderIntermediateMessage, fetchChatListHole: self.fetchChatListHoleWrapper, fetchMessageHistoryHole: self.fetchMessageHistoryHoleWrapper, sendUnsentMessage: self.sendUnsentMessageWrapper, unsentMessageIndices: self.messageHistoryUnsentTable!.get(), validateReadState: self.validatePeerReadStateWrapper, invalidatedReadStatePeerIds: self.invalidatedReadStateTable!.get()) print("(Postbox initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") } @@ -271,15 +299,15 @@ public final class Postbox { } private func addMessages(messages: [StoreMessage], location: AddMessagesLocation) { - self.messageHistoryTable.addMessages(messages, location: location, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations) + self.messageHistoryTable.addMessages(messages, location: location, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) } private func addHole(id: MessageId) { - self.messageHistoryTable.addHoles([id], operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations) + self.messageHistoryTable.addHoles([id], operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) } private func fillHole(hole: MessageHistoryHole, fillType: HoleFillType, tagMask: MessageTags?, messages: [StoreMessage]) { - self.messageHistoryTable.fillHole(hole.id, fillType: fillType, tagMask: tagMask, messages: messages, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations) + self.messageHistoryTable.fillHole(hole.id, fillType: fillType, tagMask: tagMask, messages: messages, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) self.currentFilledHolesByPeerId.insert(hole.id.peerId) } @@ -288,7 +316,19 @@ public final class Postbox { } private func deleteMessages(messageIds: [MessageId]) { - self.messageHistoryTable.removeMessages(messageIds, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations) + self.messageHistoryTable.removeMessages(messageIds, operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: ¤tUnsentOperations, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) + } + + private func resetIncomingReadStates(states: [PeerId: [MessageId.Namespace: PeerReadState]]) { + self.messageHistoryTable.resetIncomingReadStates(states, operationsByPeerId: &self.currentOperationsByPeerId, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) + } + + private func applyIncomingReadMaxId(messageId: MessageId) { + self.messageHistoryTable.applyIncomingReadMaxId(messageId, operationsByPeerId: &self.currentOperationsByPeerId, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) + } + + private func validateIncomingReadState(peerId: PeerId) { + self.invalidatedReadStateTable.remove(peerId, operations: &self.currentInvalidatedReadStateOperations) } private func fetchEarlierHistoryEntries(peerId: PeerId, index: MessageIndex?, count: Int, tagMask: MessageTags? = nil) -> [MutableMessageHistoryEntry] { @@ -383,7 +423,7 @@ public final class Postbox { for entry in intermediateEntries { switch entry { case let .Message(message): - entries.append(.IntermediateMessageEntry(message)) + entries.append(.IntermediateMessageEntry(message, self.readStateTable.getCombinedState(message.id.peerId))) case let .Hole(hole): entries.append(.HoleEntry(hole)) case let .Nothing(index): @@ -394,7 +434,7 @@ public final class Postbox { if let intermediateLower = intermediateLower { switch intermediateLower { case let .Message(message): - lower = .IntermediateMessageEntry(message) + lower = .IntermediateMessageEntry(message, self.readStateTable.getCombinedState(message.id.peerId)) case let .Hole(hole): lower = .HoleEntry(hole) case let .Nothing(index): @@ -405,7 +445,7 @@ public final class Postbox { if let intermediateUpper = intermediateUpper { switch intermediateUpper { case let .Message(message): - upper = .IntermediateMessageEntry(message) + upper = .IntermediateMessageEntry(message, self.readStateTable.getCombinedState(message.id.peerId)) case let .Hole(hole): upper = .HoleEntry(hole) case let .Nothing(index): @@ -422,7 +462,7 @@ public final class Postbox { for entry in intermediateEntries { switch entry { case let .Message(message): - entries.append(.IntermediateMessageEntry(message)) + entries.append(.IntermediateMessageEntry(message, self.readStateTable.getCombinedState(message.id.peerId))) case let .Hole(hole): entries.append(.HoleEntry(hole)) case let .Nothing(index): @@ -438,7 +478,7 @@ public final class Postbox { for entry in intermediateEntries { switch entry { case let .Message(message): - entries.append(.IntermediateMessageEntry(message)) + entries.append(.IntermediateMessageEntry(message, self.readStateTable.getCombinedState(message.id.peerId))) case let .Nothing(index): entries.append(.Nothing(index)) case let .Hole(index): @@ -475,6 +515,12 @@ public final class Postbox { }).start() } + private func validatePeerReadStateWrapper(peerId: PeerId) -> Disposable { + return (self.validatePeerReadStateImpl.get() |> mapToSignal { validate -> Signal in + return validate(peerId) + }).start() + } + private func beforeCommit() { var chatListOperations: [ChatListOperation] = [] self.chatListTable.replay(self.currentOperationsByPeerId, messageHistoryTable: self.messageHistoryTable, operations: &chatListOperations) @@ -482,13 +528,14 @@ public final class Postbox { self.chatListTable.replaceHole(index, hole: hole, operations: &chatListOperations) } - self.viewTracker.updateViews(currentOperationsByPeerId: self.currentOperationsByPeerId, peerIdsWithFilledHoles: self.currentFilledHolesByPeerId, chatListOperations: chatListOperations, currentUpdatedPeers: self.currentUpdatedPeers, unsentMessageOperations: self.currentUnsentOperations) + self.viewTracker.updateViews(currentOperationsByPeerId: self.currentOperationsByPeerId, peerIdsWithFilledHoles: self.currentFilledHolesByPeerId, chatListOperations: chatListOperations, currentUpdatedPeers: self.currentUpdatedPeers, unsentMessageOperations: self.currentUnsentOperations, invalidatedReadStateOperations: self.currentInvalidatedReadStateOperations) self.currentOperationsByPeerId.removeAll() self.currentFilledHolesByPeerId.removeAll() self.currentUpdatedPeers.removeAll() self.currentReplaceChatListHoles.removeAll() self.currentUnsentOperations.removeAll() + self.currentInvalidatedReadStateOperations.removeAll() for table in self.tables { table.beforeCommit() @@ -521,7 +568,7 @@ public final class Postbox { private func updateMessage(index: MessageIndex, update: Message -> StoreMessage) { if let intermediateMessage = self.messageHistoryTable.getMessage(index) { let message = self.renderIntermediateMessage(intermediateMessage) - self.messageHistoryTable.updateMessage(index.id, message: update(message), operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: &self.currentUnsentOperations) + self.messageHistoryTable.updateMessage(index.id, message: update(message), operationsByPeerId: &self.currentOperationsByPeerId, unsentMessageOperations: &self.currentUnsentOperations, invalidatedReadStateOperations: &self.currentInvalidatedReadStateOperations) } } @@ -564,7 +611,7 @@ public final class Postbox { self.queue.dispatch { let (entries, earlier, later) = self.fetchAroundHistoryEntries(index, count: count, tagMask: tagMask) - let mutableView = MutableMessageHistoryView(earlier: earlier, entries: entries, later: later, tagMask: tagMask, count: count) + let mutableView = MutableMessageHistoryView(combinedReadState: self.readStateTable.getCombinedState(peerId), earlier: earlier, entries: entries, later: later, tagMask: tagMask, count: count) mutableView.render(self.renderIntermediateMessage) subscriber.putNext((MessageHistoryView(mutableView), .Generic)) diff --git a/Postbox/SqliteValueBox.swift b/Postbox/SqliteValueBox.swift index 33631777b2..8185d75c6f 100644 --- a/Postbox/SqliteValueBox.swift +++ b/Postbox/SqliteValueBox.swift @@ -17,6 +17,10 @@ private struct SqlitePreparedStatement { sqlite3_bind_int(statement, Int32(index), number) } + func bind(index: Int, number: Int64) { + sqlite3_bind_int64(statement, Int32(index), number) + } + func reset() { sqlite3_reset(statement) sqlite3_clear_bindings(statement) @@ -30,6 +34,10 @@ private struct SqlitePreparedStatement { return result == SQLITE_ROW } + func int64At(index: Int) -> Int64 { + return sqlite3_column_int64(statement, Int32(index)) + } + func valueAt(index: Int) -> ReadBuffer { let valueLength = sqlite3_column_bytes(statement, Int32(index)) let valueData = sqlite3_column_blob(statement, Int32(index)) @@ -108,13 +116,14 @@ public final class SqliteValueBox: ValueBox { database.execute("PRAGMA wal_autocheckpoint=200") database.execute("PRAGMA journal_size_limit=1536") - let result = database.scalar("PRAGMA user_version") as! Int64 + let result = self.getUserVersion(database) if result != 1 { database.execute("PRAGMA user_version=1") database.execute("CREATE TABLE __meta_tables (name INTEGER)") } - for row in database.prepare("SELECT name FROM __meta_tables").run() { - self.tables.insert(Int32(row[0] as! Int64)) + + for table in self.listTables(database).map({Int32($0)}) { + self.tables.insert(table) } lock.unlock() @@ -142,15 +151,38 @@ public final class SqliteValueBox: ValueBox { } public func begin() { - self.database.transaction() + self.database.execute("BEGIN") } public func commit() { let startTime = CFAbsoluteTimeGetCurrent() - self.database.commit() + self.database.execute("COMMIT") self.commitTime += CFAbsoluteTimeGetCurrent() - startTime } + private func getUserVersion(database: Database) -> Int64 { + var statement: COpaquePointer = nil + sqlite3_prepare_v2(database.handle, "PRAGMA user_version", -1, &statement, nil) + let preparedStatement = SqlitePreparedStatement(statement: statement) + preparedStatement.step() + let value = preparedStatement.int64At(0) + preparedStatement.destroy() + return value + } + + private func listTables(database: Database) -> [Int64] { + var statement: COpaquePointer = nil + sqlite3_prepare_v2(database.handle, "SELECT name FROM __meta_tables", -1, &statement, nil) + let preparedStatement = SqlitePreparedStatement(statement: statement) + var tables: [Int64] = [] + while preparedStatement.step() { + let value = preparedStatement.int64At(0) + tables.append(value) + } + preparedStatement.destroy() + return tables + } + private func getStatement(table: Int32, key: ValueBoxKey) -> SqlitePreparedStatement { let resultStatement: SqlitePreparedStatement @@ -503,7 +535,7 @@ public final class SqliteValueBox: ValueBox { } } - public func range(table: Int32, start: ValueBoxKey, end: ValueBoxKey, keys: ValueBoxKey -> Bool, limit: Int) { + public func range(table: Int32, start: ValueBoxKey, end: ValueBoxKey, @noescape keys: ValueBoxKey -> Bool, limit: Int) { if self.tables.contains(table) { let statement: SqlitePreparedStatement diff --git a/Postbox/ValueBox.swift b/Postbox/ValueBox.swift index 83973eeb8f..2ade4659cc 100644 --- a/Postbox/ValueBox.swift +++ b/Postbox/ValueBox.swift @@ -8,7 +8,7 @@ public protocol ValueBox { func endStats() func range(table: Int32, start: ValueBoxKey, end: ValueBoxKey, @noescape values: (ValueBoxKey, ReadBuffer) -> Bool, limit: Int) - func range(table: Int32, start: ValueBoxKey, end: ValueBoxKey, keys: ValueBoxKey -> Bool, limit: Int) + func range(table: Int32, start: ValueBoxKey, end: ValueBoxKey, @noescape keys: ValueBoxKey -> Bool, limit: Int) func get(table: Int32, key: ValueBoxKey) -> ReadBuffer? func exists(table: Int32, key: ValueBoxKey) -> Bool func set(table: Int32, key: ValueBoxKey, value: MemoryBuffer) diff --git a/Postbox/ViewTracker.swift b/Postbox/ViewTracker.swift index 276b655c86..772c2efdee 100644 --- a/Postbox/ViewTracker.swift +++ b/Postbox/ViewTracker.swift @@ -16,16 +16,19 @@ final class ViewTracker { private let fetchChatListHole: ChatListHole -> Disposable private let fetchMessageHistoryHole: (MessageHistoryHole, MessageTags?) -> Disposable private let sendUnsentMessage: MessageIndex -> Disposable + private let validateReadState: PeerId -> Disposable private var chatListViews = Bag<(MutableChatListView, Pipe<(ChatListView, ViewUpdateType)>)>() private var messageHistoryViews: [PeerId: Bag<(MutableMessageHistoryView, Pipe<(MessageHistoryView, ViewUpdateType)>)>] = [:] private var unsentMessageView: UnsentMessageHistoryView + private var invalidatedReadStatesView: InvalidatedPeerReadStatesView private var chatListHoleDisposables: [(ChatListHole, Disposable)] = [] private var holeDisposablesByPeerId: [PeerId: [(MessageHistoryHole, Disposable)]] = [:] private var unsentMessageDisposables: [MessageIndex: Disposable] = [:] + private var validateReadStatesDisposables: [PeerId: Disposable] = [:] - init(queue: Queue, fetchEarlierHistoryEntries: (PeerId, MessageIndex?, Int, MessageTags?) -> [MutableMessageHistoryEntry], fetchLaterHistoryEntries: (PeerId, MessageIndex?, Int, MessageTags?) -> [MutableMessageHistoryEntry], fetchEarlierChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry], fetchLaterChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry], renderMessage: IntermediateMessage -> Message, fetchChatListHole: ChatListHole -> Disposable, fetchMessageHistoryHole: (MessageHistoryHole, MessageTags?) -> Disposable, sendUnsentMessage: MessageIndex -> Disposable, unsentMessageIndices: [MessageIndex]) { + init(queue: Queue, fetchEarlierHistoryEntries: (PeerId, MessageIndex?, Int, MessageTags?) -> [MutableMessageHistoryEntry], fetchLaterHistoryEntries: (PeerId, MessageIndex?, Int, MessageTags?) -> [MutableMessageHistoryEntry], fetchEarlierChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry], fetchLaterChatEntries: (MessageIndex?, Int) -> [MutableChatListEntry], renderMessage: IntermediateMessage -> Message, fetchChatListHole: ChatListHole -> Disposable, fetchMessageHistoryHole: (MessageHistoryHole, MessageTags?) -> Disposable, sendUnsentMessage: MessageIndex -> Disposable, unsentMessageIndices: [MessageIndex], validateReadState: PeerId -> Disposable, invalidatedReadStatePeerIds: [PeerId]) { self.queue = queue self.fetchEarlierHistoryEntries = fetchEarlierHistoryEntries self.fetchLaterHistoryEntries = fetchLaterHistoryEntries @@ -35,9 +38,12 @@ final class ViewTracker { self.fetchChatListHole = fetchChatListHole self.fetchMessageHistoryHole = fetchMessageHistoryHole self.sendUnsentMessage = sendUnsentMessage + self.validateReadState = validateReadState self.unsentMessageView = UnsentMessageHistoryView(indices: unsentMessageIndices) + self.invalidatedReadStatesView = InvalidatedPeerReadStatesView(peerIds: invalidatedReadStatePeerIds) self.unsentViewUpdated() + self.invalidatedReadStateViewUpdated() } deinit { @@ -91,7 +97,7 @@ final class ViewTracker { self.updateTrackedChatListHoles() } - func updateViews(currentOperationsByPeerId currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], peerIdsWithFilledHoles: Set, chatListOperations: [ChatListOperation], currentUpdatedPeers: [PeerId: Peer], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation]) { + func updateViews(currentOperationsByPeerId currentOperationsByPeerId: [PeerId: [MessageHistoryOperation]], peerIdsWithFilledHoles: Set, chatListOperations: [ChatListOperation], currentUpdatedPeers: [PeerId: Peer], unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation], invalidatedReadStateOperations: [IntermediateMessageHistoryInvalidatedReadStateOperation]) { var updateTrackedHolesPeerIds: [PeerId] = [] for (peerId, bag) in self.messageHistoryViews { @@ -147,6 +153,10 @@ final class ViewTracker { if self.unsentMessageView.replay(unsentMessageOperations) { self.unsentViewUpdated() } + + if self.invalidatedReadStatesView.replay(invalidatedReadStateOperations) { + self.invalidatedReadStateViewUpdated() + } } private func updateTrackedChatListHoles() { @@ -270,7 +280,7 @@ final class ViewTracker { private func unsentViewUpdated() { var removeIndices: [MessageIndex] = [] - for (index, _) in unsentMessageDisposables { + for (index, _) in self.unsentMessageDisposables { var found = false for currentIndex in self.unsentMessageView.indices { if currentIndex == index { @@ -302,4 +312,32 @@ final class ViewTracker { } } } + + private func invalidatedReadStateViewUpdated() { + var removePeerIds: [PeerId] = [] + let currentPeerIds = self.invalidatedReadStatesView.peerIds + for (peerId, _) in self.validateReadStatesDisposables { + if !currentPeerIds.contains(peerId) { + removePeerIds.append(peerId) + } + } + + for peerId in removePeerIds { + self.validateReadStatesDisposables.removeValueForKey(peerId)?.dispose() + } + + for peerId in currentPeerIds { + var found = false + for (currentPeerId, _) in validateReadStatesDisposables { + if peerId == currentPeerId { + found = true + break + } + } + + if !found { + self.validateReadStatesDisposables[peerId] = self.validateReadState(peerId) + } + } + } } diff --git a/PostboxTests/ChatListTableTests.swift b/PostboxTests/ChatListTableTests.swift index 1e0c17ff18..7f569f75a4 100644 --- a/PostboxTests/ChatListTableTests.swift +++ b/PostboxTests/ChatListTableTests.swift @@ -73,6 +73,8 @@ class ChatListTableTests: XCTestCase { var historyMetadataTable: MessageHistoryMetadataTable? var unsentTable: MessageHistoryUnsentTable? var tagsTable: MessageHistoryTagsTable? + var readStateTable: MessageHistoryReadStateTable? + var invalidatedReadStateTable: MessageHistoryInvalidatedReadStateTable? override class func setUp() { super.setUp() @@ -95,7 +97,9 @@ class ChatListTableTests: XCTestCase { self.indexTable = MessageHistoryIndexTable(valueBox: self.valueBox!, tableId: 1, globalMessageIdsTable: self.globalMessageIdsTable!, metadataTable: self.historyMetadataTable!, seedConfiguration: seedConfiguration) self.mediaCleanupTable = MediaCleanupTable(valueBox: self.valueBox!, tableId: 3) self.mediaTable = MessageMediaTable(valueBox: self.valueBox!, tableId: 2, mediaCleanupTable: self.mediaCleanupTable!) - self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!, historyMetadataTable: self.historyMetadataTable!, unsentTable: self.unsentTable!, tagsTable: self.tagsTable!) + self.readStateTable = MessageHistoryReadStateTable(valueBox: self.valueBox!, tableId: 11) + self.invalidatedReadStateTable = MessageHistoryInvalidatedReadStateTable(valueBox: self.valueBox!, tableId: 12) + self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!, historyMetadataTable: self.historyMetadataTable!, unsentTable: self.unsentTable!, tagsTable: self.tagsTable!, readStateTable: self.readStateTable!, invalidatedReadStateTable: invalidatedReadStateTable) self.chatListIndexTable = ChatListIndexTable(valueBox: self.valueBox!, tableId: 5) self.chatListTable = ChatListTable(valueBox: self.valueBox!, tableId: 6, indexTable: self.chatListIndexTable!, metadataTable: self.historyMetadataTable!, seedConfiguration: seedConfiguration) } diff --git a/PostboxTests/MessageHistoryTableTests.swift b/PostboxTests/MessageHistoryTableTests.swift index bacbc988f7..ddef648dab 100644 --- a/PostboxTests/MessageHistoryTableTests.swift +++ b/PostboxTests/MessageHistoryTableTests.swift @@ -202,6 +202,8 @@ class MessageHistoryTableTests: XCTestCase { var historyMetadataTable: MessageHistoryMetadataTable? var unsentTable: MessageHistoryUnsentTable? var tagsTable: MessageHistoryTagsTable? + var readStateTable: MessageHistoryReadStateTable? + var invalidatedReadStateTable: MessageHistoryInvalidatedReadStateTable? override class func setUp() { super.setUp() @@ -228,7 +230,9 @@ class MessageHistoryTableTests: XCTestCase { self.indexTable = MessageHistoryIndexTable(valueBox: self.valueBox!, tableId: 1, globalMessageIdsTable: self.globalMessageIdsTable!, metadataTable: self.historyMetadataTable!, seedConfiguration: seedConfiguration) self.mediaCleanupTable = MediaCleanupTable(valueBox: self.valueBox!, tableId: 3) self.mediaTable = MessageMediaTable(valueBox: self.valueBox!, tableId: 2, mediaCleanupTable: self.mediaCleanupTable!) - self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!, historyMetadataTable: self.historyMetadataTable!, unsentTable: self.unsentTable!, tagsTable: self.tagsTable!) + self.readStateTable = MessageHistoryReadStateTable(valueBox: self.valueBox!, tableId: 10) + self.invalidatedReadStateTable = MessageHistoryInvalidatedReadStateTable(valueBox: self.valueBox!, tableId: 11) + self.historyTable = MessageHistoryTable(valueBox: self.valueBox!, tableId: 4, messageHistoryIndexTable: self.indexTable!, messageMediaTable: self.mediaTable!, historyMetadataTable: self.historyMetadataTable!, unsentTable: self.unsentTable!, tagsTable: self.tagsTable!, readStateTable: self.readStateTable!, invalidatedReadStateTable: invalidatedReadStateTable) self.peerTable = PeerTable(valueBox: self.valueBox!, tableId: 6) self.peerTable!.set(peer) }