diff --git a/Postbox.xcodeproj/project.pbxproj b/Postbox.xcodeproj/project.pbxproj index 8c3ebdccf2..1d86e8a9ee 100644 --- a/Postbox.xcodeproj/project.pbxproj +++ b/Postbox.xcodeproj/project.pbxproj @@ -169,6 +169,8 @@ D0E3A7881B28AE9C00A402D9 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E3A7871B28AE9C00A402D9 /* Coding.swift */; }; D0E3A79E1B28B50400A402D9 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E3A79D1B28B50400A402D9 /* Message.swift */; }; D0E3A7A21B28B7DC00A402D9 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E3A7A11B28B7DC00A402D9 /* Media.swift */; }; + D0F7AB321DCFAB18009AD9A1 /* PeerChatTopTaggedMessageIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F7AB311DCFAB18009AD9A1 /* PeerChatTopTaggedMessageIds.swift */; }; + D0F7AB331DCFAB1C009AD9A1 /* PeerChatTopTaggedMessageIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F7AB311DCFAB18009AD9A1 /* PeerChatTopTaggedMessageIds.swift */; }; D0F9E85B1C565EBB00037222 /* MessageMediaTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E85A1C565EBB00037222 /* MessageMediaTable.swift */; }; D0F9E8611C57766A00037222 /* MessageHistoryTableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E8601C57766A00037222 /* MessageHistoryTableTests.swift */; }; D0F9E8631C579F0200037222 /* MediaCleanupTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9E8621C579F0200037222 /* MediaCleanupTable.swift */; }; @@ -282,6 +284,7 @@ D0E3A7871B28AE9C00A402D9 /* Coding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = ""; }; D0E3A79D1B28B50400A402D9 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; D0E3A7A11B28B7DC00A402D9 /* Media.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = ""; }; + D0F7AB311DCFAB18009AD9A1 /* PeerChatTopTaggedMessageIds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerChatTopTaggedMessageIds.swift; sourceTree = ""; }; D0F9E85A1C565EBB00037222 /* MessageMediaTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageMediaTable.swift; sourceTree = ""; }; D0F9E8601C57766A00037222 /* MessageHistoryTableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryTableTests.swift; sourceTree = ""; }; D0F9E8621C579F0200037222 /* MediaCleanupTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaCleanupTable.swift; sourceTree = ""; }; @@ -411,6 +414,7 @@ D021E0D51DB4FCFC00C6B04F /* ItemCollectionInfoTable.swift */, D021E0D71DB4FD1300C6B04F /* ItemCollectionItemTable.swift */, D07CFF821DCA909100761F81 /* PeerChatInterfaceStateTable.swift */, + D0F7AB311DCFAB18009AD9A1 /* PeerChatTopTaggedMessageIds.swift */, ); name = Tables; sourceTree = ""; @@ -781,6 +785,7 @@ D0B418321D7DFE16004562A4 /* SqliteValueBox.swift in Sources */, D073CE8C1DCBF3BB007511FD /* ChatListTable.swift in Sources */, D073CE921DCBF3BB007511FD /* PeerTable.swift in Sources */, + D0F7AB331DCFAB1C009AD9A1 /* PeerChatTopTaggedMessageIds.swift in Sources */, D073CE741DCBF3B4007511FD /* Peer.swift in Sources */, D073CE7A1DCBF3B4007511FD /* PeerReadState.swift in Sources */, D073CE801DCBF3B4007511FD /* PeerChatInterfaceState.swift in Sources */, @@ -863,6 +868,7 @@ D00EED1E1C81F28D00341DFF /* MessageHistoryTagsTable.swift in Sources */, D044CA2C1C617E2D002160FF /* MessageHistoryMetadataTable.swift in Sources */, D03120FC1DA55427006A2A60 /* PeerNotificationSettings.swift in Sources */, + D0F7AB321DCFAB18009AD9A1 /* PeerChatTopTaggedMessageIds.swift in Sources */, D03BCCF81C73561C0097A291 /* Table.swift in Sources */, D021E0DC1DB5237C00C6B04F /* ItemCollectionsView.swift in Sources */, D0C735281C864DF300BB3149 /* PeerChatStateTable.swift in Sources */, @@ -973,7 +979,7 @@ PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 3.0.1; }; name = Hockeyapp; }; @@ -1223,7 +1229,7 @@ PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 3.0.1; }; name = Debug; }; @@ -1254,7 +1260,7 @@ PROVISIONING_PROFILE_SPECIFIER = X834Q8SBVP/; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 3.0.1; }; name = Release; }; diff --git a/Postbox.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/PostboxMac.xcscheme b/Postbox.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/PostboxMac.xcscheme index f4a9378c19..f05213e2b6 100644 --- a/Postbox.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/PostboxMac.xcscheme +++ b/Postbox.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/PostboxMac.xcscheme @@ -1,6 +1,6 @@ ReadBuffer? { + if Decoder.positionOnKey(self.buffer.memory, offset: &self.offset, maxOffset: self.buffer.length, length: self.buffer.length, key: key, valueType: .Bytes) { + var length: Int32 = 0 + memcpy(&length, self.buffer.memory + self.offset, 4) + self.offset += 4 + Int(length) + let copyBytes = malloc(Int(length))! + memcpy(copyBytes, self.buffer.memory.advanced(by: self.offset - Int(length)), Int(length)) + return ReadBuffer(memory: copyBytes, length: Int(length), freeWhenDone: true) + } else { + return nil + } + } } diff --git a/Postbox/IntermediateMessage.swift b/Postbox/IntermediateMessage.swift index de95c68f27..d3b02068ce 100644 --- a/Postbox/IntermediateMessage.swift +++ b/Postbox/IntermediateMessage.swift @@ -23,6 +23,7 @@ struct IntermediateMessageForwardInfo { class IntermediateMessage { let stableId: UInt32 + let stableVersion: UInt32 let id: MessageId let timestamp: Int32 let flags: MessageFlags @@ -34,8 +35,9 @@ class IntermediateMessage { let embeddedMediaData: ReadBuffer let referencedMedia: [MediaId] - init(stableId: UInt32, id: MessageId, timestamp: Int32, flags: MessageFlags, tags: MessageTags, forwardInfo: IntermediateMessageForwardInfo?, authorId: PeerId?, text: String, attributesData: ReadBuffer, embeddedMediaData: ReadBuffer, referencedMedia: [MediaId]) { + init(stableId: UInt32, stableVersion: UInt32, id: MessageId, timestamp: Int32, flags: MessageFlags, tags: MessageTags, forwardInfo: IntermediateMessageForwardInfo?, authorId: PeerId?, text: String, attributesData: ReadBuffer, embeddedMediaData: ReadBuffer, referencedMedia: [MediaId]) { self.stableId = stableId + self.stableVersion = stableVersion self.id = id self.timestamp = timestamp self.flags = flags diff --git a/Postbox/Message.swift b/Postbox/Message.swift index 923c712c9b..3b03f86136 100644 --- a/Postbox/Message.swift +++ b/Postbox/Message.swift @@ -69,7 +69,7 @@ public struct MessageId: Hashable, Comparable, CustomStringConvertible { var i = 0 var array: [MessageId] = [] while i < Int(length) { - array[i] = MessageId(buffer) + array.append(MessageId(buffer)) i += 1 } return array @@ -201,12 +201,17 @@ public struct MessageFlags: OptionSet { rawValue |= MessageFlags.Incoming.rawValue } + if flags.contains(StoreMessageFlags.Personal) { + rawValue |= MessageFlags.Personal.rawValue + } + self.rawValue = rawValue } public static let Unsent = MessageFlags(rawValue: 1) public static let Failed = MessageFlags(rawValue: 2) public static let Incoming = MessageFlags(rawValue: 4) + public static let Personal = MessageFlags(rawValue: 8) } public struct StoreMessageForwardInfo { @@ -268,6 +273,7 @@ public extension MessageAttribute { public final class Message { public let stableId: UInt32 + public let stableVersion: UInt32 public let id: MessageId public let timestamp: Int32 public let flags: MessageFlags @@ -279,9 +285,11 @@ public final class Message { public let media: [Media] public let peers: SimpleDictionary public let associatedMessages: SimpleDictionary + public let associatedMessageIds: [MessageId] - public init(stableId: UInt32, id: MessageId, timestamp: Int32, flags: MessageFlags, tags: MessageTags, forwardInfo: MessageForwardInfo?, author: Peer?, text: String, attributes: [MessageAttribute], media: [Media], peers: SimpleDictionary, associatedMessages: SimpleDictionary) { + public init(stableId: UInt32, stableVersion: UInt32, id: MessageId, timestamp: Int32, flags: MessageFlags, tags: MessageTags, forwardInfo: MessageForwardInfo?, author: Peer?, text: String, attributes: [MessageAttribute], media: [Media], peers: SimpleDictionary, associatedMessages: SimpleDictionary, associatedMessageIds: [MessageId]) { self.stableId = stableId + self.stableVersion = stableVersion self.id = id self.timestamp = timestamp self.flags = flags @@ -293,6 +301,7 @@ public final class Message { self.media = media self.peers = peers self.associatedMessages = associatedMessages + self.associatedMessageIds = associatedMessageIds } } @@ -310,6 +319,7 @@ public struct StoreMessageFlags: OptionSet { public static let Unsent = StoreMessageFlags(rawValue: 1) public static let Failed = StoreMessageFlags(rawValue: 2) public static let Incoming = StoreMessageFlags(rawValue: 4) + public static let Personal = StoreMessageFlags(rawValue: 8) } public enum StoreMessageId { diff --git a/Postbox/MessageHistoryTable.swift b/Postbox/MessageHistoryTable.swift index 66632dfe5e..7cb2fb5100 100644 --- a/Postbox/MessageHistoryTable.swift +++ b/Postbox/MessageHistoryTable.swift @@ -450,6 +450,9 @@ final class MessageHistoryTable: Table { var stableId: UInt32 = self.historyMetadataTable.getNextStableMessageIndexId() sharedBuffer.write(&stableId, offset: 0, length: 4) + var stableVersion: UInt32 = 0 + sharedBuffer.write(&stableVersion, offset: 0, length: 4) + var flags = MessageFlags(message.flags) sharedBuffer.write(&flags.rawValue, offset: 0, length: 4) @@ -562,7 +565,7 @@ final class MessageHistoryTable: Table { self.valueBox.set(self.tableId, key: self.key(MessageIndex(message), key: sharedKey), value: sharedBuffer) - return IntermediateMessage(stableId: stableId, id: message.id, timestamp: message.timestamp, flags: flags, tags: message.tags, forwardInfo: intermediateForwardInfo, authorId: message.authorId, text: message.text, attributesData: attributesBuffer.makeReadBufferAndReset(), embeddedMediaData: embeddedMediaBuffer.makeReadBufferAndReset(), referencedMedia: referencedMedia) + return IntermediateMessage(stableId: stableId, stableVersion: stableVersion, id: message.id, timestamp: message.timestamp, flags: flags, tags: message.tags, forwardInfo: intermediateForwardInfo, authorId: message.authorId, text: message.text, attributesData: attributesBuffer.makeReadBufferAndReset(), embeddedMediaData: embeddedMediaBuffer.makeReadBufferAndReset(), referencedMedia: referencedMedia) } private func justInsertHole(_ hole: MessageHistoryHole, sharedBuffer: WriteBuffer = WriteBuffer()) { @@ -706,7 +709,7 @@ final class MessageHistoryTable: Table { updatedEmbeddedMediaBuffer.write(encodedBuffer.memory, offset: 0, length: encodedBuffer.length) } - self.storeIntermediateMessage(IntermediateMessage(stableId: message.stableId, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: updatedEmbeddedMediaBuffer.readBufferNoCopy(), referencedMedia: message.referencedMedia), sharedKey: self.key(index)) + self.storeIntermediateMessage(IntermediateMessage(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: updatedEmbeddedMediaBuffer.readBufferNoCopy(), referencedMedia: message.referencedMedia), sharedKey: self.key(index)) let operation: MessageHistoryOperation = .UpdateEmbeddedMedia(index, updatedEmbeddedMediaBuffer.makeReadBufferAndReset()) if operationsByPeerId[index.id.peerId] == nil { @@ -799,10 +802,6 @@ final class MessageHistoryTable: Table { break } - if previousMessage.tags != message.tags { - assertionFailure() - } - sharedBuffer.reset() var type: Int8 = 0 @@ -811,6 +810,9 @@ final class MessageHistoryTable: Table { var stableId: UInt32 = previousMessage.stableId sharedBuffer.write(&stableId, offset: 0, length: 4) + var stableVersion: UInt32 = previousMessage.stableVersion + 1 + sharedBuffer.write(&stableVersion, offset: 0, length: 4) + var flags = MessageFlags(message.flags) sharedBuffer.write(&flags.rawValue, offset: 0, length: 4) @@ -923,7 +925,7 @@ final class MessageHistoryTable: Table { self.valueBox.set(self.tableId, key: self.key(MessageIndex(message), key: sharedKey), value: sharedBuffer) - return IntermediateMessage(stableId: stableId, id: message.id, timestamp: message.timestamp, flags: flags, tags: tags, forwardInfo: intermediateForwardInfo, authorId: message.authorId, text: message.text, attributesData: attributesBuffer.makeReadBufferAndReset(), embeddedMediaData: embeddedMediaBuffer.makeReadBufferAndReset(), referencedMedia: referencedMedia) + return IntermediateMessage(stableId: stableId, stableVersion: stableVersion, id: message.id, timestamp: message.timestamp, flags: flags, tags: tags, forwardInfo: intermediateForwardInfo, authorId: message.authorId, text: message.text, attributesData: attributesBuffer.makeReadBufferAndReset(), embeddedMediaData: embeddedMediaBuffer.makeReadBufferAndReset(), referencedMedia: referencedMedia) } else { return nil } @@ -932,7 +934,7 @@ final class MessageHistoryTable: Table { private func justUpdateTimestamp(_ index: MessageIndex, timestamp: Int32) { if let previousMessage = self.getMessage(index) { self.valueBox.remove(self.tableId, key: self.key(index)) - var updatedMessage = IntermediateMessage(stableId: previousMessage.stableId, id: previousMessage.id, timestamp: timestamp, flags: previousMessage.flags, tags: previousMessage.tags, forwardInfo: previousMessage.forwardInfo, authorId: previousMessage.authorId, text: previousMessage.text, attributesData: previousMessage.attributesData, embeddedMediaData: previousMessage.embeddedMediaData, referencedMedia: previousMessage.referencedMedia) + var updatedMessage = IntermediateMessage(stableId: previousMessage.stableId, stableVersion: previousMessage.stableVersion + 1, id: previousMessage.id, timestamp: timestamp, flags: previousMessage.flags, tags: previousMessage.tags, forwardInfo: previousMessage.forwardInfo, authorId: previousMessage.authorId, text: previousMessage.text, attributesData: previousMessage.attributesData, embeddedMediaData: previousMessage.embeddedMediaData, referencedMedia: previousMessage.referencedMedia) self.storeIntermediateMessage(updatedMessage, sharedKey: self.key(index)) let tags = previousMessage.tags.rawValue @@ -985,7 +987,7 @@ final class MessageHistoryTable: Table { if let extractedMedia = extractedMedia { var updatedReferencedMedia = message.referencedMedia updatedReferencedMedia.append(extractedMedia.id!) - self.storeIntermediateMessage(IntermediateMessage(stableId: message.stableId, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: updatedEmbeddedMediaBuffer.readBufferNoCopy(), referencedMedia: updatedReferencedMedia), sharedKey: self.key(index)) + self.storeIntermediateMessage(IntermediateMessage(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: updatedEmbeddedMediaBuffer.readBufferNoCopy(), referencedMedia: updatedReferencedMedia), sharedKey: self.key(index)) return extractedMedia } @@ -1002,6 +1004,9 @@ final class MessageHistoryTable: Table { var stableId: UInt32 = message.stableId sharedBuffer.write(&stableId, offset: 0, length: 4) + var stableVersion: UInt32 = message.stableVersion + sharedBuffer.write(&stableVersion, offset: 0, length: 4) + var flagsValue: UInt32 = message.flags.rawValue sharedBuffer.write(&flagsValue, offset: 0, length: 4) @@ -1079,6 +1084,9 @@ final class MessageHistoryTable: Table { var stableId: UInt32 = 0 value.read(&stableId, offset: 0, length: 4) + var stableVersion: UInt32 = 0 + value.read(&stableVersion, offset: 0, length: 4) + var flagsValue: UInt32 = 0 value.read(&flagsValue, offset: 0, length: 4) let flags = MessageFlags(rawValue: flagsValue) @@ -1170,7 +1178,7 @@ final class MessageHistoryTable: Table { referencedMediaIds.append(MediaId(namespace: idNamespace, id: idId)) } - return .Message(IntermediateMessage(stableId: stableId, id: index.id, timestamp: index.timestamp, flags: flags, tags: tags, forwardInfo: forwardInfo, authorId: authorId, text: text, attributesData: attributesData, embeddedMediaData: embeddedMediaData, referencedMedia: referencedMediaIds)) + return .Message(IntermediateMessage(stableId: stableId, stableVersion: stableVersion, id: index.id, timestamp: index.timestamp, flags: flags, tags: tags, forwardInfo: forwardInfo, authorId: authorId, text: text, attributesData: attributesData, embeddedMediaData: embeddedMediaData, referencedMedia: referencedMediaIds)) } else { var stableId: UInt32 = 0 value.read(&stableId, offset: 0, length: 4) @@ -1252,6 +1260,7 @@ final class MessageHistoryTable: Table { } } + var associatedMessageIds: [MessageId] = [] var associatedMessages = SimpleDictionary() for attribute in parsedAttributes { for peerId in attribute.associatedPeerIds { @@ -1259,6 +1268,7 @@ final class MessageHistoryTable: Table { peers[peer.id] = peer } } + associatedMessageIds.append(contentsOf: attribute.associatedMessageIds) for messageId in attribute.associatedMessageIds { if let entry = self.messageHistoryIndexTable.get(messageId) { if case let .Message(index) = entry { @@ -1270,7 +1280,7 @@ final class MessageHistoryTable: Table { } } - return Message(stableId: message.stableId, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: forwardInfo, author: author, text: message.text, attributes: parsedAttributes, media: parsedMedia, peers: peers, associatedMessages: associatedMessages) + return Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: forwardInfo, author: author, text: message.text, attributes: parsedAttributes, media: parsedMedia, peers: peers, associatedMessages: associatedMessages, associatedMessageIds: associatedMessageIds) } func entriesAround(_ index: MessageIndex, count: Int, operationsByPeerId: inout [PeerId: [MessageHistoryOperation]], unsentMessageOperations: inout [IntermediateMessageHistoryUnsentOperation], updatedPeerReadStateOperations: inout [PeerId: PeerReadStateSynchronizationOperation?]) -> (entries: [IntermediateMessageHistoryEntry], lower: IntermediateMessageHistoryEntry?, upper: IntermediateMessageHistoryEntry?) { diff --git a/Postbox/MessageHistoryView.swift b/Postbox/MessageHistoryView.swift index 631ab927e2..a1e9d9b744 100644 --- a/Postbox/MessageHistoryView.swift +++ b/Postbox/MessageHistoryView.swift @@ -50,10 +50,10 @@ enum MutableMessageHistoryEntry { func updatedTimestamp(_ timestamp: Int32) -> MutableMessageHistoryEntry { switch self { case let .IntermediateMessageEntry(message, location): - var updatedMessage = IntermediateMessage(stableId: message.stableId, id: message.id, timestamp: timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: message.embeddedMediaData, referencedMedia: message.referencedMedia) + var updatedMessage = IntermediateMessage(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, timestamp: timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: message.embeddedMediaData, referencedMedia: message.referencedMedia) return .IntermediateMessageEntry(updatedMessage, location) case let .MessageEntry(message, location): - var updatedMessage = Message(stableId: message.stableId, id: message.id, timestamp: timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: message.peers, associatedMessages: message.associatedMessages) + var updatedMessage = Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, timestamp: timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds) return .MessageEntry(updatedMessage, location) case let .HoleEntry(hole, location): var updatedHole = MessageHistoryHole(stableId: hole.stableId, maxIndex: MessageIndex(id: hole.maxIndex.id, timestamp: timestamp), min: hole.min, tags: hole.tags) @@ -208,16 +208,10 @@ final class MutableMessageHistoryView { } for _ in 0 ..< self.entries.count - 1 - maxClipIndex { - /*if case let .MessageEntry(message) = self.entries.last! { - print("remove last \(message.text)") - }*/ self.entries.removeLast() } for _ in 0 ..< minClipIndex { - /*if case let .MessageEntry(message) = self.entries.first! { - print("remove first \(message.text)") - }*/ self.entries.removeFirst() } @@ -258,7 +252,7 @@ final class MutableMessageHistoryView { return true } - func replay(_ operations: [MessageHistoryOperation], holeFillDirections: [MessageIndex: HoleFillDirection], updatedMedia: [MediaId: Media?], context: MutableMessageHistoryViewReplayContext) -> Bool { + func replay(_ operations: [MessageHistoryOperation], holeFillDirections: [MessageIndex: HoleFillDirection], updatedMedia: [MediaId: Media?], context: MutableMessageHistoryViewReplayContext, renderIntermediateMessage: (IntermediateMessage) -> Message) -> Bool { let tagMask = self.tagMask let unwrappedTagMask: UInt32 = tagMask?.rawValue ?? 0 @@ -287,7 +281,7 @@ final class MutableMessageHistoryView { case let .UpdateEmbeddedMedia(index, embeddedMediaData): for i in 0 ..< self.entries.count { if case let .IntermediateMessageEntry(message, _) = self.entries[i] , MessageIndex(message) == index { - self.entries[i] = .IntermediateMessageEntry(IntermediateMessage(stableId: message.stableId, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: embeddedMediaData, referencedMedia: message.referencedMedia), nil) + self.entries[i] = .IntermediateMessageEntry(IntermediateMessage(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, authorId: message.authorId, text: message.text, attributesData: message.attributesData, embeddedMediaData: embeddedMediaData, referencedMedia: message.referencedMedia), nil) hasChanges = true break } @@ -328,7 +322,7 @@ final class MutableMessageHistoryView { messageMedia.append(media) } } - let updatedMessage = Message(stableId: message.stableId, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: messageMedia, peers: message.peers, associatedMessages: message.associatedMessages) + let updatedMessage = Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: messageMedia, peers: message.peers, associatedMessages: message.associatedMessages, associatedMessageIds: message.associatedMessageIds) self.entries[i] = .MessageEntry(updatedMessage, nil) hasChanges = true } @@ -359,6 +353,33 @@ final class MutableMessageHistoryView { } } + for operation in operations { + switch operation { + case let .InsertMessage(intermediateMessage): + for i in 0 ..< self.entries.count { + switch self.entries[i] { + case let .MessageEntry(message, location): + if message.associatedMessageIds.count != message.associatedMessages.count { + if message.associatedMessageIds.contains(intermediateMessage.id) && message.associatedMessages[intermediateMessage.id] == nil { + var updatedAssociatedMessages = message.associatedMessages + let renderedMessage = renderIntermediateMessage(intermediateMessage) + updatedAssociatedMessages[intermediateMessage.id] = renderedMessage + let updatedMessage = Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: message.peers, associatedMessages: updatedAssociatedMessages, associatedMessageIds: message.associatedMessageIds) + self.entries[i] = .MessageEntry(updatedMessage, location) + hasChanges = true + } + } + break + default: + break + } + } + break + default: + break + } + } + return hasChanges } @@ -419,6 +440,11 @@ final class MutableMessageHistoryView { hasChanges = true } + var ids = Set() + for index in indices { + ids.insert(index.id) + } + if self.entries.count != 0 { var i = self.entries.count - 1 while i >= 0 { @@ -426,6 +452,16 @@ final class MutableMessageHistoryView { self.entries.remove(at: i) context.removedEntries = true hasChanges = true + } else { + switch self.entries[i] { + case let .MessageEntry(message, location): + if let updatedAssociatedMessages = message.associatedMessages.filteredOut(keysIn: ids) { + let updatedMessage = Message(stableId: message.stableId, stableVersion: message.stableVersion, id: message.id, timestamp: message.timestamp, flags: message.flags, tags: message.tags, forwardInfo: message.forwardInfo, author: message.author, text: message.text, attributes: message.attributes, media: message.media, peers: message.peers, associatedMessages: updatedAssociatedMessages, associatedMessageIds: message.associatedMessageIds) + self.entries[i] = .MessageEntry(updatedMessage, location) + } + default: + break + } } i -= 1 } diff --git a/Postbox/PeerChatTopTaggedMessageIds.swift b/Postbox/PeerChatTopTaggedMessageIds.swift new file mode 100644 index 0000000000..f7ac6d0dd5 --- /dev/null +++ b/Postbox/PeerChatTopTaggedMessageIds.swift @@ -0,0 +1,15 @@ +import Foundation + +final class PeerChatTopTaggedMessageIdsTable: Table { + private var cachedTopIds: [PeerId: [MessageId.Namespace: MessageId?]] = [:] + private var updatedPeerIds = Set() + + private let sharedKey = ValueBoxKey(length: 8 + 4 + 4) + + override func beforeCommit() { + for peerId in self.updatedPeerIds { + + } + self.updatedPeerIds.removeAll() + } +} diff --git a/Postbox/Postbox.swift b/Postbox/Postbox.swift index 333a096336..7612ea2b8c 100644 --- a/Postbox/Postbox.swift +++ b/Postbox/Postbox.swift @@ -335,7 +335,7 @@ public final class Postbox { self.metadataTable = MetadataTable(valueBox: self.valueBox, tableId: 0) let userVersion: Int32? = self.metadataTable.userVersion() - let currentUserVersion: Int32 = 14 + let currentUserVersion: Int32 = 15 if userVersion != currentUserVersion { self.valueBox.drop() diff --git a/Postbox/SimpleDictionary.swift b/Postbox/SimpleDictionary.swift index f9a1d24591..32ae469d43 100644 --- a/Postbox/SimpleDictionary.swift +++ b/Postbox/SimpleDictionary.swift @@ -1,11 +1,45 @@ import Foundation -public struct SimpleDictionary: Sequence { +public struct SimpleDictionary: Sequence { private var items: [(K, V)] = [] + public var count: Int { + return self.items.count + } + + public var isEmpty: Bool { + return self.items.isEmpty + } + public init() { } + private init(items: [(K, V)] = []) { + self.items = items + } + + public func filteredOut(keysIn: Set) -> SimpleDictionary? { + var hasUpdates = false + for (key, _) in self.items { + if keysIn.contains(key) { + hasUpdates = true + break + } + } + if hasUpdates { + var updatedItems: [(K, V)] = [] + for (key, value) in self.items { + if !keysIn.contains(key) { + updatedItems.append((key, value)) + break + } + } + return SimpleDictionary(items: updatedItems) + } else { + return nil + } + } + public subscript(key: K) -> V? { get { for (k, value) in self.items { diff --git a/Postbox/ViewTracker.swift b/Postbox/ViewTracker.swift index 14b5b0c173..12684afbf2 100644 --- a/Postbox/ViewTracker.swift +++ b/Postbox/ViewTracker.swift @@ -253,7 +253,7 @@ final class ViewTracker { updateType = .Generic } - if mutableView.replay(operations ?? [], holeFillDirections: transaction.peerIdsWithFilledHoles[peerId] ?? [:], updatedMedia: transaction.updatedMedia, context: context) { + if mutableView.replay(operations ?? [], holeFillDirections: transaction.peerIdsWithFilledHoles[peerId] ?? [:], updatedMedia: transaction.updatedMedia, context: context, renderIntermediateMessage: self.renderMessage) { mutableView.complete(context: context, fetchEarlier: { index, count in return self.fetchEarlierHistoryEntries(peerId, index, count, mutableView.tagMask) }, fetchLater: { index, count in diff --git a/PostboxTests/ChatListTableTests.swift b/PostboxTests/ChatListTableTests.swift index 3178b94934..f0e6bd6c36 100644 --- a/PostboxTests/ChatListTableTests.swift +++ b/PostboxTests/ChatListTableTests.swift @@ -6,6 +6,8 @@ import XCTest import Postbox @testable import Postbox +import SwiftSignalKit + private let namespace: Int32 = 1 private let authorPeerId = PeerId(namespace: 2, id: 3) @@ -75,6 +77,7 @@ class ChatListTableTests: XCTestCase { var tagsTable: MessageHistoryTagsTable? var readStateTable: MessageHistoryReadStateTable? var synchronizeReadStateTable: MessageHistorySynchronizeReadStateTable? + var peerChatInterfaceStateTable: PeerChatInterfaceStateTable? override class func setUp() { super.setUp() @@ -86,7 +89,7 @@ class ChatListTableTests: XCTestCase { var randomId: Int64 = 0 arc4random_buf(&randomId, 8) path = NSTemporaryDirectory() + "\(randomId)" - self.valueBox = SqliteValueBox(basePath: path!) + self.valueBox = SqliteValueBox(basePath: path!, queue: Queue.mainQueue()) let seedConfiguration = SeedConfiguration(initializeChatListWithHoles: [], initializeMessageNamespacesWithHoles: [], existingMessageTags: []) @@ -102,6 +105,7 @@ class ChatListTableTests: XCTestCase { 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!, synchronizeReadStateTable: self.synchronizeReadStateTable!) self.chatListIndexTable = ChatListIndexTable(valueBox: self.valueBox!, tableId: 5) self.chatListTable = ChatListTable(valueBox: self.valueBox!, tableId: 6, indexTable: self.chatListIndexTable!, metadataTable: self.historyMetadataTable!, seedConfiguration: seedConfiguration) + self.peerChatInterfaceStateTable = PeerChatInterfaceStateTable(valueBox: self.valueBox!, tableId: 20) } override func tearDown() { @@ -123,18 +127,20 @@ class ChatListTableTests: XCTestCase { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + let updatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?] = [:] self.historyTable!.addMessages([StoreMessage(id: MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id), timestamp: timestamp, flags: [], tags: [], forwardInfo: nil, authorId: authorPeerId, text: text, attributes: [], media: media)], location: .Random, operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) var operations: [ChatListOperation] = [] - self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) + self.chatListTable!.replay(historyOperationsByPeerId: operationsByPeerId, updatedPeerChatListEmbeddedStates: updatedPeerChatListEmbeddedStates, messageHistoryTable: self.historyTable!, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable!, operations: &operations) } private func addHole(_ peerId: Int32, _ id: Int32) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + let updatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?] = [:] self.historyTable!.addHoles([MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id)], operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) var operations: [ChatListOperation] = [] - self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) + self.chatListTable!.replay(historyOperationsByPeerId: operationsByPeerId, updatedPeerChatListEmbeddedStates: updatedPeerChatListEmbeddedStates, messageHistoryTable: self.historyTable!, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable!, operations: &operations) } private func addChatListHole(_ peerId: Int32, _ id: Int32, _ timestamp: Int32) { @@ -156,24 +162,26 @@ class ChatListTableTests: XCTestCase { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + let updatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?] = [:] self.historyTable!.removeMessages(ids.map({ MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: $0) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) var operations: [ChatListOperation] = [] - self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) + self.chatListTable!.replay(historyOperationsByPeerId: operationsByPeerId, updatedPeerChatListEmbeddedStates: updatedPeerChatListEmbeddedStates, messageHistoryTable: self.historyTable!, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable!, operations: &operations) } private func fillHole(_ peerId: Int32, _ id: Int32, _ fillType: HoleFill, _ messages: [(Int32, Int32, String, [Media])]) { var operationsByPeerId: [PeerId: [MessageHistoryOperation]] = [:] var unsentMessageOperations: [IntermediateMessageHistoryUnsentOperation] = [] var updatedPeerReadStateOperations: [PeerId: PeerReadStateSynchronizationOperation?] = [:] + let updatedPeerChatListEmbeddedStates: [PeerId: PeerChatListEmbeddedInterfaceState?] = [:] self.historyTable!.fillHole(MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: id), fillType: fillType, tagMask: nil, messages: messages.map({ StoreMessage(id: MessageId(peerId: PeerId(namespace: namespace, id: peerId), namespace: namespace, id: $0.0), timestamp: $0.1, flags: [], tags: [], forwardInfo: nil, authorId: authorPeerId, text: $0.2, attributes: [], media: $0.3) }), operationsByPeerId: &operationsByPeerId, unsentMessageOperations: &unsentMessageOperations, updatedPeerReadStateOperations: &updatedPeerReadStateOperations) var operations: [ChatListOperation] = [] - self.chatListTable!.replay(operationsByPeerId, messageHistoryTable: self.historyTable!, operations: &operations) + self.chatListTable!.replay(historyOperationsByPeerId: operationsByPeerId, updatedPeerChatListEmbeddedStates: updatedPeerChatListEmbeddedStates, messageHistoryTable: self.historyTable!, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable!, operations: &operations) } private func expectEntries(_ entries: [Entry]) { - let actualEntries = self.chatListTable!.debugList(self.historyTable!).map({ entry -> Entry in + let actualEntries = self.chatListTable!.debugList(self.historyTable!, peerChatInterfaceStateTable: self.peerChatInterfaceStateTable!).map({ entry -> Entry in switch entry { - case let .Message(message): + case let .Message(_, message, _): if message.authorId != authorPeerId { XCTFail("Expected authorId \(authorPeerId), actual \(message.authorId)") } diff --git a/PostboxTests/MessageHistoryIndexTableTests.swift b/PostboxTests/MessageHistoryIndexTableTests.swift index 1875a1e6f9..c39f0cb771 100644 --- a/PostboxTests/MessageHistoryIndexTableTests.swift +++ b/PostboxTests/MessageHistoryIndexTableTests.swift @@ -6,6 +6,8 @@ import XCTest import Postbox @testable import Postbox +import SwiftSignalKit + private let peerId = PeerId(namespace: 1, id: 1) private let namespace: Int32 = 1 @@ -73,7 +75,7 @@ class MessageHistoryIndexTableTests: XCTestCase { var randomId: Int64 = 0 arc4random_buf(&randomId, 8) path = NSTemporaryDirectory() + "\(randomId)" - self.valueBox = SqliteValueBox(basePath: path!) + self.valueBox = SqliteValueBox(basePath: path!, queue: Queue.mainQueue()) let seedConfiguration = SeedConfiguration(initializeChatListWithHoles: [], initializeMessageNamespacesWithHoles: [], existingMessageTags: []) diff --git a/PostboxTests/MessageHistoryTableTests.swift b/PostboxTests/MessageHistoryTableTests.swift index 0dc11db243..7c6775adf7 100644 --- a/PostboxTests/MessageHistoryTableTests.swift +++ b/PostboxTests/MessageHistoryTableTests.swift @@ -6,6 +6,8 @@ import XCTest import Postbox @testable import Postbox +import SwiftSignalKit + private let peerId = PeerId(namespace: 1, id: 1) private let namespace: Int32 = 1 private let authorPeerId = PeerId(namespace: 1, id: 6) @@ -120,6 +122,10 @@ private class TestExternalMedia: Media { } private class TestPeer: Peer { + var indexName: PeerIndexNameRepresentation { + return .title("Test") + } + let id: PeerId let data: String @@ -160,7 +166,7 @@ private enum MediaEntry: Equatable { case let .Direct(media, referenceCount): self = .Direct(media, referenceCount) case let .MessageReference(index): - self = MessageReference(index.id.id) + self = .MessageReference(index.id.id) } } } @@ -219,7 +225,7 @@ class MessageHistoryTableTests: XCTestCase { var randomId: Int64 = 0 arc4random_buf(&randomId, 8) path = NSTemporaryDirectory() + "\(randomId)" - self.valueBox = SqliteValueBox(basePath: path!) + self.valueBox = SqliteValueBox(basePath: path!, queue: Queue.mainQueue()) let seedConfiguration = SeedConfiguration(initializeChatListWithHoles: [], initializeMessageNamespacesWithHoles: [], existingMessageTags: [.First, .Second]) @@ -330,12 +336,12 @@ class MessageHistoryTableTests: XCTestCase { } } - private func expectUnsent(_ indices: [(Int32, Int32)]) { - let actualUnsent = self.unsentTable!.get().map({ ($0.id.id, $0.timestamp) }) + private func expectUnsent(_ indices: [Int32]) { + let actualUnsent = self.unsentTable!.get().map({ $0.id }) var match = true if actualUnsent.count == indices.count { for i in 0 ..< indices.count { - if indices[i].0 != actualUnsent[i].0 || indices[i].1 != actualUnsent[i].1 { + if indices[i] != actualUnsent[i] { match = false break } @@ -778,13 +784,13 @@ class MessageHistoryTableTests: XCTestCase { func testAddUnsent() { addMessage(100, 100, "m100", [], [.Unsent]) expectEntries([.Message(100, 100, "m100", [], [.Unsent])]) - expectUnsent([(100, 100)]) + expectUnsent([100]) } func testRemoveUnsent() { addMessage(100, 100, "m100", [], [.Unsent]) expectEntries([.Message(100, 100, "m100", [], [.Unsent])]) - expectUnsent([(100, 100)]) + expectUnsent([100]) removeMessages([100]) expectEntries([]) @@ -794,7 +800,7 @@ class MessageHistoryTableTests: XCTestCase { func testUpdateUnsentToSentSameIndex() { addMessage(100, 100, "m100", [], [.Unsent]) expectEntries([.Message(100, 100, "m100", [], [.Unsent])]) - expectUnsent([(100, 100)]) + expectUnsent([100]) updateMessage(100, 100, 100, "m100", [], [], []) expectEntries([.Message(100, 100, "m100", [], [])]) @@ -804,7 +810,7 @@ class MessageHistoryTableTests: XCTestCase { func testUpdateUnsentToFailed() { addMessage(100, 100, "m100", [], [.Unsent]) expectEntries([.Message(100, 100, "m100", [], [.Unsent])]) - expectUnsent([(100, 100)]) + expectUnsent([100]) updateMessage(100, 100, 100, "m100", [], [.Unsent, .Failed], []) expectEntries([.Message(100, 100, "m100", [], [.Unsent, .Failed])]) @@ -814,7 +820,7 @@ class MessageHistoryTableTests: XCTestCase { func testUpdateDifferentIndex() { addMessage(100, 100, "m100", [], [.Unsent]) expectEntries([.Message(100, 100, "m100", [], [.Unsent])]) - expectUnsent([(100, 100)]) + expectUnsent([100]) updateMessage(100, 200, 200, "m100", [], [], []) expectEntries([.Message(200, 200, "m100", [], [])]) @@ -826,7 +832,7 @@ class MessageHistoryTableTests: XCTestCase { addMessage(100, 100, "m100", [], [.Unsent]) expectEntries([.Hole(1, 99, 100), .Message(100, 100, "m100", [], [.Unsent]), .Hole(101, Int32.max, Int32.max)]) - expectUnsent([(100, 100)]) + expectUnsent([100]) updateMessage(100, 200, 200, "m100", [], [], []) expectEntries([.Hole(1, 199, 200), .Message(200, 200, "m100", [], []), .Hole(201, Int32.max, Int32.max)]) diff --git a/PostboxTests/OrderStatisticTreeTests.swift b/PostboxTests/OrderStatisticTreeTests.swift index c4779434db..2e17bf0b1e 100644 --- a/PostboxTests/OrderStatisticTreeTests.swift +++ b/PostboxTests/OrderStatisticTreeTests.swift @@ -6,6 +6,8 @@ import XCTest import Postbox @testable import Postbox +import SwiftSignalKit + private let peerId = PeerId(namespace: 1, id: 1) private let namespace: Int32 = 1 private let authorPeerId = PeerId(namespace: 1, id: 6) @@ -83,7 +85,7 @@ class OrderStatisticTreeTests: XCTestCase { var randomId: Int64 = 0 arc4random_buf(&randomId, 8) path = NSTemporaryDirectory() + "\(randomId)" - self.valueBox = SqliteValueBox(basePath: path!) + self.valueBox = SqliteValueBox(basePath: path!, queue: Queue.mainQueue()) let seedConfiguration = SeedConfiguration(initializeChatListWithHoles: [], initializeMessageNamespacesWithHoles: [], existingMessageTags: [.First, .Second]) diff --git a/PostboxTests/ReadStateTableTests.swift b/PostboxTests/ReadStateTableTests.swift index eb51113df4..8005f60b71 100644 --- a/PostboxTests/ReadStateTableTests.swift +++ b/PostboxTests/ReadStateTableTests.swift @@ -6,6 +6,8 @@ import XCTest import Postbox @testable import Postbox +import SwiftSignalKit + private let peerId = PeerId(namespace: 1, id: 1) private let namespace: Int32 = 1 private let authorPeerId = PeerId(namespace: 1, id: 6) @@ -83,7 +85,7 @@ class ReadStateTableTests: XCTestCase { var randomId: Int64 = 0 arc4random_buf(&randomId, 8) path = NSTemporaryDirectory() + "\(randomId)" - self.valueBox = SqliteValueBox(basePath: path!) + self.valueBox = SqliteValueBox(basePath: path!, queue: Queue.mainQueue()) let seedConfiguration = SeedConfiguration(initializeChatListWithHoles: [], initializeMessageNamespacesWithHoles: [], existingMessageTags: [.First, .Second])