Merge branch 'charlesmchen/cvc73'

This commit is contained in:
Matthew Chen 2020-12-15 11:13:33 -03:00
commit fe95bcfb61
15 changed files with 150 additions and 67 deletions

View File

@ -100,7 +100,10 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
let timestampLabel = componentView.timestampLabel
let textColor: UIColor
if isOverlayingMedia {
if wasRemotelyDeleted {
owsAssertDebug(!isOverlayingMedia)
textColor = Theme.primaryTextColor
} else if isOverlayingMedia {
textColor = .ows_white
} else if isOutsideBubble {
textColor = Theme.secondaryTextAndIconColor
@ -218,7 +221,7 @@ public class CVComponentFooter: CVComponentBase, CVComponent {
}
private var tapForMoreLabelConfig: CVLabelConfig? {
guard hasTapForMore else {
guard hasTapForMore, !wasRemotelyDeleted else {
return nil
}
guard let message = interaction as? TSMessage else {

View File

@ -73,6 +73,7 @@ public class CVViewState: NSObject {
public var scrollContinuity: ScrollContinuity = .bottom
public var scrollContinuityMap: CVScrollContinuityMap?
public var scrollActionForSizeTransition: CVScrollAction?
public var scrollActionForUpdate: CVScrollAction?
public var lastKnownDistanceFromBottom: CGFloat?
@objc
public var lastSearchedText: String?

View File

@ -457,6 +457,7 @@ extension ConversationViewController: CVLoadCoordinatorDelegate {
let renderState = update.renderState
let isScrolledToBottom = updateToken.isScrolledToBottom
let viewState = self.viewState
var scrollAction = scrollActionParam
@ -469,23 +470,11 @@ extension ConversationViewController: CVLoadCoordinatorDelegate {
owsFailDebug("Missing loadType.")
}
let batchUpdatesBlock = {
AssertIsOnMainThread()
let section = Self.messageSection
var hasInserted = false
var hasUpdated = false
// Update scroll action to auto-scroll if necessary.
if scrollAction.action == .none, !self.isUserScrolling {
for item in items {
switch item {
case .insert(let renderItem, let newIndex):
// Always perform inserts before updates.
owsAssertDebug(!hasUpdated)
Logger.verbose("insert newIndex: \(newIndex)")
let indexPath = IndexPath(row: newIndex, section: section)
self.collectionView.insertItems(at: [indexPath])
hasInserted = true
case .insert(let renderItem, _):
var wasJustInserted = false
if let lastMessageForInboxSortId = updateToken.lastMessageForInboxSortId,
@ -500,31 +489,54 @@ extension ConversationViewController: CVLoadCoordinatorDelegate {
case .typingIndicator:
isAutoScrollInteraction = true
case .incomingMessage,
.outgoingMessage,
.call,
.error,
.info:
.outgoingMessage,
.call,
.error,
.info:
isAutoScrollInteraction = wasJustInserted
default:
isAutoScrollInteraction = false
}
if scrollAction.action == .none,
!self.isUserScrolling {
if let outgoingMessage = renderItem.interaction as? TSOutgoingMessage,
!outgoingMessage.isFromLinkedDevice,
wasJustInserted {
// Whenever we send an outgoing message from the local device,
// auto-scroll to the bottom of the conversation, regardless
// of scroll state.
scrollAction = CVScrollAction(action: .bottomOfLoadWindow, isAnimated: false)
} else if isAutoScrollInteraction,
isScrolledToBottom {
// If we're already at the bottom of the conversation and
// a freshly inserted message or typing indicator appears,
// auto-scroll to show it.
scrollAction = CVScrollAction(action: .bottomOfLoadWindow, isAnimated: false)
}
if let outgoingMessage = renderItem.interaction as? TSOutgoingMessage,
!outgoingMessage.isFromLinkedDevice,
wasJustInserted {
// Whenever we send an outgoing message from the local device,
// auto-scroll to the bottom of the conversation, regardless
// of scroll state.
scrollAction = CVScrollAction(action: .bottomOfLoadWindow, isAnimated: false)
break
} else if isAutoScrollInteraction,
isScrolledToBottom {
// If we're already at the bottom of the conversation and
// a freshly inserted message or typing indicator appears,
// auto-scroll to show it.
scrollAction = CVScrollAction(action: .bottomOfLoadWindow, isAnimated: false)
break
}
default:
break
}
}
}
viewState.scrollActionForUpdate = scrollAction
let batchUpdatesBlock = {
AssertIsOnMainThread()
let section = Self.messageSection
var hasInserted = false
var hasUpdated = false
for item in items {
switch item {
case .insert(_, let newIndex):
// Always perform inserts before updates.
owsAssertDebug(!hasUpdated)
Logger.verbose("insert newIndex: \(newIndex)")
let indexPath = IndexPath(row: newIndex, section: section)
self.collectionView.insertItems(at: [indexPath])
hasInserted = true
case .update(_, let oldIndex, _):
Logger.verbose("update oldIndex: \(oldIndex)")
let indexPath = IndexPath(row: oldIndex, section: section)
@ -554,6 +566,8 @@ extension ConversationViewController: CVLoadCoordinatorDelegate {
self.perform(scrollAction: scrollAction)
viewState.scrollActionForUpdate = nil
if !finished {
Logger.warn("performBatchUpdates did not finish")
Logger.warn("Layout: \(self.layout.debugDescription)")

View File

@ -24,6 +24,7 @@ extension ConversationViewController {
// with the updated -contentOffset in -scrollViewDidScroll:. So instead, we'll just see what layoutAttributes
// are now in the collection view's visible content rect. This should be safe, since it's computed from the
// already updated -contentOffset.
layout.prepare()
let visibleLayoutAttributes = layout.layoutAttributesForElements(in: visibleContentRect) ?? []
let lastVisibleIndexPath = visibleLayoutAttributes
@ -91,6 +92,8 @@ extension ConversationViewController {
}
private func percentOfIndexPathVisibleAboveBottom(_ indexPath: IndexPath) -> CGFloat {
layout.prepare()
// If we don't have layout attributes, it's not visible
guard let attributes = layout.layoutAttributesForItem(at: indexPath) else { return 0.0 }

View File

@ -87,7 +87,7 @@ public struct CVScrollAction: Equatable, CustomStringConvertible {
// MARK: - CustomStringConvertible
public var description: String {
"action: \(action), isAnimated: \(isAnimated)"
"[scrollAction: \(action), isAnimated: \(isAnimated)]"
}
}
@ -236,6 +236,7 @@ extension ConversationViewController {
guard !isUserScrolling else { return }
view.layoutIfNeeded()
layout.prepare()
guard let attributes = layout.layoutAttributesForItem(at: indexPath) else {
return owsFailDebug("failed to get attributes for indexPath \(indexPath)")
@ -505,23 +506,28 @@ extension ConversationViewController {
Logger.verbose("---- proposedContentOffset: \(proposedContentOffset)")
if isPresentingMessageActions,
let contentOffset = contentOffsetForMessageActionInteraction {
Logger.verbose("---- contentOffsetForMessageActionInteraction: \(contentOffset)")
let contentOffset = targetContentOffsetForMessageActionInteraction {
Logger.verbose("---- targetContentOffsetForMessageActionInteraction: \(contentOffset)")
return contentOffset
}
if let contentOffset = contentOffsetForSizeTransition() {
Logger.verbose("---- contentOffsetForSizeTransition: \(contentOffset)")
if let contentOffset = targetContentOffsetForSizeTransition() {
Logger.verbose("---- targetContentOffsetForSizeTransition: \(contentOffset)")
return contentOffset
}
if let contentOffset = contentOffsetForScrollContinuityMap() {
Logger.verbose("---- contentOffsetForScrollContinuityMap: \(contentOffset)")
if let contentOffset = targetContentOffsetForUpdate() {
Logger.verbose("---- targetContentOffsetForUpdate: \(contentOffset)")
return contentOffset
}
if let contentOffset = targetContentOffsetForScrollContinuityMap() {
Logger.verbose("---- targetContentOffsetForScrollContinuityMap: \(contentOffset)")
return contentOffset
}
if scrollContinuity == .bottom,
let contentOffset = contentOffsetForBottom() {
let contentOffset = targetContentOffsetForBottom() {
Logger.verbose("---- forLastKnownDistanceFromBottom: \(contentOffset)")
return contentOffset
}
@ -530,7 +536,7 @@ extension ConversationViewController {
return proposedContentOffset
}
private func contentOffsetForBottom() -> CGPoint? {
private func targetContentOffsetForBottom() -> CGPoint? {
guard let lastKnownDistanceFromBottom = self.lastKnownDistanceFromBottom else {
return nil
}
@ -607,7 +613,7 @@ extension ConversationViewController {
// We use this hook to ensure scroll state continuity. As the collection
// view's content size changes, we want to keep the same cells in view.
private func contentOffsetForScrollContinuityMap() -> CGPoint? {
private func targetContentOffsetForScrollContinuityMap() -> CGPoint? {
guard let scrollContinuityMap = viewState.scrollContinuityMap else {
return nil
}
@ -652,10 +658,27 @@ extension ConversationViewController {
return nil
}
private func contentOffsetForSizeTransition() -> CGPoint? {
private func targetContentOffsetForSizeTransition() -> CGPoint? {
guard let scrollAction = viewState.scrollActionForSizeTransition else {
return nil
}
owsAssertDebug(!scrollAction.isAnimated)
return targetContentOffsetForScrollAction(scrollAction)
}
private func targetContentOffsetForUpdate() -> CGPoint? {
guard let scrollAction = viewState.scrollActionForUpdate else {
return nil
}
guard scrollAction.action != .none, !scrollAction.isAnimated else {
return nil
}
return targetContentOffsetForScrollAction(scrollAction)
}
private func targetContentOffsetForScrollAction(_ scrollAction: CVScrollAction) -> CGPoint? {
owsAssertDebug(!scrollAction.isAnimated)
layout.prepare()
switch scrollAction.action {
@ -705,7 +728,7 @@ extension ConversationViewController {
}
}
private var contentOffsetForMessageActionInteraction: CGPoint? {
private var targetContentOffsetForMessageActionInteraction: CGPoint? {
guard isPresentingMessageActions,
let messageActionsViewController = messageActionsViewController else {
owsFailDebug("Not presenting message actions.")
@ -718,6 +741,7 @@ extension ConversationViewController {
// This is expected if the menu action interaction is being deleted.
return nil
}
layout.prepare()
guard let layoutAttributes = layout.layoutAttributesForItem(at: indexPath) else {
owsFailDebug("Missing layoutAttributes.")
return nil

View File

@ -200,6 +200,7 @@ extension ConversationViewController {
return nil
}
layout.prepare()
guard let firstFrame = self.layout.layoutAttributesForItem(at: first)?.frame else {
owsFailDebug("firstFrame was unexpectedly nil")
return nil

View File

@ -1830,7 +1830,7 @@ typedef enum : NSUInteger {
return [self indexPathForInteractionUniqueId:messageActionInteractionId] == nil;
}
- (nullable NSValue *)contentOffsetForMessageActionInteraction
- (nullable NSValue *)targetContentOffsetForMessageActionInteraction
{
OWSAssertDebug(self.messageActionsViewController);
@ -1845,6 +1845,7 @@ typedef enum : NSUInteger {
// This is expected if the menu action interaction is being deleted.
return nil;
}
[self.layout prepareLayout];
UICollectionViewLayoutAttributes *_Nullable layoutAttributes =
[self.layout layoutAttributesForItemAtIndexPath:indexPath];
if (layoutAttributes == nil) {

View File

@ -208,8 +208,9 @@ public class ConversationViewLayout: UICollectionViewLayout {
@objc
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var result = [UICollectionViewLayoutAttributes]()
owsAssertDebug(hasLayout)
var result = [UICollectionViewLayoutAttributes]()
if let headerLayoutAttributes = headerLayoutAttributes {
result.append(headerLayoutAttributes)
}
@ -222,6 +223,8 @@ public class ConversationViewLayout: UICollectionViewLayout {
@objc
public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
owsAssertDebug(hasLayout)
guard let attributes = itemAttributesMap[indexPath.row] else {
Logger.verbose("Missing attributes: \(itemAttributesMap.keys)")
return nil
@ -231,6 +234,8 @@ public class ConversationViewLayout: UICollectionViewLayout {
@objc
public override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
owsAssertDebug(hasLayout)
if elementKind == UICollectionView.elementKindSectionHeader {
return headerLayoutAttributes
} else if elementKind == UICollectionView.elementKindSectionFooter {

View File

@ -1046,7 +1046,7 @@ extension ConversationSettingsViewController: ColorPickerDelegate {
self.thread.updateConversationColorName(conversationColor.name, transaction: transaction)
}
contactsManager.avatarCache.removeAllImages()
contactsManager.removeAllFromAvatarCache()
contactsManager.clearColorNameCache()
updateTableContents()
conversationSettingsViewDelegate?.conversationColorWasUpdated()

View File

@ -26,8 +26,8 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
@property (nonatomic, readonly) SDSKeyValueStore *keyValueStore;
@property (nonnull, readonly) ImageCache *avatarCache;
// Do not access this property directly.
@property (nonnull, readonly) ImageCache *avatarCachePrivate;
@property (atomic, readonly) NSArray<Contact *> *allContacts;

View File

@ -80,7 +80,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan
_keyValueStore = [[SDSKeyValueStore alloc] initWithCollection:OWSContactsManagerCollection];
// TODO: We need to configure the limits of this cache.
_avatarCache = [ImageCache new];
_avatarCachePrivate = [ImageCache new];
_colorNameCache = [NSCache new];
_allContacts = @[];
@ -514,7 +514,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan
SignalServiceAddress *address = notification.userInfo[kNSNotificationKey_ProfileAddress];
OWSAssertDebug(address.isValid);
[self.avatarCache removeAllImagesForKey:address.stringForDisplay];
[self removeAllFromAvatarCacheWithKey:address.stringForDisplay];
}];
}
@ -553,7 +553,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan
[self.cnContactCache removeAllObjects];
[self.cnContactAvatarCache removeAllObjects];
[self.avatarCache removeAllImages];
[self removeAllFromAvatarCache];
[self intersectContacts:allContacts
isUserRequested:isUserRequested

View File

@ -13,6 +13,34 @@ public extension OWSContactsManager {
return SDSDatabaseStorage.shared
}
// MARK: - Avatar Cache
private static let unfairLock = UnfairLock()
func getImageFromAvatarCache(key: String, diameter: CGFloat) -> UIImage? {
Self.unfairLock.withLock {
self.avatarCachePrivate.image(forKey: key as NSString, diameter: diameter)
}
}
func setImageForAvatarCache(_ image: UIImage, forKey key: String, diameter: CGFloat) {
Self.unfairLock.withLock {
self.avatarCachePrivate.setImage(image, forKey: key as NSString, diameter: diameter)
}
}
func removeAllFromAvatarCacheWithKey(_ key: String) {
Self.unfairLock.withLock {
self.avatarCachePrivate.removeAllImages(forKey: key as NSString)
}
}
func removeAllFromAvatarCache() {
Self.unfairLock.withLock {
self.avatarCachePrivate.removeAllImages()
}
}
// MARK: -
func sortSignalAccountsWithSneakyTransaction(_ signalAccounts: [SignalAccount]) -> [SignalAccount] {

View File

@ -297,7 +297,9 @@ public class DisplayableText: NSObject {
switch textValue {
case .text(let text):
let truncatedText = text.substring(to: snippetLength)
let truncatedText = (text.substring(to: snippetLength)
.ows_stripped()
+ "")
truncatedContent = Content(textValue: .text(text: truncatedText),
naturalAlignment: truncatedText.naturalTextAlignment)
case .attributedText(let attributedText):

View File

@ -155,8 +155,8 @@ NS_ASSUME_NONNULL_BEGIN
OWSCAssertDebug(self.address.isLocalAddress);
NSString *noteToSelfCacheKey = [NSString stringWithFormat:@"%@:note-to-self", self.cacheKey];
UIImage *_Nullable cachedAvatar =
[OWSContactAvatarBuilder.contactsManager.avatarCache imageForKey:noteToSelfCacheKey
diameter:(CGFloat)self.diameter];
[OWSContactAvatarBuilder.contactsManager getImageFromAvatarCacheWithKey:noteToSelfCacheKey
diameter:(CGFloat)self.diameter];
if (cachedAvatar) {
return cachedAvatar;
}
@ -167,9 +167,9 @@ NS_ASSUME_NONNULL_BEGIN
return nil;
}
[OWSContactAvatarBuilder.contactsManager.avatarCache setImage:image
forKey:noteToSelfCacheKey
diameter:self.diameter];
[OWSContactAvatarBuilder.contactsManager setImageForAvatarCache:image
forKey:noteToSelfCacheKey
diameter:self.diameter];
return image;
}
@ -205,7 +205,8 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable UIImage *)buildDefaultImage
{
UIImage *_Nullable cachedAvatar =
[OWSContactAvatarBuilder.contactsManager.avatarCache imageForKey:self.cacheKey diameter:(CGFloat)self.diameter];
[OWSContactAvatarBuilder.contactsManager getImageFromAvatarCacheWithKey:self.cacheKey
diameter:(CGFloat)self.diameter];
if (cachedAvatar) {
return cachedAvatar;
}
@ -245,7 +246,7 @@ NS_ASSUME_NONNULL_BEGIN
return nil;
}
[OWSContactAvatarBuilder.contactsManager.avatarCache setImage:image forKey:self.cacheKey diameter:self.diameter];
[OWSContactAvatarBuilder.contactsManager setImageForAvatarCache:image forKey:self.cacheKey diameter:self.diameter];
return image;
}

View File

@ -67,7 +67,7 @@ NS_ASSUME_NONNULL_BEGIN
stringWithFormat:@"%@-%d-%lu", groupId.hexadecimalString, Theme.isDarkThemeEnabled, (unsigned long)diameter];
UIImage *_Nullable cachedAvatar =
[OWSGroupAvatarBuilder.contactsManager.avatarCache imageForKey:cacheKey diameter:(CGFloat)diameter];
[OWSGroupAvatarBuilder.contactsManager getImageFromAvatarCacheWithKey:cacheKey diameter:(CGFloat)diameter];
if (cachedAvatar) {
return cachedAvatar;
}
@ -85,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
return nil;
}
[OWSGroupAvatarBuilder.contactsManager.avatarCache setImage:image forKey:cacheKey diameter:diameter];
[OWSGroupAvatarBuilder.contactsManager setImageForAvatarCache:image forKey:cacheKey diameter:diameter];
return image;
}