Modify MediaGalleryCellView to handle still images.

This commit is contained in:
Matthew Chen 2018-11-05 16:43:46 -05:00
parent ec6de40bd9
commit cf057e3af3
5 changed files with 427 additions and 16 deletions

View File

@ -155,6 +155,7 @@
3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496744E2076ACCE00080B5F /* LongTextViewController.swift */; };
349EA07C2162AEA800F7B17F /* OWS111UDAttributesMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */; };
34A55F3720485465002CC6DE /* OWS2FARegistrationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */; };
34A8B3512190A40E00218A25 /* MediaGalleryCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A8B3502190A40E00218A25 /* MediaGalleryCellView.swift */; };
34A910601FFEB114000C4745 /* OWSBackup.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A9105F1FFEB114000C4745 /* OWSBackup.m */; };
34ABB2C42090C59700C727A6 /* OWSResaveCollectionDBMigration.m in Sources */ = {isa = PBXBuildFile; fileRef = 34ABB2C22090C59600C727A6 /* OWSResaveCollectionDBMigration.m */; };
34ABB2C52090C59700C727A6 /* OWSResaveCollectionDBMigration.h in Headers */ = {isa = PBXBuildFile; fileRef = 34ABB2C32090C59700C727A6 /* OWSResaveCollectionDBMigration.h */; };
@ -802,6 +803,7 @@
349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS111UDAttributesMigration.swift; sourceTree = "<group>"; };
34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWS2FARegistrationViewController.m; sourceTree = "<group>"; };
34A55F3620485464002CC6DE /* OWS2FARegistrationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS2FARegistrationViewController.h; sourceTree = "<group>"; };
34A8B3502190A40E00218A25 /* MediaGalleryCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaGalleryCellView.swift; sourceTree = "<group>"; };
34A9105E1FFEB113000C4745 /* OWSBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackup.h; sourceTree = "<group>"; };
34A9105F1FFEB114000C4745 /* OWSBackup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackup.m; sourceTree = "<group>"; };
34ABB2C22090C59600C727A6 /* OWSResaveCollectionDBMigration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSResaveCollectionDBMigration.m; sourceTree = "<group>"; };
@ -1825,6 +1827,7 @@
34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */,
34D1F0961F867BFC0066283D /* ConversationViewCell.h */,
34D1F0971F867BFC0066283D /* ConversationViewCell.m */,
34A8B3502190A40E00218A25 /* MediaGalleryCellView.swift */,
34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */,
34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */,
34DBF005206C3CB100025978 /* OWSBubbleShapeView.h */,
@ -3363,6 +3366,7 @@
3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */,
34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */,
34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */,
34A8B3512190A40E00218A25 /* MediaGalleryCellView.swift in Sources */,
340FC8CD20518C77007AEB0F /* OWSBackupJob.m in Sources */,
34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */,
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,

View File

@ -0,0 +1,358 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc(OWSMediaGalleryCellViewDelegate)
public protocol MediaGalleryCellViewDelegate: class {
@objc(tryToLoadCellMedia:mediaView:cacheKey:canLoadAsync:)
func tryToLoadCellMedia(loadCellMediaBlock: @escaping () -> Any?,
mediaView: UIView,
cacheKey: String,
canLoadAsync: Bool) -> Any?
}
@objc(OWSMediaGalleryCellView)
public class MediaGalleryCellView: UIView {
private weak var delegate: MediaGalleryCellViewDelegate?
private let items: [ConversationMediaGalleryItem]
private let itemViews: [MediaItemView]
private static let kSpacingPts: CGFloat = 2
private static let kMaxItems = 5
@objc
public required init(delegate: MediaGalleryCellViewDelegate,
items: [ConversationMediaGalleryItem],
maxMessageWidth: CGFloat) {
self.delegate = delegate
self.items = items
Logger.verbose("items: \(items.count)")
self.itemViews = MediaGalleryCellView.itemsToDisplay(forItems: items).map {
MediaItemView(delegate: delegate,
item: $0)
}
super.init(frame: .zero)
self.backgroundColor = .white
createContents(maxMessageWidth: maxMessageWidth)
}
private func createContents(maxMessageWidth: CGFloat) {
Logger.verbose("itemViews: \(itemViews.count)")
switch itemViews.count {
case 0:
return
case 1:
guard let itemView = itemViews.first else {
owsFailDebug("Missing item view.")
return
}
addSubview(itemView)
itemView.autoPinEdgesToSuperviewEdges()
case 4:
// Square
let imageSize = (maxMessageWidth - MediaGalleryCellView.kSpacingPts) / 2
for itemView in itemViews {
itemView.autoSetDimensions(to: CGSize(width: imageSize, height: imageSize))
}
let topViews = Array(itemViews[0..<2])
let topStack = UIStackView(arrangedSubviews: topViews)
topStack.axis = .horizontal
topStack.spacing = MediaGalleryCellView.kSpacingPts
let bottomViews = Array(itemViews[2..<4])
let bottomStack = UIStackView(arrangedSubviews: bottomViews)
bottomStack.axis = .horizontal
bottomStack.spacing = MediaGalleryCellView.kSpacingPts
let vStackView = UIStackView(arrangedSubviews: [topStack, bottomStack])
vStackView.axis = .vertical
vStackView.spacing = MediaGalleryCellView.kSpacingPts
addSubview(vStackView)
vStackView.autoPinEdgesToSuperviewEdges()
case 2:
// X X
// side-by-side.
let imageSize = (maxMessageWidth - MediaGalleryCellView.kSpacingPts) / 2
for itemView in itemViews {
itemView.autoSetDimensions(to: CGSize(width: imageSize, height: imageSize))
}
let views = Array(itemViews[0..<2])
let hStackView = UIStackView(arrangedSubviews: views)
hStackView.axis = .horizontal
hStackView.spacing = MediaGalleryCellView.kSpacingPts
addSubview(hStackView)
hStackView.autoPinEdgesToSuperviewEdges()
case 3:
// x
// X
// x
// Big on left, 2 small on right.
let smallImageSize = (maxMessageWidth - MediaGalleryCellView.kSpacingPts * 2) / 3
let bigImageSize = smallImageSize * 2 + MediaGalleryCellView.kSpacingPts
guard let leftItemView = itemViews.first else {
owsFailDebug("Missing view")
return
}
leftItemView.autoSetDimensions(to: CGSize(width: bigImageSize, height: bigImageSize))
let rightViews = Array(itemViews[1..<3])
for itemView in rightViews {
itemView.autoSetDimensions(to: CGSize(width: smallImageSize, height: smallImageSize))
}
let rightStack = UIStackView(arrangedSubviews: rightViews)
rightStack.axis = .vertical
rightStack.spacing = MediaGalleryCellView.kSpacingPts
let hStackView = UIStackView(arrangedSubviews: [leftItemView, rightStack])
hStackView.axis = .horizontal
hStackView.spacing = MediaGalleryCellView.kSpacingPts
addSubview(hStackView)
hStackView.autoPinEdgesToSuperviewEdges()
default:
// X X
// xxx
// 2 big on top, 3 small on bottom.
let bigImageSize = (maxMessageWidth - MediaGalleryCellView.kSpacingPts) / 2
let smallImageSize = (maxMessageWidth - MediaGalleryCellView.kSpacingPts * 2) / 3
let topViews = Array(itemViews[0..<2])
for itemView in topViews {
itemView.autoSetDimensions(to: CGSize(width: bigImageSize, height: bigImageSize))
}
let topStack = UIStackView(arrangedSubviews: topViews)
topStack.axis = .horizontal
topStack.spacing = MediaGalleryCellView.kSpacingPts
let bottomViews = Array(itemViews[2..<5])
for itemView in bottomViews {
itemView.autoSetDimensions(to: CGSize(width: smallImageSize, height: smallImageSize))
}
let bottomStack = UIStackView(arrangedSubviews: bottomViews)
bottomStack.axis = .horizontal
bottomStack.spacing = MediaGalleryCellView.kSpacingPts
let vStackView = UIStackView(arrangedSubviews: [topStack, bottomStack])
vStackView.axis = .vertical
vStackView.spacing = MediaGalleryCellView.kSpacingPts
addSubview(vStackView)
vStackView.autoPinEdgesToSuperviewEdges()
}
}
@objc
public func loadMedia() {
for itemView in itemViews {
itemView.loadMedia()
}
}
@objc
public func unloadMedia() {
for itemView in itemViews {
itemView.unloadMedia()
}
}
@available(*, unavailable, message: "use other init() instead.")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
private class func itemsToDisplay(forItems items: [ConversationMediaGalleryItem]) -> [ConversationMediaGalleryItem] {
let validItems = items.filter {
$0.attachmentStream != nil
}
Logger.verbose("validItems: \(validItems.count)")
guard validItems.count < kMaxItems else {
Logger.verbose("validItems MAX: \(validItems.count)")
return Array(validItems[0..<kMaxItems])
}
return validItems
}
@objc
public class func layoutSize(forMaxMessageWidth maxMessageWidth: CGFloat,
items: [ConversationMediaGalleryItem]) -> CGSize {
let itemCount = itemsToDisplay(forItems: items).count
switch itemCount {
case 0, 1, 4:
// Square
return CGSize(width: maxMessageWidth, height: maxMessageWidth)
case 2:
// X X
// side-by-side.
let imageSize = (maxMessageWidth - kSpacingPts) / 2
return CGSize(width: maxMessageWidth, height: imageSize)
case 3:
// x
// X
// x
// Big on left, 2 small on right.
let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3
let bigImageSize = smallImageSize * 2 + kSpacingPts
return CGSize(width: maxMessageWidth, height: bigImageSize)
default:
// X X
// xxx
// 2 big on top, 3 small on bottom.
let bigImageSize = (maxMessageWidth - kSpacingPts) / 2
let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3
return CGSize(width: maxMessageWidth, height: bigImageSize + smallImageSize + kSpacingPts)
}
}
private class MediaItemView: UIView {
private weak var delegate: MediaGalleryCellViewDelegate?
private let item: ConversationMediaGalleryItem
private var loadBlock : (() -> Void)?
private var unloadBlock : (() -> Void)?
required init(delegate: MediaGalleryCellViewDelegate,
item: ConversationMediaGalleryItem) {
self.delegate = delegate
self.item = item
super.init(frame: .zero)
// TODO:
self.backgroundColor = .white
self.backgroundColor = .red
createContents()
}
@available(*, unavailable, message: "use other init() instead.")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
private func createContents() {
guard let attachmentStream = item.attachmentStream else {
// TODO: Handle this case.
owsFailDebug("Missing attachment stream.")
return
}
if attachmentStream.isAnimated {
} else if attachmentStream.isVideo {
} else if attachmentStream.isImage {
loadStillImage(attachmentStream: attachmentStream)
}
}
private func loadStillImage(attachmentStream: TSAttachmentStream) {
Logger.verbose("loadStillImage")
guard let cacheKey = attachmentStream.uniqueId else {
owsFailDebug("Attachment stream missing unique ID.")
return
}
let stillImageView = UIImageView()
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
stillImageView.contentMode = .scaleAspectFill
// Use trilinear filters for better scaling quality at
// some performance cost.
stillImageView.layer.minificationFilter = kCAFilterTrilinear
stillImageView.layer.magnificationFilter = kCAFilterTrilinear
stillImageView.backgroundColor = .white
addSubview(stillImageView)
stillImageView.autoPinEdgesToSuperviewEdges()
// [self addAttachmentUploadViewIfNecessary];
loadBlock = { [weak self] in
guard let strongSelf = self else {
return
}
guard let strongDelegate = strongSelf.delegate else {
return
}
if stillImageView.image != nil {
return
}
let cachedValue = strongDelegate.tryToLoadCellMedia(loadCellMediaBlock: { () -> Any? in
return attachmentStream.thumbnailImageMedium(success: { (image) in
Logger.verbose("Loaded thumbnail")
stillImageView.image = image
}, failure: {
Logger.verbose("Could not load thumbnail")
})
},
mediaView: stillImageView,
cacheKey: cacheKey,
canLoadAsync: true)
Logger.verbose("cachedValue: \(cachedValue)")
guard let image = cachedValue as? UIImage else {
return
}
stillImageView.image = image
}
unloadBlock = {
stillImageView.image = nil
}
}
func loadMedia() {
guard let loadBlock = loadBlock else {
owsFailDebug("Missing loadBlock")
return
}
loadBlock()
}
func unloadMedia() {
guard let unloadBlock = unloadBlock else {
owsFailDebug("Missing unloadBlock")
return
}
unloadBlock()
}
private class func itemsToDisplay(forItems items: [ConversationMediaGalleryItem]) -> Int {
let validItemCount = items.filter {
$0.attachmentStream != nil
}.count
return max(1, min(5, validItemCount))
}
@objc
public class func layoutSize(forMaxMessageWidth maxMessageWidth: CGFloat,
items: [ConversationMediaGalleryItem]) -> CGSize {
let itemCount = itemsToDisplay(forItems: items)
switch itemCount {
case 0, 1, 4:
// Square
//
// TODO: What's the correct size here?
return CGSize(width: maxMessageWidth, height: maxMessageWidth)
case 2:
// X X
// side-by-side.
let imageSize = (maxMessageWidth - kSpacingPts) / 2
return CGSize(width: maxMessageWidth, height: imageSize)
case 3:
// x
// X
// x
// Big on left, 2 small on right.
let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3
let bigImageSize = smallImageSize * 2 + kSpacingPts
return CGSize(width: maxMessageWidth, height: bigImageSize)
default:
// X X
// xxx
// 2 big on top, 3 small on bottom.
let bigImageSize = (maxMessageWidth - kSpacingPts) / 2
let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3
return CGSize(width: maxMessageWidth, height: bigImageSize + smallImageSize + kSpacingPts)
}
}
}
}

View File

@ -24,7 +24,11 @@ NS_ASSUME_NONNULL_BEGIN
const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
= UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent;
@interface OWSMessageBubbleView () <OWSQuotedMessageViewDelegate, OWSContactShareButtonsViewDelegate>
typedef _Nullable id (^LoadCellMediaBlock)(void);
@interface OWSMessageBubbleView () <OWSQuotedMessageViewDelegate,
OWSContactShareButtonsViewDelegate,
OWSMediaGalleryCellViewDelegate>
@property (nonatomic) OWSBubbleView *bubbleView;
@ -331,8 +335,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
bodyMediaView = [self loadViewForContactShare];
break;
case OWSMessageCellType_MediaGallery:
// TODO:
bodyMediaView = [self loadViewForGenericAttachment];
bodyMediaView = [self loadViewForMediaGallery];
break;
}
@ -621,8 +624,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
case OWSMessageCellType_ContactShare:
return NO;
case OWSMessageCellType_MediaGallery:
// TODO:
return NO;
return YES;
}
}
@ -664,12 +666,17 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
// but lazy-load any expensive media (photo, gif, etc.) used in those views. Note that
// this lazy-load can fail, in which case we modify the view hierarchy to use an "error"
// state. The didCellMediaFailToLoad reflects media load fails.
- (nullable id)tryToLoadCellMedia:(nullable id (^)(void))loadCellMediaBlock
- (nullable id)tryToLoadCellMedia:(LoadCellMediaBlock)loadCellMediaBlock
mediaView:(UIView *)mediaView
cacheKey:(NSString *)cacheKey
canLoadAsync:(BOOL)canLoadAsync
{
OWSAssertDebug(self.attachmentStream);
OWSAssertIsOnMainThread();
if (self.cellType == OWSMessageCellType_MediaGallery) {
OWSAssertDebug(self.viewItem.mediaGalleryItems);
} else {
OWSAssertDebug(self.attachmentStream);
}
OWSAssertDebug(mediaView);
OWSAssertDebug(cacheKey);
OWSAssertDebug(self.cellMediaCache);
@ -831,6 +838,25 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
return tapForMoreLabel;
}
- (UIView *)loadViewForMediaGallery
{
OWSAssertDebug(self.viewItem.mediaGalleryItems);
OWSLogVerbose(@"self.viewItem.mediaGalleryItems: %lu", (unsigned long)self.viewItem.mediaGalleryItems.count);
OWSMediaGalleryCellView *galleryView =
[[OWSMediaGalleryCellView alloc] initWithDelegate:self
items:self.viewItem.mediaGalleryItems
maxMessageWidth:self.conversationStyle.maxMessageWidth];
self.loadCellContentBlock = ^{
[galleryView loadMedia];
};
self.unloadCellContentBlock = ^{
[galleryView unloadMedia];
};
return galleryView;
}
- (UIView *)loadViewForStillImage
{
OWSAssertDebug(self.attachmentStream);
@ -1218,8 +1244,8 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
result = CGSizeMake(maxMessageWidth, [OWSContactShareView bubbleHeight]);
break;
case OWSMessageCellType_MediaGallery:
// Always use a "max size square".
result = CGSizeMake(maxMessageWidth, maxMessageWidth);
result = [OWSMediaGalleryCellView layoutSizeForMaxMessageWidth:maxMessageWidth
items:self.viewItem.mediaGalleryItems];
break;
}

View File

@ -542,7 +542,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if ([message isMediaGalleryWithTransaction:transaction]) {
OWSAssertDebug(attachments.count > 0);
// TODO: Handle captions.
self.mediaGalleryItems = [self mediaGalleryItemsForAttachments:attachments];
NSArray<ConversationMediaGalleryItem *> *mediaGalleryItems = [self mediaGalleryItemsForAttachments:attachments];
self.mediaGalleryItems = mediaGalleryItems;
self.messageCellType = OWSMessageCellType_MediaGallery;
return;
}
@ -668,8 +669,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
mediaSize:CGSizeZero]];
continue;
}
CGSize mediaSize = [self.attachmentStream imageSize];
if (self.mediaSize.width <= 0 || self.mediaSize.height <= 0) {
CGSize mediaSize = [attachmentStream imageSize];
if (mediaSize.width <= 0 || mediaSize.height <= 0) {
OWSLogWarn(@"Filtering media with invalid size.");
[mediaGalleryItems addObject:[[ConversationMediaGalleryItem alloc] initWithAttachment:attachment
attachmentStream:nil
@ -932,7 +933,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
}
case OWSMessageCellType_MediaGallery: {
// TODO: We need a "canShareMediaAction" method.
OWSAssertDebug(self.mediaGalleryItems.count > 0);
OWSAssertDebug(self.mediaGalleryItems);
NSMutableArray<TSAttachmentStream *> *attachmentStreams = [NSMutableArray new];
for (ConversationMediaGalleryItem *mediaGalleryItem in self.mediaGalleryItems) {
if (mediaGalleryItem.attachmentStream) {

View File

@ -127,9 +127,13 @@ NS_ASSUME_NONNULL_BEGIN
actionBlock:^{
[DebugUIMessages sendNTextMessagesInThread:thread];
}],
[OWSTableItem itemWithTitle:@"Send Multi-Image Message"
[OWSTableItem itemWithTitle:@"Send Media Gallery"
actionBlock:^{
[DebugUIMessages sendMultiImageMessageInThread:thread];
[DebugUIMessages sendMediaGalleryInThread:thread];
}],
[OWSTableItem itemWithTitle:@"Send Exemplary Media Galleries"
actionBlock:^{
[DebugUIMessages sendExemplaryMediaGalleriesInThread:thread];
}],
[OWSTableItem itemWithTitle:@"Select Fake"
actionBlock:^{
@ -4634,13 +4638,31 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac
}
}
+ (void)sendMultiImageMessageInThread:(TSThread *)thread
+ (void)sendMediaGalleryInThread:(TSThread *)thread
{
OWSLogInfo(@"");
const uint32_t kMinImageCount = 2;
const uint32_t kMaxImageCount = 10;
uint32_t imageCount = kMinImageCount + arc4random_uniform(kMaxImageCount - kMinImageCount);
[self sendMediaGalleryInThread:thread imageCount:imageCount];
}
+ (void)sendExemplaryMediaGalleriesInThread:(TSThread *)thread
{
OWSLogInfo(@"");
[self sendMediaGalleryInThread:thread imageCount:2];
[self sendMediaGalleryInThread:thread imageCount:3];
[self sendMediaGalleryInThread:thread imageCount:4];
[self sendMediaGalleryInThread:thread imageCount:5];
[self sendMediaGalleryInThread:thread imageCount:6];
}
+ (void)sendMediaGalleryInThread:(TSThread *)thread imageCount:(uint32_t)imageCount
{
OWSAssertDebug(imageCount > 0);
OWSLogInfo(@"");
NSMutableArray<SignalAttachment *> *attachments = [NSMutableArray new];
for (uint32_t i = 0; i < imageCount; i++) {