Merge branch 'mkirk/picker-bubble-preview' into release/2.30.2

pull/1/head
Michael Kirk 6 years ago
commit c07147210c

@ -67,66 +67,67 @@ struct MessageActionBuilder {
}
}
extension ConversationViewItem {
@objc
class ConversationViewItemActions: NSObject {
@objc
func textActions(delegate: MessageActionsDelegate) -> [MenuAction] {
class func textActions(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> [MenuAction] {
var actions: [MenuAction] = []
let replyAction = MessageActionBuilder.reply(conversationViewItem: self, delegate: delegate)
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(replyAction)
if self.hasBodyTextActionContent {
let copyTextAction = MessageActionBuilder.copyText(conversationViewItem: self, delegate: delegate)
if conversationViewItem.hasBodyTextActionContent {
let copyTextAction = MessageActionBuilder.copyText(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(copyTextAction)
}
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: self, delegate: delegate)
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(deleteAction)
let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: self, delegate: delegate)
let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(showDetailsAction)
return actions
}
@objc
func mediaActions(delegate: MessageActionsDelegate) -> [MenuAction] {
class func mediaActions(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> [MenuAction] {
var actions: [MenuAction] = []
let replyAction = MessageActionBuilder.reply(conversationViewItem: self, delegate: delegate)
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(replyAction)
if self.hasMediaActionContent {
let copyMediaAction = MessageActionBuilder.copyMedia(conversationViewItem: self, delegate: delegate)
if conversationViewItem.hasMediaActionContent {
let copyMediaAction = MessageActionBuilder.copyMedia(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(copyMediaAction)
if self.canSaveMedia() {
let saveMediaAction = MessageActionBuilder.saveMedia(conversationViewItem: self, delegate: delegate)
if conversationViewItem.canSaveMedia() {
let saveMediaAction = MessageActionBuilder.saveMedia(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(saveMediaAction)
}
}
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: self, delegate: delegate)
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(deleteAction)
let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: self, delegate: delegate)
let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(showDetailsAction)
return actions
}
@objc
func quotedMessageActions(delegate: MessageActionsDelegate) -> [MenuAction] {
let replyAction = MessageActionBuilder.reply(conversationViewItem: self, delegate: delegate)
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: self, delegate: delegate)
let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: self, delegate: delegate)
class func quotedMessageActions(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> [MenuAction] {
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)
let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: conversationViewItem, delegate: delegate)
return [replyAction, deleteAction, showDetailsAction]
}
@objc
func infoMessageActions(delegate: MessageActionsDelegate) -> [MenuAction] {
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: self, delegate: delegate)
class func infoMessageActions(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> [MenuAction] {
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)
return [deleteAction]
}

@ -9,6 +9,7 @@
#import "AppSettingsViewController.h"
#import "ContactCellView.h"
#import "ContactTableViewCell.h"
#import "ConversationViewCell.h"
#import "ConversationViewItem.h"
#import "DateUtil.h"
#import "DebugUIPage.h"
@ -21,6 +22,7 @@
#import "NotificationsManager.h"
#import "OWSAddToContactViewController.h"
#import "OWSAnyTouchGestureRecognizer.h"
#import "OWSAudioMessageView.h"
#import "OWSAudioPlayer.h"
#import "OWSBackup.h"
#import "OWSBackupIO.h"

@ -75,18 +75,18 @@ class ColorPicker: NSObject, ColorPickerViewDelegate {
@objc
let sheetViewController: SheetViewController
private let currentConversationColor: OWSConversationColor
@objc
init(currentConversationColor: OWSConversationColor) {
self.currentConversationColor = currentConversationColor
init(thread: TSThread) {
let colorName = thread.conversationColorName
let currentConversationColor = OWSConversationColor.conversationColorOrDefault(colorName: colorName)
sheetViewController = SheetViewController()
super.init()
let colorPickerView = ColorPickerView()
let colorPickerView = ColorPickerView(thread: thread)
colorPickerView.delegate = self
colorPickerView.select(conversationColor: currentConversationColor)
sheetViewController.contentView.addSubview(colorPickerView)
colorPickerView.autoPinEdgesToSuperviewEdges()
}
@ -105,21 +105,42 @@ protocol ColorPickerViewDelegate: class {
class ColorPickerView: UIView, ColorViewDelegate {
private let colorViews: [ColorView]
let conversationStyle: ConversationStyle
var outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ())
var incomingMessageView = OWSMessageBubbleView(forAutoLayout: ())
weak var delegate: ColorPickerViewDelegate?
override init(frame: CGRect) {
// This is mostly a developer convenience - OWSMessageCell asserts at some point
// that the available method width is greater than 0.
// We ultimately use the width of the picker view which will be larger.
let kMinimumConversationWidth: CGFloat = 300
override var bounds: CGRect {
didSet {
updateMockConversationView()
}
}
let mockConversationView: UIView = UIView()
init(thread: TSThread) {
let allConversationColors = OWSConversationColor.conversationColorNames.map { OWSConversationColor.conversationColorOrDefault(colorName: $0) }
self.colorViews = allConversationColors.map { ColorView(conversationColor: $0) }
super.init(frame: frame)
self.conversationStyle = ConversationStyle(thread: thread)
super.init(frame: .zero)
colorViews.forEach { $0.delegate = self }
let headerView = self.buildHeaderView()
mockConversationView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
mockConversationView.backgroundColor = Theme.backgroundColor
self.updateMockConversationView()
let paletteView = self.buildPaletteView(colorViews: colorViews)
let rowsStackView = UIStackView(arrangedSubviews: [headerView, paletteView])
let rowsStackView = UIStackView(arrangedSubviews: [headerView, mockConversationView, paletteView])
rowsStackView.axis = .vertical
addSubview(rowsStackView)
rowsStackView.autoPinEdgesToSuperviewEdges()
@ -134,6 +155,7 @@ class ColorPickerView: UIView, ColorViewDelegate {
func colorViewWasTapped(_ colorView: ColorView) {
self.select(conversationColor: colorView.conversationColor)
self.delegate?.colorPickerView(self, didPickConversationColor: colorView.conversationColor)
updateMockConversationView()
}
fileprivate func select(conversationColor selectedConversationColor: OWSConversationColor) {
@ -166,9 +188,59 @@ class ColorPickerView: UIView, ColorViewDelegate {
return headerView
}
private func updateMockConversationView() {
conversationStyle.viewWidth = max(bounds.size.width, kMinimumConversationWidth)
mockConversationView.subviews.forEach { $0.removeFromSuperview() }
// outgoing
outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ())
let outgoingItem = MockConversationViewItem()
let outgoingText = NSLocalizedString("COLOR_PICKER_DEMO_MESSAGE_1", comment: "The first of two messages demonstrating the chosen conversation color, by rendering this message in an outgoing message bubble.")
outgoingItem.interaction = MockOutgoingMessage(messageBody: outgoingText)
outgoingItem.displayableBodyText = DisplayableText.displayableText(outgoingText)
outgoingItem.interactionType = .outgoingMessage
outgoingMessageView.viewItem = outgoingItem
outgoingMessageView.cellMediaCache = NSCache()
outgoingMessageView.conversationStyle = conversationStyle
outgoingMessageView.configureViews()
outgoingMessageView.loadContent()
let outgoingCell = UIView()
outgoingCell.addSubview(outgoingMessageView)
outgoingMessageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .leading)
let outgoingSize = outgoingMessageView.measureSize()
outgoingMessageView.autoSetDimensions(to: outgoingSize)
// incoming
incomingMessageView = OWSMessageBubbleView(forAutoLayout: ())
let incomingItem = MockConversationViewItem()
let incomingText = NSLocalizedString("COLOR_PICKER_DEMO_MESSAGE_2", comment: "The second of two messages demonstrating the chosen conversation color, by rendering this message in an incoming message bubble.")
incomingItem.interaction = MockIncomingMessage(messageBody: incomingText)
incomingItem.displayableBodyText = DisplayableText.displayableText(incomingText)
incomingItem.interactionType = .incomingMessage
incomingMessageView.viewItem = incomingItem
incomingMessageView.cellMediaCache = NSCache()
incomingMessageView.conversationStyle = conversationStyle
incomingMessageView.configureViews()
incomingMessageView.loadContent()
let incomingCell = UIView()
incomingCell.addSubview(incomingMessageView)
incomingMessageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .trailing)
let incomingSize = incomingMessageView.measureSize()
incomingMessageView.autoSetDimensions(to: incomingSize)
let messagesStackView = UIStackView(arrangedSubviews: [outgoingCell, incomingCell])
messagesStackView.axis = .vertical
messagesStackView.spacing = 12
mockConversationView.addSubview(messagesStackView)
messagesStackView.autoPinEdgesToSuperviewMargins()
}
private func buildPaletteView(colorViews: [ColorView]) -> UIView {
let paletteView = UIView()
paletteView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
paletteView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 0, right: 16)
let kRowLength = 4
let rows: [UIView] = colorViews.chunked(by: kRowLength).map { colorViewsInRow in
@ -178,10 +250,210 @@ class ColorPickerView: UIView, ColorViewDelegate {
}
let rowsStackView = UIStackView(arrangedSubviews: rows)
rowsStackView.axis = .vertical
rowsStackView.spacing = ScaleFromIPhone5To7Plus(16, 50)
rowsStackView.spacing = ScaleFromIPhone5To7Plus(12, 50)
paletteView.addSubview(rowsStackView)
rowsStackView.ows_autoPinToSuperviewMargins()
// no-op gesture to keep taps from dismissing SheetView
paletteView.addGestureRecognizer(UITapGestureRecognizer(target: nil, action: nil))
return paletteView
}
}
// MARK: Mock Classes for rendering demo conversation
@objc
private class MockConversationViewItem: NSObject, ConversationViewItem {
var interaction: TSInteraction = TSMessage()
var interactionType: OWSInteractionType = OWSInteractionType.unknown
var quotedReply: OWSQuotedReplyModel?
var isGroupThread: Bool = false
var hasBodyText: Bool = true
var isQuotedReply: Bool = false
var hasQuotedAttachment: Bool = false
var hasQuotedText: Bool = false
var hasCellHeader: Bool = false
var isExpiringMessage: Bool = false
var shouldShowDate: Bool = false
var shouldShowSenderAvatar: Bool = false
var senderName: NSAttributedString?
var shouldHideFooter: Bool = false
var isFirstInCluster: Bool = true
var isLastInCluster: Bool = true
var unreadIndicator: OWSUnreadIndicator?
var lastAudioMessageView: OWSAudioMessageView?
var audioDurationSeconds: CGFloat = 0
var audioProgressSeconds: CGFloat = 0
var messageCellType: OWSMessageCellType = .textMessage
var displayableBodyText: DisplayableText?
var attachmentStream: TSAttachmentStream?
var attachmentPointer: TSAttachmentPointer?
var mediaSize: CGSize = .zero
var displayableQuotedText: DisplayableText?
var quotedAttachmentMimetype: String?
var quotedRecipientId: String?
var didCellMediaFailToLoad: Bool = false
var contactShare: ContactShareViewModel?
var systemMessageText: String?
var authorConversationColorName: String?
var hasBodyTextActionContent: Bool = false
var hasMediaActionContent: Bool = false
override init() {
super.init()
}
func dequeueCell(for collectionView: UICollectionView, indexPath: IndexPath) -> ConversationViewCell {
owsFailDebug("unexpected invocation")
return ConversationViewCell(forAutoLayout: ())
}
func replace(_ interaction: TSInteraction, transaction: YapDatabaseReadTransaction) {
owsFailDebug("unexpected invocation")
return
}
func clearCachedLayoutState() {
owsFailDebug("unexpected invocation")
return
}
func copyMediaAction() {
owsFailDebug("unexpected invocation")
return
}
func copyTextAction() {
owsFailDebug("unexpected invocation")
return
}
func shareMediaAction() {
owsFailDebug("unexpected invocation")
return
}
func shareTextAction() {
owsFailDebug("unexpected invocation")
return
}
func saveMediaAction() {
owsFailDebug("unexpected invocation")
return
}
func deleteAction() {
owsFailDebug("unexpected invocation")
return
}
func canSaveMedia() -> Bool {
owsFailDebug("unexpected invocation")
return false
}
func audioPlaybackState() -> AudioPlaybackState {
owsFailDebug("unexpected invocation")
return AudioPlaybackState.paused
}
func setAudioPlaybackState(_ state: AudioPlaybackState) {
owsFailDebug("unexpected invocation")
return
}
func setAudioProgress(_ progress: CGFloat, duration: CGFloat) {
owsFailDebug("unexpected invocation")
return
}
func cellSize() -> CGSize {
owsFailDebug("unexpected invocation")
return CGSize.zero
}
func vSpacing(withPreviousLayoutItem previousLayoutItem: ConversationViewLayoutItem) -> CGFloat {
owsFailDebug("unexpected invocation")
return 2
}
}
private class MockIncomingMessage: TSIncomingMessage {
init(messageBody: String) {
super.init(incomingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
in: TSThread(),
authorId: "+fake-id",
sourceDeviceId: 1,
messageBody: messageBody,
attachmentIds: [],
expiresInSeconds: 0,
quotedMessage: nil,
contactShare: nil)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(dictionary dictionaryValue: [AnyHashable: Any]!) throws {
fatalError("init(dictionary:) has not been implemented")
}
override func save(with transaction: YapDatabaseReadWriteTransaction) {
// no - op
owsFailDebug("shouldn't save mock message")
}
}
private class MockOutgoingMessage: TSOutgoingMessage {
init(messageBody: String) {
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
in: nil,
messageBody: messageBody,
attachmentIds: [],
expiresInSeconds: 0,
expireStartedAt: 0,
isVoiceMessage: false,
groupMetaMessage: .unspecified,
quotedMessage: nil,
contactShare: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(dictionary dictionaryValue: [AnyHashable: Any]!) throws {
fatalError("init(dictionary:) has not been implemented")
}
override func save(with transaction: YapDatabaseReadWriteTransaction) {
// no - op
owsFailDebug("shouldn't save mock message")
}
class MockOutgoingMessageRecipientState: TSOutgoingMessageRecipientState {
override var state: OWSOutgoingMessageRecipientState {
return OWSOutgoingMessageRecipientState.sent
}
override var deliveryTimestamp: NSNumber? {
return NSNumber(value: NSDate.ows_millisecondTimeStamp())
}
override var readTimestamp: NSNumber? {
return NSNumber(value: NSDate.ows_millisecondTimeStamp())
}
}
override func readRecipientIds() -> [String] {
// makes message appear as read
return ["fake-non-empty-id"]
}
override func recipientState(forRecipientId recipientId: String) -> TSOutgoingMessageRecipientState? {
return MockOutgoingMessageRecipientState()
}
}

@ -6,7 +6,6 @@ NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class ConversationViewCell;
@class ConversationViewItem;
@class OWSContactOffersInteraction;
@class OWSContactsManager;
@class TSAttachmentPointer;
@ -19,13 +18,15 @@ NS_ASSUME_NONNULL_BEGIN
@class TSOutgoingMessage;
@class TSQuotedMessage;
@protocol ConversationViewItem;
@protocol ConversationViewCellDelegate <NSObject>
- (void)conversationCell:(ConversationViewCell *)cell didLongpressTextViewItem:(ConversationViewItem *)viewItem;
- (void)conversationCell:(ConversationViewCell *)cell didLongpressMediaViewItem:(ConversationViewItem *)viewItem;
- (void)conversationCell:(ConversationViewCell *)cell didLongpressQuoteViewItem:(ConversationViewItem *)viewItem;
- (void)conversationCell:(ConversationViewCell *)cell didLongpressTextViewItem:(id<ConversationViewItem>)viewItem;
- (void)conversationCell:(ConversationViewCell *)cell didLongpressMediaViewItem:(id<ConversationViewItem>)viewItem;
- (void)conversationCell:(ConversationViewCell *)cell didLongpressQuoteViewItem:(id<ConversationViewItem>)viewItem;
- (void)conversationCell:(ConversationViewCell *)cell
didLongpressSystemMessageViewItem:(ConversationViewItem *)viewItem;
didLongpressSystemMessageViewItem:(id<ConversationViewItem>)viewItem;
#pragma mark - System Cell
@ -68,7 +69,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable, weak) id<ConversationViewCellDelegate> delegate;
@property (nonatomic, nullable) ConversationViewItem *viewItem;
@property (nonatomic, nullable) id<ConversationViewItem> viewItem;
// Cells are prefetched but expensive cells (e.g. media) should only load
// when visible and unload when no longer visible. Non-visible cells can

@ -7,14 +7,15 @@
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class ConversationViewItem;
@class TSAttachmentStream;
@protocol ConversationViewItem;
@interface OWSAudioMessageView : UIStackView
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream
isIncoming:(BOOL)isIncoming
viewItem:(ConversationViewItem *)viewItem
viewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle;
- (void)createContents;

@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) TSAttachmentStream *attachmentStream;
@property (nonatomic) BOOL isIncoming;
@property (nonatomic, weak) ConversationViewItem *viewItem;
@property (nonatomic, weak) id<ConversationViewItem> viewItem;
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
@property (nonatomic, nullable) UIButton *audioPlayPauseButton;
@ -32,7 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream
isIncoming:(BOOL)isIncoming
viewItem:(ConversationViewItem *)viewItem
viewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle
{
self = [super init];

@ -6,7 +6,9 @@ NS_ASSUME_NONNULL_BEGIN
@class ContactShareViewModel;
@class ConversationStyle;
@class ConversationViewItem;
@protocol ConversationViewItem;
@class OWSContact;
@class OWSQuotedReplyModel;
@class TSAttachmentPointer;
@ -25,27 +27,27 @@ extern const UIDataDetectorTypes kOWSAllowedDataDetectorTypes;
@protocol OWSMessageBubbleViewDelegate
- (void)didTapImageViewItem:(ConversationViewItem *)viewItem
- (void)didTapImageViewItem:(id<ConversationViewItem>)viewItem
attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIView *)imageView;
- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem
- (void)didTapVideoViewItem:(id<ConversationViewItem>)viewItem
attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIView *)imageView;
- (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream;
- (void)didTapAudioViewItem:(id<ConversationViewItem>)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream;
- (void)didTapTruncatedTextMessage:(ConversationViewItem *)conversationItem;
- (void)didTapTruncatedTextMessage:(id<ConversationViewItem>)conversationItem;
- (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem
- (void)didTapFailedIncomingAttachment:(id<ConversationViewItem>)viewItem
attachmentPointer:(TSAttachmentPointer *)attachmentPointer;
- (void)didTapConversationItem:(ConversationViewItem *)viewItem quotedReply:(OWSQuotedReplyModel *)quotedReply;
- (void)didTapConversationItem:(ConversationViewItem *)viewItem
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem quotedReply:(OWSQuotedReplyModel *)quotedReply;
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem
quotedReply:(OWSQuotedReplyModel *)quotedReply
failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer;
- (void)didTapContactShareViewItem:(ConversationViewItem *)viewItem;
- (void)didTapContactShareViewItem:(id<ConversationViewItem>)viewItem;
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare
NS_SWIFT_NAME(didTapSendMessage(toContactShare:));
@ -60,7 +62,7 @@ extern const UIDataDetectorTypes kOWSAllowedDataDetectorTypes;
@interface OWSMessageBubbleView : UIView
@property (nonatomic, nullable) ConversationViewItem *viewItem;
@property (nonatomic, nullable) id<ConversationViewItem> viewItem;
@property (nonatomic) ConversationStyle *conversationStyle;

@ -2,19 +2,20 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class ConversationViewItem;
NS_ASSUME_NONNULL_BEGIN
@protocol ConversationViewItem;
@interface OWSMessageFooterView : UIStackView
- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem
- (void)configureWithConversationViewItem:(id<ConversationViewItem>)viewItem
isOverlayingMedia:(BOOL)isOverlayingMedia
conversationStyle:(ConversationStyle *)conversationStyle
isIncoming:(BOOL)isIncoming;
- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem;
- (CGSize)measureWithConversationViewItem:(id<ConversationViewItem>)viewItem;
- (void)prepareForReuse;

@ -85,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Load
- (void)configureWithConversationViewItem:(ConversationViewItem *)viewItem
- (void)configureWithConversationViewItem:(id<ConversationViewItem>)viewItem
isOverlayingMedia:(BOOL)isOverlayingMedia
conversationStyle:(ConversationStyle *)conversationStyle
isIncoming:(BOOL)isIncoming
@ -186,7 +186,7 @@ NS_ASSUME_NONNULL_BEGIN
[self.statusIndicatorImageView.layer addAnimation:animation forKey:@"animation"];
}
- (BOOL)isFailedOutgoingMessage:(ConversationViewItem *)viewItem
- (BOOL)isFailedOutgoingMessage:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
@ -200,7 +200,7 @@ NS_ASSUME_NONNULL_BEGIN
return messageStatus == MessageReceiptStatusFailed;
}
- (void)configureLabelsWithConversationViewItem:(ConversationViewItem *)viewItem
- (void)configureLabelsWithConversationViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
@ -217,7 +217,7 @@ NS_ASSUME_NONNULL_BEGIN
self.timestampLabel.text = timestampLabelText.localizedUppercaseString;
}
- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem
- (CGSize)measureWithConversationViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
@ -243,7 +243,7 @@ NS_ASSUME_NONNULL_BEGIN
return CGSizeCeil(result);
}
- (nullable NSString *)messageStatusTextForConversationViewItem:(ConversationViewItem *)viewItem
- (nullable NSString *)messageStatusTextForConversationViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
if (viewItem.interaction.interactionType != OWSInteractionType_OutgoingMessage) {

@ -5,16 +5,17 @@
extern const CGFloat OWSMessageHeaderViewDateHeaderVMargin;
@class ConversationStyle;
@class ConversationViewItem;
@protocol ConversationViewItem;
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageHeaderView : UIStackView
- (void)loadForDisplayWithViewItem:(ConversationViewItem *)viewItem
- (void)loadForDisplayWithViewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle;
- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem
- (CGSize)measureWithConversationViewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle;
@end

@ -72,7 +72,7 @@ const CGFloat OWSMessageHeaderViewDateHeaderVMargin = 23;
[self addSubview:self.stackView];
}
- (void)loadForDisplayWithViewItem:(ConversationViewItem *)viewItem
- (void)loadForDisplayWithViewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle
{
OWSAssertDebug(viewItem);
@ -100,7 +100,7 @@ const CGFloat OWSMessageHeaderViewDateHeaderVMargin = 23;
];
}
- (CGFloat)strokeThicknessWithViewItem:(ConversationViewItem *)viewItem
- (CGFloat)strokeThicknessWithViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
@ -111,7 +111,7 @@ const CGFloat OWSMessageHeaderViewDateHeaderVMargin = 23;
}
}
- (UIColor *)strokeColorWithViewItem:(ConversationViewItem *)viewItem
- (UIColor *)strokeColorWithViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
@ -122,7 +122,7 @@ const CGFloat OWSMessageHeaderViewDateHeaderVMargin = 23;
}
}
- (void)configureLabelsWithViewItem:(ConversationViewItem *)viewItem
- (void)configureLabelsWithViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
@ -158,7 +158,7 @@ const CGFloat OWSMessageHeaderViewDateHeaderVMargin = 23;
}
}
- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem
- (CGSize)measureWithConversationViewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle
{
OWSAssertDebug(viewItem);

@ -175,8 +175,8 @@ typedef enum : NSUInteger {
@property (nonatomic, readonly) ConversationViewLayout *layout;
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
@property (nonatomic) NSArray<ConversationViewItem *> *viewItems;
@property (nonatomic) NSMutableDictionary<NSString *, ConversationViewItem *> *viewItemCache;
@property (nonatomic) NSArray<id<ConversationViewItem>> *viewItems;
@property (nonatomic) NSMutableDictionary<NSString *, id<ConversationViewItem>> *viewItemCache;
@property (nonatomic, nullable) AVAudioRecorder *audioRecorder;
@property (nonatomic, nullable) OWSAudioPlayer *audioAttachmentPlayer;
@ -717,7 +717,7 @@ typedef enum : NSUInteger {
- (NSIndexPath *_Nullable)indexPathOfUnreadMessagesIndicator
{
NSInteger row = 0;
for (ConversationViewItem *viewItem in self.viewItems) {
for (id<ConversationViewItem> viewItem in self.viewItems) {
if (viewItem.unreadIndicator) {
return [NSIndexPath indexPathForRow:row inSection:0];
}
@ -1553,7 +1553,7 @@ typedef enum : NSUInteger {
OWSLogInfo(@"didChangePreferredContentSize");
// Evacuate cached cell sizes.
for (ConversationViewItem *viewItem in self.viewItems) {
for (id<ConversationViewItem> viewItem in self.viewItems) {
[viewItem clearCachedLayoutState];
}
[self resetContentAndLayout];
@ -1933,12 +1933,12 @@ typedef enum : NSUInteger {
#pragma mark - MessageActionsDelegate
- (void)messageActionsShowDetailsForItem:(ConversationViewItem *)conversationViewItem
- (void)messageActionsShowDetailsForItem:(id<ConversationViewItem>)conversationViewItem
{
[self showDetailViewForViewItem:conversationViewItem];
}
- (void)messageActionsReplyToItem:(ConversationViewItem *)conversationViewItem
- (void)messageActionsReplyToItem:(id<ConversationViewItem>)conversationViewItem
{
[self populateReplyForViewItem:conversationViewItem];
}
@ -2006,27 +2006,32 @@ typedef enum : NSUInteger {
#pragma mark - ConversationViewCellDelegate
- (void)conversationCell:(ConversationViewCell *)cell didLongpressMediaViewItem:(ConversationViewItem *)viewItem
- (void)conversationCell:(ConversationViewCell *)cell didLongpressMediaViewItem:(id<ConversationViewItem>)viewItem
{
NSArray<MenuAction *> *messageActions = [viewItem mediaActionsWithDelegate:self];
NSArray<MenuAction *> *messageActions =
[ConversationViewItemActions mediaActionsWithConversationViewItem:viewItem delegate:self];
[self presentMessageActions:messageActions withFocusedCell:cell];
}
- (void)conversationCell:(ConversationViewCell *)cell didLongpressTextViewItem:(ConversationViewItem *)viewItem
- (void)conversationCell:(ConversationViewCell *)cell didLongpressTextViewItem:(id<ConversationViewItem>)viewItem
{
NSArray<MenuAction *> *messageActions = [viewItem textActionsWithDelegate:self];
NSArray<MenuAction *> *messageActions =
[ConversationViewItemActions textActionsWithConversationViewItem:viewItem delegate:self];
[self presentMessageActions:messageActions withFocusedCell:cell];
}
- (void)conversationCell:(ConversationViewCell *)cell didLongpressQuoteViewItem:(ConversationViewItem *)viewItem
- (void)conversationCell:(ConversationViewCell *)cell didLongpressQuoteViewItem:(id<ConversationViewItem>)viewItem
{
NSArray<MenuAction *> *messageActions = [viewItem quotedMessageActionsWithDelegate:self];
NSArray<MenuAction *> *messageActions =
[ConversationViewItemActions quotedMessageActionsWithConversationViewItem:viewItem delegate:self];
[self presentMessageActions:messageActions withFocusedCell:cell];
}
- (void)conversationCell:(ConversationViewCell *)cell didLongpressSystemMessageViewItem:(ConversationViewItem *)viewItem
- (void)conversationCell:(ConversationViewCell *)cell
didLongpressSystemMessageViewItem:(id<ConversationViewItem>)viewItem
{
NSArray<MenuAction *> *messageActions = [viewItem infoMessageActionsWithDelegate:self];
NSArray<MenuAction *> *messageActions =
[ConversationViewItemActions infoMessageActionsWithConversationViewItem:viewItem delegate:self];
[self presentMessageActions:messageActions withFocusedCell:cell];
}
@ -2141,7 +2146,7 @@ typedef enum : NSUInteger {
#pragma mark - OWSMessageBubbleViewDelegate
- (void)didTapImageViewItem:(ConversationViewItem *)viewItem
- (void)didTapImageViewItem:(id<ConversationViewItem>)viewItem
attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIView *)imageView
{
@ -2172,7 +2177,7 @@ typedef enum : NSUInteger {
[vc presentDetailViewFromViewController:self mediaMessage:mediaMessage replacingView:imageView];
}
- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem
- (void)didTapVideoViewItem:(id<ConversationViewItem>)viewItem
attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIImageView *)imageView
{
@ -2201,7 +2206,7 @@ typedef enum : NSUInteger {
[vc presentDetailViewFromViewController:self mediaMessage:mediaMessage replacingView:imageView];
}
- (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream
- (void)didTapAudioViewItem:(id<ConversationViewItem>)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream
{
OWSAssertIsOnMainThread();
OWSAssertDebug(viewItem);
@ -2231,7 +2236,7 @@ typedef enum : NSUInteger {
[self.audioAttachmentPlayer playWithPlaybackAudioCategory];
}
- (void)didTapTruncatedTextMessage:(ConversationViewItem *)conversationItem
- (void)didTapTruncatedTextMessage:(id<ConversationViewItem>)conversationItem
{
OWSAssertIsOnMainThread();
OWSAssertDebug(conversationItem);
@ -2241,7 +2246,7 @@ typedef enum : NSUInteger {
[self.navigationController pushViewController:view animated:YES];
}
- (void)didTapContactShareViewItem:(ConversationViewItem *)conversationItem
- (void)didTapContactShareViewItem:(id<ConversationViewItem>)conversationItem
{
OWSAssertIsOnMainThread();
OWSAssertDebug(conversationItem);
@ -2276,7 +2281,7 @@ typedef enum : NSUInteger {
[self.contactShareViewHelper showAddToContactsWithContactShare:contactShare fromViewController:self];
}
- (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem
- (void)didTapFailedIncomingAttachment:(id<ConversationViewItem>)viewItem
attachmentPointer:(TSAttachmentPointer *)attachmentPointer
{
OWSAssertIsOnMainThread();
@ -2296,7 +2301,7 @@ typedef enum : NSUInteger {
[self handleUnsentMessageTap:message];
}
- (void)didTapConversationItem:(ConversationViewItem *)viewItem
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem
quotedReply:(OWSQuotedReplyModel *)quotedReply
failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer
{
@ -2334,7 +2339,7 @@ typedef enum : NSUInteger {
}];
}
- (void)didTapConversationItem:(ConversationViewItem *)viewItem quotedReply:(OWSQuotedReplyModel *)quotedReply
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem quotedReply:(OWSQuotedReplyModel *)quotedReply
{
OWSAssertIsOnMainThread();
OWSAssertDebug(viewItem);
@ -2469,7 +2474,7 @@ typedef enum : NSUInteger {
return @(groupIndex);
}
- (void)showDetailViewForViewItem:(ConversationViewItem *)conversationItem
- (void)showDetailViewForViewItem:(id<ConversationViewItem>)conversationItem
{
OWSAssertIsOnMainThread();
OWSAssertDebug(conversationItem);
@ -2484,7 +2489,7 @@ typedef enum : NSUInteger {
[self.navigationController pushViewController:view animated:YES];
}
- (void)populateReplyForViewItem:(ConversationViewItem *)conversationItem
- (void)populateReplyForViewItem:(id<ConversationViewItem>)conversationItem
{
OWSLogDebug(@"user did tap reply");
@ -2559,7 +2564,7 @@ typedef enum : NSUInteger {
NSIndexPath *_Nullable indexPathOfUnreadIndicator = [self indexPathOfUnreadMessagesIndicator];
if (indexPathOfUnreadIndicator) {
ConversationViewItem *oldIndicatorItem = [self viewItemForIndex:indexPathOfUnreadIndicator.row];
id<ConversationViewItem> oldIndicatorItem = [self viewItemForIndex:indexPathOfUnreadIndicator.row];
OWSAssertDebug(oldIndicatorItem);
// TODO ideally this would be happening within the *same* transaction that caused the unreadMessageIndicator
@ -2673,7 +2678,7 @@ typedef enum : NSUInteger {
BOOL isScrolledUp = scrollSpaceToBottom > pageHeight * 1.f;
if (self.viewItems.count > 0) {
ConversationViewItem *lastViewItem = [self.viewItems lastObject];
id<ConversationViewItem> lastViewItem = [self.viewItems lastObject];
OWSAssertDebug(lastViewItem);
if (lastViewItem.interaction.timestampForSorting > self.lastVisibleTimestamp) {
@ -3289,7 +3294,7 @@ typedef enum : NSUInteger {
case YapDatabaseViewChangeUpdate: {
YapCollectionKey *collectionKey = rowChange.collectionKey;
if (collectionKey.key) {
ConversationViewItem *_Nullable viewItem = self.viewItemCache[collectionKey.key];
id<ConversationViewItem> _Nullable viewItem = self.viewItemCache[collectionKey.key];
if (viewItem) {
[self reloadInteractionForViewItem:viewItem];
} else {
@ -3373,7 +3378,8 @@ typedef enum : NSUInteger {
(unsigned long)rowChange.finalIndex);
[self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]];
ConversationViewItem *_Nullable viewItem = [self viewItemForIndex:(NSInteger)rowChange.finalIndex];
id<ConversationViewItem> _Nullable viewItem =
[self viewItemForIndex:(NSInteger)rowChange.finalIndex];
if ([viewItem.interaction isKindOfClass:[TSOutgoingMessage class]]) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction;
if (!outgoingMessage.isFromLinkedDevice) {
@ -3508,7 +3514,7 @@ typedef enum : NSUInteger {
isOnlyModifyingLastMessage = NO;
break;
case YapDatabaseViewChangeInsert: {
ConversationViewItem *_Nullable viewItem = [self viewItemForIndex:(NSInteger)rowChange.finalIndex];
id<ConversationViewItem> _Nullable viewItem = [self viewItemForIndex:(NSInteger)rowChange.finalIndex];
if (([viewItem.interaction isKindOfClass:[TSIncomingMessage class]] ||
[viewItem.interaction isKindOfClass:[TSOutgoingMessage class]])
&& rowChange.finalIndex >= oldViewItemCount) {
@ -3524,7 +3530,7 @@ typedef enum : NSUInteger {
if (rowChange.changes == YapDatabaseViewChangedDependency) {
continue;
}
ConversationViewItem *_Nullable viewItem = [self viewItemForIndex:(NSInteger)rowChange.finalIndex];
id<ConversationViewItem> _Nullable viewItem = [self viewItemForIndex:(NSInteger)rowChange.finalIndex];
if (([viewItem.interaction isKindOfClass:[TSIncomingMessage class]] ||
[viewItem.interaction isKindOfClass:[TSOutgoingMessage class]])
&& rowChange.finalIndex >= oldViewItemCount) {
@ -3855,7 +3861,7 @@ typedef enum : NSUInteger {
return lastVisibleIndexPath;
}
- (nullable ConversationViewItem *)lastVisibleViewItem
- (nullable id<ConversationViewItem>)lastVisibleViewItem
{
NSIndexPath *_Nullable lastVisibleIndexPath = [self lastVisibleIndexPath];
if (!lastVisibleIndexPath) {
@ -3870,7 +3876,7 @@ typedef enum : NSUInteger {
- (void)didScrollToBottom
{
ConversationViewItem *_Nullable lastVisibleViewItem = [self.viewItems lastObject];
id<ConversationViewItem> _Nullable lastVisibleViewItem = [self.viewItems lastObject];
if (lastVisibleViewItem) {
uint64_t lastVisibleTimestamp = lastVisibleViewItem.interaction.timestampForSorting;
self.lastVisibleTimestamp = MAX(self.lastVisibleTimestamp, lastVisibleTimestamp);
@ -3883,7 +3889,7 @@ typedef enum : NSUInteger {
- (void)updateLastVisibleTimestamp
{
ConversationViewItem *_Nullable lastVisibleViewItem = [self lastVisibleViewItem];
id<ConversationViewItem> _Nullable lastVisibleViewItem = [self lastVisibleViewItem];
if (lastVisibleViewItem) {
uint64_t lastVisibleTimestamp = lastVisibleViewItem.interaction.timestampForSorting;
self.lastVisibleTimestamp = MAX(self.lastVisibleTimestamp, lastVisibleTimestamp);
@ -4640,7 +4646,7 @@ typedef enum : NSUInteger {
// any new items inserted while we were not observing. We therefore find the
// first item at or after the "view horizon". See the comments below which explain
// the "view horizon".
ConversationViewItem *_Nullable lastViewItem = self.viewItems.lastObject;
id<ConversationViewItem> _Nullable lastViewItem = self.viewItems.lastObject;
BOOL hasAddedNewItems = (lastViewItem && previousLastTimestamp
&& lastViewItem.interaction.timestamp > previousLastTimestamp.unsignedLongLongValue);
@ -4671,7 +4677,7 @@ typedef enum : NSUInteger {
// We'll use this later to update the view to reflect any changes made while
// we were not observing the database. See extendRangeToIncludeUnobservedItems
// and the logic above.
ConversationViewItem *_Nullable lastViewItem = self.viewItems.lastObject;
id<ConversationViewItem> _Nullable lastViewItem = self.viewItems.lastObject;
if (lastViewItem) {
self.previousLastTimestamp = @(lastViewItem.interaction.timestamp);
} else {
@ -4717,7 +4723,7 @@ typedef enum : NSUInteger {
NSUInteger mid = (left + right) / 2;
OWSAssertDebug(left <= mid);
OWSAssertDebug(mid < right);
ConversationViewItem *viewItem = self.viewItems[mid];
id<ConversationViewItem> viewItem = self.viewItems[mid];
if (viewItem.interaction.timestamp >= viewHorizonTimestamp) {
right = mid;
} else {
@ -4726,7 +4732,7 @@ typedef enum : NSUInteger {
}
}
OWSAssertDebug(left == right);
ConversationViewItem *viewItem = self.viewItems[left];
id<ConversationViewItem> viewItem = self.viewItems[left];
if (viewItem.interaction.timestamp >= viewHorizonTimestamp) {
OWSLogInfo(@"firstIndexPathAtViewHorizonTimestamp: %zd / %zd", left, self.viewItems.count);
return [NSIndexPath indexPathForRow:(NSInteger) left inSection:0];
@ -4860,8 +4866,8 @@ typedef enum : NSUInteger {
// Returns NO on error.
- (BOOL)reloadViewItems
{
NSMutableArray<ConversationViewItem *> *viewItems = [NSMutableArray new];
NSMutableDictionary<NSString *, ConversationViewItem *> *viewItemCache = [NSMutableDictionary new];
NSMutableArray<id<ConversationViewItem>> *viewItems = [NSMutableArray new];
NSMutableDictionary<NSString *, id<ConversationViewItem>> *viewItemCache = [NSMutableDictionary new];
NSUInteger count = [self.messageMappings numberOfItemsInSection:0];
BOOL isGroupThread = self.isGroupConversation;
@ -4890,12 +4896,12 @@ typedef enum : NSUInteger {
continue;
}
ConversationViewItem *_Nullable viewItem = self.viewItemCache[interaction.uniqueId];
id<ConversationViewItem> _Nullable viewItem = self.viewItemCache[interaction.uniqueId];
if (!viewItem) {
viewItem = [[ConversationViewItem alloc] initWithInteraction:interaction
isGroupThread:isGroupThread
transaction:transaction
conversationStyle:self.conversationStyle];
viewItem = [[ConversationInteractionViewItem alloc] initWithInteraction:interaction
isGroupThread:isGroupThread
transaction:transaction
conversationStyle:self.conversationStyle];
}
[viewItems addObject:viewItem];
OWSAssertDebug(!viewItemCache[interaction.uniqueId]);
@ -4916,7 +4922,7 @@ typedef enum : NSUInteger {
uint64_t collapseCutoffTimestamp = [NSDate ows_millisecondsSince1970ForDate:self.collapseCutoffDate];
BOOL hasPlacedUnreadIndicator = NO;
for (ConversationViewItem *viewItem in viewItems) {
for (id<ConversationViewItem> viewItem in viewItems) {
BOOL canShowDate = NO;
switch (viewItem.interaction.interactionType) {
case OWSInteractionType_Unknown:
@ -4992,9 +4998,9 @@ typedef enum : NSUInteger {
//
// NOTE: This logic uses the break properties which are set in the previous pass.
for (NSUInteger i = 0; i < viewItems.count; i++) {
ConversationViewItem *viewItem = viewItems[i];
ConversationViewItem *_Nullable previousViewItem = (i > 0 ? viewItems[i - 1] : nil);
ConversationViewItem *_Nullable nextViewItem = (i + 1 < viewItems.count ? viewItems[i + 1] : nil);
id<ConversationViewItem> viewItem = viewItems[i];
id<ConversationViewItem> _Nullable previousViewItem = (i > 0 ? viewItems[i - 1] : nil);
id<ConversationViewItem> _Nullable nextViewItem = (i + 1 < viewItems.count ? viewItems[i + 1] : nil);
BOOL shouldShowSenderAvatar = NO;
BOOL shouldHideFooter = NO;
BOOL isFirstInCluster = YES;
@ -5143,7 +5149,7 @@ typedef enum : NSUInteger {
// Whenever an interaction is modified, we need to reload it from the DB
// and update the corresponding view item.
- (void)reloadInteractionForViewItem:(ConversationViewItem *)viewItem
- (void)reloadInteractionForViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertIsOnMainThread();
OWSAssertDebug(viewItem);
@ -5164,7 +5170,7 @@ typedef enum : NSUInteger {
}];
}
- (nullable ConversationViewItem *)viewItemForIndex:(NSInteger)index
- (nullable id<ConversationViewItem>)viewItemForIndex:(NSInteger)index
{
if (index < 0 || index >= (NSInteger)self.viewItems.count) {
OWSFailDebug(@"Invalid view item index: %lu", (unsigned long)index);
@ -5183,7 +5189,7 @@ typedef enum : NSUInteger {
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ConversationViewItem *_Nullable viewItem = [self viewItemForIndex:indexPath.row];
id<ConversationViewItem> _Nullable viewItem = [self viewItemForIndex:indexPath.row];
ConversationViewCell *cell = [viewItem dequeueCellForCollectionView:self.collectionView indexPath:indexPath];
if (!cell) {
OWSFailDebug(@"Could not dequeue cell.");

@ -43,7 +43,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
//
// Critically, this class implements ConversationViewLayoutItem
// and does caching of the cell's size.
@interface ConversationViewItem : NSObject <ConversationViewLayoutItem, OWSAudioPlayerDelegate>
@protocol ConversationViewItem <NSObject, ConversationViewLayoutItem, OWSAudioPlayerDelegate>
@property (nonatomic, readonly) TSInteraction *interaction;
@ -69,14 +69,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@property (nonatomic, nullable) OWSUnreadIndicator *unreadIndicator;
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithInteraction:(TSInteraction *)interaction
isGroupThread:(BOOL)isGroupThread
transaction:(YapDatabaseReadTransaction *)transaction
conversationStyle:(ConversationStyle *)conversationStyle;
- (ConversationViewCell *)dequeueCellForCollectionView:(UICollectionView *)collectionView
indexPath:(NSIndexPath *)indexPath;
@ -89,21 +81,20 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@property (nonatomic, weak) OWSAudioMessageView *lastAudioMessageView;
@property (nonatomic, readonly) CGFloat audioDurationSeconds;
- (CGFloat)audioProgressSeconds;
@property (nonatomic, readonly) CGFloat audioProgressSeconds;
#pragma mark - View State Caching
// These methods only apply to text & attachment messages.
- (OWSMessageCellType)messageCellType;
- (nullable DisplayableText *)displayableBodyText;
- (nullable TSAttachmentStream *)attachmentStream;
- (nullable TSAttachmentPointer *)attachmentPointer;
- (CGSize)mediaSize;
@property (nonatomic, readonly) OWSMessageCellType messageCellType;
@property (nonatomic, readonly, nullable) DisplayableText *displayableBodyText;
@property (nonatomic, readonly, nullable) TSAttachmentStream *attachmentStream;
@property (nonatomic, readonly, nullable) TSAttachmentPointer *attachmentPointer;
@property (nonatomic, readonly) CGSize mediaSize;
- (nullable DisplayableText *)displayableQuotedText;
- (nullable NSString *)quotedAttachmentMimetype;
- (nullable NSString *)quotedRecipientId;
@property (nonatomic, readonly, nullable) DisplayableText *displayableQuotedText;
@property (nonatomic, readonly, nullable) NSString *quotedAttachmentMimetype;
@property (nonatomic, readonly, nullable) NSString *quotedRecipientId;
// We don't want to try to load the media for this item (if any)
// if a load has previously failed.
@ -132,4 +123,15 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@end
@interface ConversationInteractionViewItem
: NSObject <ConversationViewItem, ConversationViewLayoutItem, OWSAudioPlayerDelegate>
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithInteraction:(TSInteraction *)interaction
isGroupThread:(BOOL)isGroupThread
transaction:(YapDatabaseReadTransaction *)transaction
conversationStyle:(ConversationStyle *)conversationStyle;
@end
NS_ASSUME_NONNULL_END

@ -46,7 +46,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
#pragma mark -
@interface ConversationViewItem ()
@interface ConversationInteractionViewItem ()
@property (nonatomic, nullable) NSValue *cachedCellSize;
@ -70,12 +70,25 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
@property (nonatomic, nullable) NSString *systemMessageText;
@property (nonatomic, nullable) TSThread *incomingMessageAuthorThread;
@property (nonatomic, nullable) NSString *authorConversationColorName;
@property (nonatomic, nullable) ConversationStyle *conversationStyle;
@end
#pragma mark -
@implementation ConversationViewItem
@implementation ConversationInteractionViewItem
@synthesize shouldShowDate = _shouldShowDate;
@synthesize shouldShowSenderAvatar = _shouldShowSenderAvatar;
@synthesize unreadIndicator = _unreadIndicator;
@synthesize didCellMediaFailToLoad = _didCellMediaFailToLoad;
@synthesize interaction = _interaction;
@synthesize isFirstInCluster = _isFirstInCluster;
@synthesize isGroupThread = _isGroupThread;
@synthesize isLastInCluster = _isLastInCluster;
@synthesize lastAudioMessageView = _lastAudioMessageView;
@synthesize senderName = _senderName;
@synthesize shouldHideFooter = _shouldHideFooter;
- (instancetype)initWithInteraction:(TSInteraction *)interaction
isGroupThread:(BOOL)isGroupThread
@ -293,7 +306,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
return measurementCell;
}
- (CGFloat)vSpacingWithPreviousLayoutItem:(ConversationViewItem *)previousLayoutItem
- (CGFloat)vSpacingWithPreviousLayoutItem:(id<ConversationViewItem>)previousLayoutItem
{
OWSAssertDebug(previousLayoutItem);

@ -1967,10 +1967,11 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssertDebug(messageToQuote);
OWSLogVerbose(@"%@", label);
[DDLog flushLog];
ConversationViewItem *viewItem = [[ConversationViewItem alloc] initWithInteraction:messageToQuote
isGroupThread:thread.isGroupThread
transaction:transaction
conversationStyle:conversationStyle];
id<ConversationViewItem> viewItem =
[[ConversationInteractionViewItem alloc] initWithInteraction:messageToQuote
isGroupThread:thread.isGroupThread
transaction:transaction
conversationStyle:conversationStyle];
quotedMessage = [
[OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:viewItem transaction:transaction]
buildQuotedMessageForSending];
@ -1986,10 +1987,11 @@ NS_ASSUME_NONNULL_BEGIN
transaction:transaction];
OWSAssertDebug(messageToQuote);
ConversationViewItem *viewItem = [[ConversationViewItem alloc] initWithInteraction:messageToQuote
isGroupThread:thread.isGroupThread
transaction:transaction
conversationStyle:conversationStyle];
id<ConversationViewItem> viewItem =
[[ConversationInteractionViewItem alloc] initWithInteraction:messageToQuote
isGroupThread:thread.isGroupThread
transaction:transaction
conversationStyle:conversationStyle];
quotedMessage = [
[OWSQuotedReplyModel quotedReplyForSendingWithConversationViewItem:viewItem transaction:transaction]
buildQuotedMessageForSending];

@ -37,7 +37,7 @@ public class LongTextViewController: OWSViewController {
guard viewItem.hasBodyText else {
return ""
}
guard let displayableText = viewItem.displayableBodyText() else {
guard let displayableText = viewItem.displayableBodyText else {
return ""
}
let messageBody = displayableText.fullText

@ -6,7 +6,8 @@
NS_ASSUME_NONNULL_BEGIN
@class ConversationViewItem;
@protocol ConversationViewItem;
@class GalleryItemBox;
@class MediaDetailViewController;
@ -18,7 +19,7 @@ typedef NS_OPTIONS(NSInteger, MediaGalleryOption) {
@protocol MediaDetailViewControllerDelegate <NSObject>
- (void)mediaDetailViewController:(MediaDetailViewController *)mediaDetailViewController
requestDeleteConversationViewItem:(ConversationViewItem *)conversationViewItem;
requestDeleteConversationViewItem:(id<ConversationViewItem>)conversationViewItem;
- (void)mediaDetailViewController:(MediaDetailViewController *)mediaDetailViewController
isPlayingVideo:(BOOL)isPlayingVideo;
@ -34,7 +35,7 @@ typedef NS_OPTIONS(NSInteger, MediaGalleryOption) {
// If viewItem is non-null, long press will show a menu controller.
- (instancetype)initWithGalleryItemBox:(GalleryItemBox *)galleryItemBox
viewItem:(ConversationViewItem *_Nullable)viewItem;
viewItem:(nullable id<ConversationViewItem>)viewItem;
#pragma mark - Actions
- (void)didPressShare:(id)sender;

@ -36,7 +36,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) UIButton *shareButton;
@property (nonatomic) TSAttachmentStream *attachmentStream;
@property (nonatomic, nullable) ConversationViewItem *viewItem;
@property (nonatomic, nullable) id<ConversationViewItem> viewItem;
@property (nonatomic, nullable) UIImage *image;
@property (nonatomic, nullable) OWSVideoPlayer *videoPlayer;
@ -63,7 +63,7 @@ NS_ASSUME_NONNULL_BEGIN
}
- (instancetype)initWithGalleryItemBox:(GalleryItemBox *)galleryItemBox
viewItem:(ConversationViewItem *_Nullable)viewItem
viewItem:(nullable id<ConversationViewItem>)viewItem
{
self = [super initWithNibName:nil bundle:nil];
if (!self) {

@ -479,10 +479,10 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
let message = galleryItem.message
let thread = message.thread(with: transaction)
let conversationStyle = ConversationStyle(thread: thread)
fetchedItem = ConversationViewItem(interaction: message,
isGroupThread: thread.isGroupThread(),
transaction: transaction,
conversationStyle: conversationStyle)
fetchedItem = ConversationInteractionViewItem(interaction: message,
isGroupThread: thread.isGroupThread(),
transaction: transaction,
conversationStyle: conversationStyle)
}
guard let viewItem = fetchedItem else {

@ -326,7 +326,7 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
guard viewItem.hasBodyText else {
return nil
}
guard let displayableText = viewItem.displayableBodyText() else {
guard let displayableText = viewItem.displayableBodyText else {
return nil
}
let messageBody = displayableText.fullText
@ -663,7 +663,7 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
if let audioAttachmentPlayer = self.audioAttachmentPlayer {
// Is this player associated with this media adapter?
if audioAttachmentPlayer.owner as? ConversationViewItem == viewItem {
if audioAttachmentPlayer.owner === viewItem {
// Tap to pause & unpause.
audioAttachmentPlayer.togglePlayState()
return

@ -250,9 +250,7 @@ const CGFloat kIconViewLength = 24;
[[OWSDisappearingMessagesConfiguration alloc] initDefaultWithThreadId:self.thread.uniqueId];
}
NSString *colorName = self.thread.conversationColorName;
OWSConversationColor *currentConversationColor = [OWSConversationColor conversationColorOrDefaultForColorName:colorName];
self.colorPicker = [[OWSColorPicker alloc] initWithCurrentConversationColor:currentConversationColor];
self.colorPicker = [[OWSColorPicker alloc] initWithThread:self.thread];
self.colorPicker.delegate = self;
[self updateTableContents];

@ -39,22 +39,22 @@
return @"abc";
}
- (ConversationViewItem *)textViewItem
- (ConversationInteractionViewItem *)textViewItem
{
TSOutgoingMessage *message =
[TSOutgoingMessage outgoingMessageInThread:self.thread messageBody:self.fakeTextMessageText attachmentId:nil];
[message save];
__block ConversationViewItem *viewItem = nil;
__block ConversationInteractionViewItem *viewItem = nil;
[self readWithBlock:^(YapDatabaseReadTransaction *transaction) {
viewItem = [[ConversationViewItem alloc] initWithInteraction:message
isGroupThread:NO
transaction:transaction
conversationStyle:self.conversationStyle];
viewItem = [[ConversationInteractionViewItem alloc] initWithInteraction:message
isGroupThread:NO
transaction:transaction
conversationStyle:self.conversationStyle];
}];
return viewItem;
}
- (ConversationViewItem *)viewItemWithAttachmentMimetype:(NSString *)mimeType filename:(NSString *)filename
- (ConversationInteractionViewItem *)viewItemWithAttachmentMimetype:(NSString *)mimeType filename:(NSString *)filename
{
OWSAssertDebug(filename.length > 0);
@ -74,33 +74,33 @@
[TSOutgoingMessage outgoingMessageInThread:self.thread messageBody:nil attachmentId:attachment.uniqueId];
[message save];
__block ConversationViewItem *viewItem = nil;
__block ConversationInteractionViewItem *viewItem = nil;
[self readWithBlock:^(YapDatabaseReadTransaction *transaction) {
viewItem = [[ConversationViewItem alloc] initWithInteraction:message
isGroupThread:NO
transaction:transaction
conversationStyle:self.conversationStyle];
viewItem = [[ConversationInteractionViewItem alloc] initWithInteraction:message
isGroupThread:NO
transaction:transaction
conversationStyle:self.conversationStyle];
}];
return viewItem;
}
- (ConversationViewItem *)stillImageViewItem
- (ConversationInteractionViewItem *)stillImageViewItem
{
return [self viewItemWithAttachmentMimetype:OWSMimeTypeImageJpeg filename:@"test-jpg.jpg"];
}
- (ConversationViewItem *)animatedImageViewItem
- (ConversationInteractionViewItem *)animatedImageViewItem
{
return [self viewItemWithAttachmentMimetype:OWSMimeTypeImageGif filename:@"test-gif.gif"];
}
- (ConversationViewItem *)videoViewItem
- (ConversationInteractionViewItem *)videoViewItem
{
return [self viewItemWithAttachmentMimetype:@"video/mp4" filename:@"test-mp4.mp4"];
}
- (ConversationViewItem *)audioViewItem
- (ConversationInteractionViewItem *)audioViewItem
{
return [self viewItemWithAttachmentMimetype:@"audio/mp3" filename:@"test-mp3.mp3"];
}
@ -109,7 +109,7 @@
- (void)testPerformDeleteEditingActionWithNonMediaMessage
{
ConversationViewItem *viewItem = self.textViewItem;
ConversationInteractionViewItem *viewItem = self.textViewItem;
XCTAssertNotNil([TSMessage fetchObjectWithUniqueID:viewItem.interaction.uniqueId]);
[viewItem deleteAction];
@ -118,7 +118,7 @@
- (void)testPerformDeleteActionWithPhotoMessage
{
ConversationViewItem *viewItem = self.stillImageViewItem;
ConversationInteractionViewItem *viewItem = self.stillImageViewItem;
XCTAssertEqual((NSUInteger)1, ((TSMessage *)viewItem.interaction).attachmentIds.count);
NSString *_Nullable attachmentId = ((TSMessage *)viewItem.interaction).attachmentIds.firstObject;
@ -140,7 +140,7 @@
- (void)testPerformDeleteEditingActionWithAnimatedMessage
{
ConversationViewItem *viewItem = self.animatedImageViewItem;
ConversationInteractionViewItem *viewItem = self.animatedImageViewItem;
XCTAssertEqual((NSUInteger)1, ((TSMessage *)viewItem.interaction).attachmentIds.count);
NSString *_Nullable attachmentId = ((TSMessage *)viewItem.interaction).attachmentIds.firstObject;
@ -162,7 +162,7 @@
- (void)testPerformDeleteEditingActionWithVideoMessage
{
ConversationViewItem *viewItem = self.videoViewItem;
ConversationInteractionViewItem *viewItem = self.videoViewItem;
XCTAssertEqual((NSUInteger)1, ((TSMessage *)viewItem.interaction).attachmentIds.count);
NSString *_Nullable attachmentId = ((TSMessage *)viewItem.interaction).attachmentIds.firstObject;
@ -184,7 +184,7 @@
- (void)testPerformDeleteEditingActionWithAudioMessage
{
ConversationViewItem *viewItem = self.audioViewItem;
ConversationInteractionViewItem *viewItem = self.audioViewItem;
XCTAssertEqual((NSUInteger)1, ((TSMessage *)viewItem.interaction).attachmentIds.count);
NSString *_Nullable attachmentId = ((TSMessage *)viewItem.interaction).attachmentIds.firstObject;
@ -212,7 +212,7 @@
UIPasteboard.generalPasteboard.items = @[];
XCTAssertNil(UIPasteboard.generalPasteboard.string);
ConversationViewItem *viewItem = self.textViewItem;
ConversationInteractionViewItem *viewItem = self.textViewItem;
[viewItem copyTextAction];
XCTAssertEqualObjects(self.fakeTextMessageText, UIPasteboard.generalPasteboard.string);
}
@ -224,7 +224,7 @@
XCTAssertNil(UIPasteboard.generalPasteboard.image);
XCTAssertNil([UIPasteboard.generalPasteboard dataForPasteboardType:(NSString *)kUTTypeJPEG]);
ConversationViewItem *viewItem = self.stillImageViewItem;
ConversationInteractionViewItem *viewItem = self.stillImageViewItem;
[viewItem copyMediaAction];
NSData *_Nullable copiedData = [UIPasteboard.generalPasteboard dataForPasteboardType:(NSString *)kUTTypeJPEG];
XCTAssertTrue(copiedData.length > 0);
@ -237,7 +237,7 @@
XCTAssertNil(UIPasteboard.generalPasteboard.image);
XCTAssertNil([UIPasteboard.generalPasteboard dataForPasteboardType:(NSString *)kUTTypeGIF]);
ConversationViewItem *viewItem = self.animatedImageViewItem;
ConversationInteractionViewItem *viewItem = self.animatedImageViewItem;
[viewItem copyMediaAction];
NSData *_Nullable copiedData = [UIPasteboard.generalPasteboard dataForPasteboardType:(NSString *)kUTTypeGIF];
XCTAssertTrue(copiedData.length > 0);
@ -249,7 +249,7 @@
UIPasteboard.generalPasteboard.items = @[];
XCTAssertNil([UIPasteboard.generalPasteboard dataForPasteboardType:(NSString *)kUTTypeMPEG4]);
ConversationViewItem *viewItem = self.videoViewItem;
ConversationInteractionViewItem *viewItem = self.videoViewItem;
[viewItem copyMediaAction];
NSData *_Nullable copiedData = [UIPasteboard.generalPasteboard dataForPasteboardType:(NSString *)kUTTypeMPEG4];
XCTAssertTrue(copiedData.length > 0);
@ -261,7 +261,7 @@
UIPasteboard.generalPasteboard.items = @[];
XCTAssertNil([UIPasteboard.generalPasteboard dataForPasteboardType:(NSString *)kUTTypeMP3]);
ConversationViewItem *viewItem = self.audioViewItem;
ConversationInteractionViewItem *viewItem = self.audioViewItem;
[viewItem copyMediaAction];
NSData *_Nullable copiedData = [UIPasteboard.generalPasteboard dataForPasteboardType:(NSString *)kUTTypeMP3];
XCTAssertTrue(copiedData.length > 0);

@ -386,6 +386,12 @@
/* Error indicating that the app was prevented from accessing the user's CloudKit account. */
"CLOUDKIT_STATUS_RESTRICTED" = "Signal was not allowed to access your iCloud account for backups.";
/* The first of two messages demonstrating the chosen conversation color, by rendering this message in an outgoing message bubble. */
"COLOR_PICKER_DEMO_MESSAGE_1" = "Choose the color of outgoing messages in this conversation.";
/* The second of two messages demonstrating the chosen conversation color, by rendering this message in an incoming message bubble. */
"COLOR_PICKER_DEMO_MESSAGE_2" = "Only you will see the color you choose.";
/* Modal Sheet title when picking a conversation color. */
"COLOR_PICKER_SHEET_TITLE" = "Conversation Color";

@ -6,7 +6,8 @@
NS_ASSUME_NONNULL_BEGIN
@class ConversationViewItem;
@protocol ConversationViewItem;
@class TSAttachmentPointer;
@class TSAttachmentStream;
@class TSMessage;
@ -42,7 +43,7 @@ NS_ASSUME_NONNULL_BEGIN
transaction:(YapDatabaseReadTransaction *)transaction;
// Builds a not-yet-sent QuotedReplyModel
+ (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(ConversationViewItem *)conversationItem
+ (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(id<ConversationViewItem>)conversationItem
transaction:(YapDatabaseReadTransaction *)transaction;
- (TSQuotedMessage *)buildQuotedMessageForSending;

@ -115,7 +115,7 @@ NS_ASSUME_NONNULL_BEGIN
thumbnailDownloadFailed:thumbnailDownloadFailed];
}
+ (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(ConversationViewItem *)conversationItem
+ (nullable instancetype)quotedReplyForSendingWithConversationViewItem:(id<ConversationViewItem>)conversationItem
transaction:(YapDatabaseReadTransaction *)transaction;
{
OWSAssertDebug(conversationItem);

Loading…
Cancel
Save