Merge branch 'mkirk/gallery-records' into release/3.0.0

This commit is contained in:
Michael Kirk 2019-11-19 14:05:02 -07:00
commit 606e2d2aee
14 changed files with 439 additions and 101 deletions

View File

@ -3792,7 +3792,7 @@ typedef OWSContact * (^OWSContactBlock)(SDSAnyWriteTransaction *transaction);
+ (void)sendFakeMessages:(NSUInteger)counter thread:(TSThread *)thread isTextOnly:(BOOL)isTextOnly
{
const NSUInteger kMaxBatchSize = 2500;
const NSUInteger kMaxBatchSize = 200;
if (counter < kMaxBatchSize) {
[self writeWithBlock:^(SDSAnyWriteTransaction *_Nonnull transaction) {
[self sendFakeMessages:counter thread:thread isTextOnly:isTextOnly transaction:transaction];
@ -3921,33 +3921,16 @@ typedef OWSContact * (^OWSContactBlock)(SDSAnyWriteTransaction *transaction);
break;
}
case 3: {
NSString *filename = @"test.jpg";
UInt32 filesize = 16;
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:@"audio/jpg"
byteCount:filesize
sourceFilename:filename
caption:nil
albumMessageId:nil];
NSError *error;
BOOL success = [attachmentStream writeData:[self createRandomNSDataOfSize:filesize] error:&error];
OWSAssertDebug(success && !error);
[attachmentStream anyInsertWithTransaction:transaction];
[self createFakeOutgoingMessage:thread
messageBody:nil
attachmentId:attachmentStream.uniqueId
filename:filename
messageState:TSOutgoingMessageStateFailed
isDelivered:NO
isRead:NO
isVoiceMessage:NO
quotedMessage:nil
contactShare:nil
linkPreview:nil
messageSticker:nil
transaction:transaction];
ConversationFactory *conversationFactory = [ConversationFactory new];
// We want to produce a variety of album sizes, but favoring smaller albums
conversationFactory.attachmentCount = MAX(0,
MIN(SignalAttachment.maxAttachmentsAllowed,
((NSInteger)((double)UINT32_MAX/(double)arc4random()) - 1)));
conversationFactory.threadCreator = ^(SDSAnyWriteTransaction *_transaction){
return thread;
};
[conversationFactory createSentMessageWithTransaction:transaction];
break;
}
}
@ -4635,7 +4618,7 @@ typedef OWSContact * (^OWSContactBlock)(SDSAnyWriteTransaction *transaction);
return [self createFakeOutgoingMessage:thread
messageBody:messageBody
attachmentId:attachment.uniqueId
attachment:attachment
filename:fakeAssetLoader.filename
messageState:messageState
isDelivered:isDelivered
@ -4650,7 +4633,7 @@ typedef OWSContact * (^OWSContactBlock)(SDSAnyWriteTransaction *transaction);
+ (TSOutgoingMessage *)createFakeOutgoingMessage:(TSThread *)thread
messageBody:(nullable NSString *)messageBody
attachmentId:(nullable NSString *)attachmentId
attachment:(nullable TSAttachment *)attachment
filename:(nullable NSString *)filename
messageState:(TSOutgoingMessageState)messageState
isDelivered:(BOOL)isDelivered
@ -4664,11 +4647,11 @@ typedef OWSContact * (^OWSContactBlock)(SDSAnyWriteTransaction *transaction);
{
OWSAssertDebug(thread);
OWSAssertDebug(transaction);
OWSAssertDebug(messageBody.length > 0 || attachmentId.length > 0 || contactShare);
OWSAssertDebug(messageBody.length > 0 || attachment != nil || contactShare);
NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
if (attachmentId) {
[attachmentIds addObject:attachmentId];
if (attachment != nil) {
[attachmentIds addObject:attachment.uniqueId];
}
// MJK TODO - remove senderTimestamp
@ -4689,6 +4672,11 @@ typedef OWSContact * (^OWSContactBlock)(SDSAnyWriteTransaction *transaction);
[message anyInsertWithTransaction:transaction];
[message updateWithFakeMessageState:messageState transaction:transaction];
[attachment anyUpdateWithTransaction:transaction block:^(TSAttachment *latest) {
// There's no public setter for albumMessageId, since it's usually set in the initializer.
// This isn't convenient for the DEBUG UI, so we abuse the migrateAlbumMessageId method.
[latest migrateAlbumMessageId:message.uniqueId];
}];
if (isDelivered) {
SignalServiceAddress *_Nullable address = thread.recipientAddresses.lastObject;
OWSAssertDebug(address.isValid);

View File

@ -575,6 +575,7 @@ class MediaGallery {
func ensureLoadedForMostRecentTileView() -> MediaGalleryItem? {
guard let mostRecentItem: MediaGalleryItem = (databaseStorage.uiReadReturningResult { transaction in
// POST GRDB - fetch messages w/ attachments rather than separate query
guard let attachment = self.mediaGalleryFinder.mostRecentMediaAttachment(transaction: transaction) else {
return nil
}

View File

@ -64,10 +64,6 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark -
typedef void (^BuildOutgoingMessageCompletionBlock)(TSOutgoingMessage *savedMessage,
NSMutableArray<OWSOutgoingAttachmentInfo *> *attachmentInfos,
SDSAnyWriteTransaction *writeTransaction);
@implementation ThreadUtil
#pragma mark - Dependencies
@ -693,8 +689,12 @@ typedef void (^BuildOutgoingMessageCompletionBlock)(TSOutgoingMessage *savedMess
[TSInteraction anyRemoveAllWithInstantationWithTransaction:transaction];
[TSAttachment anyRemoveAllWithInstantationWithTransaction:transaction];
[SignalRecipient anyRemoveAllWithInstantationWithTransaction:transaction];
// Deleting attachments above should be enough to remove any gallery items, but
// we redunantly clean up *all* gallery items to be safe.
[AnyMediaGalleryFinder didRemoveAllContentWithTransaction:transaction];
}];
[TSAttachmentStream deleteAttachments];
[TSAttachmentStream deleteAttachmentsFromDisk];
}
#pragma mark - Find Content

View File

@ -711,3 +711,27 @@ CREATE
ON "model_SignalAccount"("recipientUUID"
)
;
CREATE
TABLE
IF NOT EXISTS "media_gallery_items" (
"attachmentId" INTEGER NOT NULL UNIQUE
,"albumMessageId" INTEGER NOT NULL
,"threadId" INTEGER NOT NULL
,"originalAlbumOrder" INTEGER NOT NULL
)
;
CREATE
INDEX "index_media_gallery_items_for_gallery"
ON "media_gallery_items"("threadId"
,"albumMessageId"
,"originalAlbumOrder"
)
;
CREATE
INDEX "index_media_gallery_items_on_attachmentId"
ON "media_gallery_items"("attachmentId"
)
;

View File

@ -374,8 +374,8 @@ typedef void (^AttachmentDownloadFailure)(NSError *error);
if (![existingAttachment isKindOfClass:[TSAttachmentPointer class]]) {
OWSFailDebug(@"Unexpected attachment pointer class: %@", existingAttachment.class);
}
// This will clobber the attachmentPointer
[attachmentStream anyOverwritingUpdateWithTransaction:transaction];
[attachmentPointer anyRemoveWithTransaction:transaction];
[attachmentStream anyInsertWithTransaction:transaction];
if (job.message != nil) {
[self reloadAndTouchLatestVersionOfMessage:job.message transaction:transaction];

View File

@ -120,7 +120,7 @@ NS_SWIFT_NAME(init(grdbId:uniqueId:albumMessageId:attachmentType:blurHash:byteCo
- (BOOL)writeConsumingDataSource:(id<DataSource>)dataSource
error:(NSError **)error NS_SWIFT_NAME(writeConsumingDataSource(_:));
+ (void)deleteAttachments;
+ (void)deleteAttachmentsFromDisk;
+ (NSString *)attachmentsFolder;
+ (NSString *)legacyAttachmentsDirPath;

View File

@ -458,11 +458,18 @@ typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail);
}
}
- (void)anyDidInsertWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyDidInsertWithTransaction:transaction];
[AnyMediaGalleryFinder didInsertAttachmentStream:self transaction:transaction];
}
- (void)anyDidRemoveWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyDidRemoveWithTransaction:transaction];
[self removeFile];
[AnyMediaGalleryFinder didRemoveAttachmentStream:self transaction:transaction];
}
- (BOOL)isValidVisualMedia
@ -607,7 +614,7 @@ typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail);
return image;
}
+ (void)deleteAttachments
+ (void)deleteAttachmentsFromDisk
{
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];

View File

@ -247,7 +247,7 @@ public class GRDBDatabaseStorageAdapter: NSObject {
deleteDBKeys()
if (CurrentAppContext().isMainApp) {
TSAttachmentStream.deleteAttachments()
TSAttachmentStream.deleteAttachmentsFromDisk()
}
// TODO: Delete Profiles on Disk?

View File

@ -46,14 +46,14 @@ public class GRDBSchemaMigrator: NSObject {
case signalAccount_add_contactAvatars
case signalAccount_add_contactAvatars_indices
case jobRecords_add_attachmentId
case createMediaGalleryItems
// NOTE: Every time we add a migration id, consider
// incrementing grdbSchemaVersionLatest.
// We only need to do this for breaking changes.
}
public static let grdbSchemaVersionDefault: UInt = 0
public static let grdbSchemaVersionLatest: UInt = 1
public static let grdbSchemaVersionLatest: UInt = 2
// An optimization for new users, we have the first migration import the latest schema
// and mark any other migrations as "already run".
@ -109,6 +109,7 @@ public class GRDBSchemaMigrator: NSObject {
"""
try database.execute(sql: sql)
}
migrator.registerMigration(MigrationId.signalAccount_add_contactAvatars_indices.rawValue) { db in
let sql = """
CREATE
@ -138,6 +139,30 @@ public class GRDBSchemaMigrator: NSObject {
}
}
migrator.registerMigration(MigrationId.createMediaGalleryItems.rawValue) { db in
try db.create(table: "media_gallery_items") { table in
table.column("attachmentId", .integer)
.notNull()
.unique()
table.column("albumMessageId", .integer)
.notNull()
table.column("threadId", .integer)
.notNull()
table.column("originalAlbumOrder", .integer)
.notNull()
}
try db.create(index: "index_media_gallery_items_for_gallery",
on: "media_gallery_items",
columns: ["threadId", "albumMessageId", "originalAlbumOrder"])
try db.create(index: "index_media_gallery_items_on_attachmentId",
on: "media_gallery_items",
columns: ["attachmentId"])
try createInitialGalleryRecords(transaction: GRDBWriteTransaction(database: db))
}
return migrator
}()
}
@ -774,3 +799,31 @@ private func createV1Schema(db: Database) throws {
try GRDBFullTextSearchFinder.createTables(database: db)
}
func createInitialGalleryRecords(transaction: GRDBWriteTransaction) throws {
try Bench(title: "createInitialGalleryRecords", logInProduction: true) {
let scope = AttachmentRecord.filter(sql: "\(attachmentColumn: .recordType) = \(SDSRecordType.attachmentStream.rawValue)")
let totalCount = try scope.fetchCount(transaction.database)
let cursor = try scope.fetchCursor(transaction.database)
var i = 0
try Batching.loop(batchSize: 500) { stopPtr in
guard let record = try cursor.next() else {
stopPtr.pointee = true
return
}
i+=1
if (i % 100) == 0 {
Logger.info("migrated \(i) / \(totalCount)")
}
guard let attachmentStream = try TSAttachment.fromRecord(record) as? TSAttachmentStream else {
owsFailDebug("unexpected record: \(record.recordType)")
return
}
try GRDBMediaGalleryFinder.insertGalleryRecord(attachmentStream: attachmentStream, transaction: transaction)
}
}
}

View File

@ -40,6 +40,8 @@ public class UIDatabaseObserver: NSObject {
public static let kMaxIncrementalRowChanges = 200
private lazy var nonModelTables: Set<String> = Set([MediaGalleryRecord.databaseTableName])
// tldr; Instead, of protecting UIDatabaseObserver state with a nested DispatchQueue,
// which would break GRDB's SchedulingWatchDog, we use objc_sync
//
@ -116,6 +118,11 @@ extension UIDatabaseObserver: TransactionObserver {
return false
}
guard !nonModelTables.contains(eventKind.tableName) else {
// Ignore updates to non-model tables
return false
}
return true
}

View File

@ -14,7 +14,8 @@ protocol MediaGalleryFinder {
func enumerateMediaAttachments(range: NSRange, transaction: ReadTransaction, block: @escaping (TSAttachment) -> Void)
}
public class AnyMediaGalleryFinder {
@objc
public class AnyMediaGalleryFinder: NSObject {
public typealias ReadTransaction = SDSAnyReadTransaction
public lazy var yapAdapter = {
@ -70,6 +71,48 @@ extension AnyMediaGalleryFinder: MediaGalleryFinder {
return yapAdapter.mostRecentMediaAttachment(transaction: yapRead)
}
}
@objc(didInsertAttachmentStream:transaction:)
public class func didInsert(attachmentStream: TSAttachmentStream, transaction: SDSAnyWriteTransaction) {
switch transaction.writeTransaction {
case .yapWrite:
break
case .grdbWrite(let grdbWrite):
do {
try GRDBMediaGalleryFinder.insertGalleryRecord(attachmentStream: attachmentStream, transaction: grdbWrite)
} catch {
owsFailDebug("error: \(error)")
}
}
}
@objc(didRemoveAttachmentStream:transaction:)
public class func didRemove(attachmentStream: TSAttachmentStream, transaction: SDSAnyWriteTransaction) {
switch transaction.writeTransaction {
case .yapWrite:
break
case .grdbWrite(let grdbWrite):
do {
try GRDBMediaGalleryFinder.removeAnyGalleryRecord(attachmentStream: attachmentStream, transaction: grdbWrite)
} catch {
owsFailDebug("error: \(error)")
}
}
}
@objc
public class func didRemoveAllContent(transaction: SDSAnyWriteTransaction) {
switch transaction.writeTransaction {
case .yapWrite:
break
case .grdbWrite(let grdbWrite):
do {
try GRDBMediaGalleryFinder.removeAllGalleryRecords(transaction: grdbWrite)
} catch {
owsFailDebug("error: \(error)")
}
}
}
}
// MARK: - GRDB
@ -84,8 +127,12 @@ public class GRDBMediaGalleryFinder: NSObject {
// MARK: -
var threadUniqueId: String {
return thread.uniqueId
var threadId: Int64 {
guard let rowId = thread.grdbId else {
owsFailDebug("thread.grdbId was unexpectedly nil")
return 0
}
return rowId.int64Value
}
public static func setup(storage: GRDBDatabaseStorageAdapter) {
@ -99,6 +146,76 @@ public class GRDBMediaGalleryFinder: NSObject {
return MIMETypeUtil.isVisualMedia(contentType)
}
public class func removeAnyGalleryRecord(attachmentStream: TSAttachmentStream, transaction: GRDBWriteTransaction) throws {
let sql = """
DELETE FROM \(MediaGalleryRecord.databaseTableName) WHERE attachmentId = ?
"""
guard let attachmentId = attachmentStream.grdbId else {
owsFailDebug("attachmentId was unexpectedly nil")
return
}
guard attachmentStream.albumMessageId != nil else {
Logger.verbose("not a gallery attachment")
return
}
try transaction.database.execute(sql: sql, arguments: [attachmentId.int64Value])
}
public class func insertGalleryRecord(attachmentStream: TSAttachmentStream, transaction: GRDBWriteTransaction) throws {
guard let attachmentRowId = attachmentStream.grdbId else {
owsFailDebug("attachmentRowId was unexpectedly nil")
return
}
guard let messageUniqueId = attachmentStream.albumMessageId else {
Logger.verbose("not a gallery attachment")
return
}
guard let message = TSMessage.anyFetchMessage(uniqueId: messageUniqueId, transaction: transaction.asAnyRead) else {
owsFailDebug("message was unexpectedly nil")
return
}
guard let messageRowId = message.grdbId else {
owsFailDebug("message was unexpectedly nil")
return
}
let thread = message.thread(transaction: transaction.asAnyRead)
guard let threadId = thread.grdbId else {
owsFailDebug("threadId was unexpectedly nil")
return
}
guard let originalAlbumIndex = message.attachmentIds.firstIndex(of: attachmentStream.uniqueId) else {
owsFailDebug("originalAlbumIndex was unexpectedly nil")
return
}
let galleryRecord = MediaGalleryRecord(attachmentId: attachmentRowId.int64Value,
albumMessageId: messageRowId.int64Value,
threadId: threadId.int64Value,
originalAlbumOrder: originalAlbumIndex)
try galleryRecord.insert(transaction.database)
}
public class func removeAllGalleryRecords(transaction: GRDBWriteTransaction) throws {
try MediaGalleryRecord.deleteAll(transaction.database)
}
}
struct MediaGalleryRecord: Codable, FetchableRecord, PersistableRecord {
static let databaseTableName = "media_gallery_items"
let attachmentId: Int64
let albumMessageId: Int64
let threadId: Int64
let originalAlbumOrder: Int
}
extension GRDBMediaGalleryFinder: MediaGalleryFinder {
@ -107,62 +224,59 @@ extension GRDBMediaGalleryFinder: MediaGalleryFinder {
func mostRecentMediaAttachment(transaction: GRDBReadTransaction) -> TSAttachment? {
let sql = """
SELECT \(AttachmentRecord.databaseTableName).*
FROM \(AttachmentRecord.databaseTableName)
INNER JOIN \(InteractionRecord.databaseTableName)
ON \(attachmentColumn: .albumMessageId) = \(interactionColumnFullyQualified: .uniqueId)
AND \(interactionColumn: .threadUniqueId) = ?
AND \(interactionColumn: .isViewOnceMessage) = FALSE
WHERE \(attachmentColumnFullyQualified: .recordType) = \(SDSRecordType.attachmentStream.rawValue)
AND \(attachmentColumn: .albumMessageId) IS NOT NULL
FROM "media_gallery_items"
INNER JOIN \(AttachmentRecord.databaseTableName)
ON media_gallery_items.attachmentId = model_TSAttachment.id
AND IsVisualMediaContentType(\(attachmentColumn: .contentType)) IS TRUE
INNER JOIN \(InteractionRecord.databaseTableName)
ON media_gallery_items.albumMessageId = \(interactionColumnFullyQualified: .id)
AND \(interactionColumn: .isViewOnceMessage) = FALSE
WHERE media_gallery_items.threadId = ?
ORDER BY
\(interactionColumnFullyQualified: .id) DESC,
\(attachmentColumnFullyQualified: .id) DESC
media_gallery_items.albumMessageId DESC,
media_gallery_items.originalAlbumOrder DESC
LIMIT 1
"""
// GRDB TODO: migrate such that attachment.id reflects ordering in TSInteraction.attachmentIds
let cursor = TSAttachment.grdbFetchCursor(sql: sql, arguments: [threadUniqueId], transaction: transaction)
let cursor = TSAttachment.grdbFetchCursor(sql: sql, arguments: [threadId], transaction: transaction)
return try! cursor.next()
}
func mediaCount(transaction: GRDBReadTransaction) -> UInt {
let sql = """
SELECT
COUNT(*)
FROM \(AttachmentRecord.databaseTableName)
INNER JOIN \(InteractionRecord.databaseTableName)
ON \(attachmentColumn: .albumMessageId) = \(interactionColumnFullyQualified: .uniqueId)
AND \(interactionColumn: .threadUniqueId) = ?
AND \(interactionColumn: .isViewOnceMessage) = FALSE
WHERE \(attachmentColumnFullyQualified: .recordType) = \(SDSRecordType.attachmentStream.rawValue)
AND \(attachmentColumn: .albumMessageId) IS NOT NULL
AND IsVisualMediaContentType(\(attachmentColumn: .contentType)) IS TRUE
SELECT COUNT(*)
FROM "media_gallery_items"
INNER JOIN \(AttachmentRecord.databaseTableName)
ON media_gallery_items.attachmentId = model_TSAttachment.id
AND IsVisualMediaContentType(\(attachmentColumn: .contentType)) IS TRUE
INNER JOIN \(InteractionRecord.databaseTableName)
ON media_gallery_items.albumMessageId = \(interactionColumnFullyQualified: .id)
AND \(interactionColumn: .isViewOnceMessage) = FALSE
WHERE media_gallery_items.threadId = ?
"""
return try! UInt.fetchOne(transaction.database, sql: sql, arguments: [threadUniqueId]) ?? 0
return try! UInt.fetchOne(transaction.database, sql: sql, arguments: [threadId]) ?? 0
}
func enumerateMediaAttachments(range: NSRange, transaction: GRDBReadTransaction, block: @escaping (TSAttachment) -> Void) {
let sql = """
SELECT \(AttachmentRecord.databaseTableName).*
FROM \(AttachmentRecord.databaseTableName)
INNER JOIN \(InteractionRecord.databaseTableName)
ON \(attachmentColumn: .albumMessageId) = \(interactionColumnFullyQualified: .uniqueId)
AND \(interactionColumn: .threadUniqueId) = ?
AND \(interactionColumn: .isViewOnceMessage) = FALSE
WHERE \(attachmentColumnFullyQualified: .recordType) = \(SDSRecordType.attachmentStream.rawValue)
AND \(attachmentColumn: .albumMessageId) IS NOT NULL
AND IsVisualMediaContentType(\(attachmentColumn: .contentType)) IS TRUE
ORDER BY
\(interactionColumnFullyQualified: .id),
\(attachmentColumnFullyQualified: .id)
LIMIT \(range.length)
OFFSET \(range.lowerBound)
SELECT \(AttachmentRecord.databaseTableName).*
FROM "media_gallery_items"
INNER JOIN \(AttachmentRecord.databaseTableName)
ON media_gallery_items.attachmentId = model_TSAttachment.id
AND IsVisualMediaContentType(\(attachmentColumn: .contentType)) IS TRUE
INNER JOIN \(InteractionRecord.databaseTableName)
ON media_gallery_items.albumMessageId = \(interactionColumnFullyQualified: .id)
AND \(interactionColumn: .isViewOnceMessage) = FALSE
WHERE media_gallery_items.threadId = ?
ORDER BY
media_gallery_items.albumMessageId,
media_gallery_items.originalAlbumOrder
LIMIT \(range.length)
OFFSET \(range.lowerBound)
"""
// GRDB TODO: migrate such that attachment.id reflects ordering in TSInteraction.attachmentIds
let cursor = TSAttachment.grdbFetchCursor(sql: sql, arguments: [threadUniqueId], transaction: transaction)
let cursor = TSAttachment.grdbFetchCursor(sql: sql, arguments: [threadId], transaction: transaction)
while let next = try! cursor.next() {
block(next)
}
@ -175,22 +289,27 @@ extension GRDBMediaGalleryFinder: MediaGalleryFinder {
SELECT
ROW_NUMBER() OVER (
ORDER BY
\(interactionColumnFullyQualified: .id),
\(attachmentColumnFullyQualified: .id)
media_gallery_items.albumMessageId,
media_gallery_items.originalAlbumOrder
) - 1 as mediaIndex,
\(attachmentColumnFullyQualified: .uniqueId)
FROM \(AttachmentRecord.databaseTableName)
media_gallery_items.attachmentId
FROM media_gallery_items
INNER JOIN \(AttachmentRecord.databaseTableName)
ON media_gallery_items.attachmentId = model_TSAttachment.id
AND IsVisualMediaContentType(\(attachmentColumn: .contentType)) IS TRUE
INNER JOIN \(InteractionRecord.databaseTableName)
ON \(attachmentColumn: .albumMessageId) = \(interactionColumnFullyQualified: .uniqueId)
AND \(interactionColumn: .threadUniqueId) = ?
ON media_gallery_items.albumMessageId = \(interactionColumnFullyQualified: .id)
AND \(interactionColumn: .isViewOnceMessage) = FALSE
WHERE \(attachmentColumnFullyQualified: .recordType) = \(SDSRecordType.attachmentStream.rawValue)
AND \(attachmentColumn: .albumMessageId) IS NOT NULL
AND IsVisualMediaContentType(\(attachmentColumn: .contentType)) IS TRUE
WHERE media_gallery_items.threadId = ?
)
WHERE \(attachmentColumn: .uniqueId) = ?
WHERE attachmentId = ?
"""
return try! Int.fetchOne(transaction.database, sql: sql, arguments: [threadUniqueId, attachment.uniqueId])
guard let attachmentRowId = attachment.grdbId else {
owsFailDebug("attachment.grdbId was unexpectedly nil")
return nil
}
return try! Int.fetchOne(transaction.database, sql: sql, arguments: [threadId, attachmentRowId])
}
}

View File

@ -805,7 +805,7 @@ NSString *const kNSUserDefaults_DatabaseExtensionVersionMap = @"kNSUserDefaults_
[self deleteDBKeys];
if (CurrentAppContext().isMainApp) {
[TSAttachmentStream deleteAttachments];
[TSAttachmentStream deleteAttachmentsFromDisk];
}
// TODO: Delete Profiles on Disk?

View File

@ -426,6 +426,62 @@ public class GroupThreadFactory: NSObject, Factory {
}
}
@objc
public class ConversationFactory: NSObject {
var databaseStorage: SDSDatabaseStorage {
return SDSDatabaseStorage.shared
}
@objc
public var attachmentCount: Int = 0
@objc
@discardableResult
public func createSentMessage(transaction: SDSAnyWriteTransaction) -> TSOutgoingMessage {
let outgoingFactory = OutgoingMessageFactory()
outgoingFactory.threadCreator = threadCreator
let message = outgoingFactory.create(transaction: transaction)
let attachmentInfos: [OutgoingAttachmentInfo] = (0..<attachmentCount).map { albumIndex in
let caption = Bool.random() ? "(\(albumIndex)) \(CommonGenerator.sentence)" : nil
return attachmentInfoBuilder(message, caption)
}
databaseStorage.asyncWrite { asyncTransaction in
let messagePreparer = OutgoingMessagePreparer(message, unsavedAttachmentInfos: attachmentInfos)
_ = try! messagePreparer.prepareMessage(transaction: asyncTransaction)
for attachment in message.allAttachments(with: asyncTransaction) as! [TSAttachmentStream] {
attachment.updateAsUploaded(withEncryptionKey: Randomness.generateRandomBytes(16),
digest: Randomness.generateRandomBytes(16),
serverId: 1,
transaction: asyncTransaction)
}
message.update(withFakeMessageState: .sent, transaction: asyncTransaction)
}
return message
}
@objc
public var threadCreator: (SDSAnyWriteTransaction) -> TSThread = { transaction in
ContactThreadFactory().create(transaction: transaction)
}
@objc
public var attachmentInfoBuilder: (TSOutgoingMessage, String?) -> OutgoingAttachmentInfo = { outgoingMessage, caption in
let dataSource = DataSourceValue.dataSource(with: ImageFactory().buildPNGData(), fileExtension: "png")!
return OutgoingAttachmentInfo(dataSource: dataSource,
contentType: "image/png",
sourceFilename: nil,
caption: caption,
albumMessageId: outgoingMessage.uniqueId)
}
}
@objc
public class AttachmentStreamFactory: NSObject, Factory {
@ -1707,22 +1763,100 @@ public class CommonGenerator: NSObject {
return result.joined(separator: " ")
}
@objc
static public var sentence: String {
return sentences.randomElement()!
}
@objc
static public func sentences(count: UInt) -> [String] {
return (0..<count).map { _ in sentence }
}
@objc
static public var paragraph: String {
let sentenceCount = UInt(arc4random_uniform(7) + 2)
return paragraph(sentenceCount: sentenceCount)
}
@objc
static public func paragraph(sentenceCount: UInt) -> String {
return sentences(count: sentenceCount).joined(separator: " ")
}
}
@objc
public class ImageFactory: NSObject {
@objc
public func build() -> UIImage {
return type(of: self).buildImage(size: sizeBuilder(),
backgroundColor: backgroundColorBuilder(),
textColor: textColorBuilder(),
text: textBuilder())
}
@objc
public func buildPNGData() -> Data {
guard let data = build().pngData() else {
owsFailDebug("data was unexpectedly nil")
return Data()
}
return data
}
@objc
public func buildJPGData() -> Data {
guard let data = build().jpegData(compressionQuality: 0.9) else {
owsFailDebug("data was unexpectedly nil")
return Data()
}
return data
}
public var sizeBuilder: () -> CGSize = { return CGSize(width: (50..<1000).randomElement()!, height: (50..<1000).randomElement()!) }
public var backgroundColorBuilder: () -> UIColor = { return [UIColor.purple, UIColor.yellow, UIColor.green, UIColor.blue, UIColor.red, UIColor.orange].randomElement()! }
public var textColorBuilder: () -> UIColor = { return [UIColor.black, UIColor.white].randomElement()! }
public var textBuilder: () -> String = { return "\(CommonGenerator.word)\n\(CommonGenerator.word)" }
public class func buildImage(size: CGSize, backgroundColor: UIColor, textColor: UIColor, text: String) -> UIImage {
return autoreleasepool {
let imageSize = CGSize(width: size.width / UIScreen.main.scale,
height: size.height / UIScreen.main.scale)
let imageFrame = CGRect(origin: .zero, size: imageSize)
let font = UIFont.boldSystemFont(ofSize: imageSize.width * 0.1)
let textAttributes: [NSAttributedString.Key: Any] = [.font: font,
.foregroundColor: textColor]
let textFrame = text.boundingRect(with: imageFrame.size,
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: textAttributes,
context: nil)
UIGraphicsBeginImageContextWithOptions(imageFrame.size, false, UIScreen.main.scale)
guard let context = UIGraphicsGetCurrentContext() else {
owsFailDebug("context was unexpectedly nil")
return UIImage()
}
context.setFillColor(backgroundColor.cgColor)
context.fill(imageFrame)
text.draw(at: CGPoint(x: imageFrame.midX - textFrame.midX,
y: imageFrame.midY - textFrame.midY),
withAttributes: textAttributes)
guard let image = UIGraphicsGetImageFromCurrentImageContext() else {
owsFailDebug("image was unexpectedly nil")
return UIImage()
}
UIGraphicsEndImageContext()
return image
}
}
}
#endif

View File

@ -119,9 +119,14 @@ NS_ASSUME_NONNULL_BEGIN
+ (NSString *)cachesDirectoryPath
{
NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
OWSAssertDebug(paths.count >= 1);
return paths[0];
static NSString *result;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
OWSAssert(paths.count >= 1);
result = paths[0];
});
return result;
}
+ (nullable NSError *)renameFilePathUsingRandomExtension:(NSString *)oldFilePath