Merge branch 'mkirk/gallery-records' into release/3.0.0
This commit is contained in:
commit
606e2d2aee
@ -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);
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
)
|
||||
;
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -247,7 +247,7 @@ public class GRDBDatabaseStorageAdapter: NSObject {
|
||||
deleteDBKeys()
|
||||
|
||||
if (CurrentAppContext().isMainApp) {
|
||||
TSAttachmentStream.deleteAttachments()
|
||||
TSAttachmentStream.deleteAttachmentsFromDisk()
|
||||
}
|
||||
|
||||
// TODO: Delete Profiles on Disk?
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@ -805,7 +805,7 @@ NSString *const kNSUserDefaults_DatabaseExtensionVersionMap = @"kNSUserDefaults_
|
||||
[self deleteDBKeys];
|
||||
|
||||
if (CurrentAppContext().isMainApp) {
|
||||
[TSAttachmentStream deleteAttachments];
|
||||
[TSAttachmentStream deleteAttachmentsFromDisk];
|
||||
}
|
||||
|
||||
// TODO: Delete Profiles on Disk?
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user