Fixed a few message scrolling bugs and a couple minor UI tweaks

Fixed a bug where sending a quote message could result in the messages jumping incorrectly
Fixed a bug where the quote input UI wasn't styled correctly on dark themes
Fixed a minor padding issue
Fixed an issue where in-conversation search could be a bit jumpy (simplified logic greatly)
This commit is contained in:
Morgan Pretty 2022-10-10 11:48:48 +11:00
parent 0af00feed9
commit 2ad8fc381d
6 changed files with 47 additions and 70 deletions

View File

@ -1559,7 +1559,9 @@ extension ConversationVC:
body: cellViewModel.body, body: cellViewModel.body,
timestampMs: cellViewModel.timestampMs, timestampMs: cellViewModel.timestampMs,
attachments: cellViewModel.attachments, attachments: cellViewModel.attachments,
linkPreviewAttachment: cellViewModel.linkPreviewAttachment linkPreviewAttachment: cellViewModel.linkPreviewAttachment,
currentUserPublicKey: cellViewModel.currentUserPublicKey,
currentUserBlindedPublicKey: cellViewModel.currentUserBlindedPublicKey
) )
guard let quoteDraft: QuotedReplyModel = maybeQuoteDraft else { return } guard let quoteDraft: QuotedReplyModel = maybeQuoteDraft else { return }

View File

@ -808,7 +808,8 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
/// Unfortunately the UITableView also does some weird things when updating (where it won't have updated it's internal data until /// Unfortunately the UITableView also does some weird things when updating (where it won't have updated it's internal data until
/// after it performs the next layout); the below code checks a condition on layout and if it passes it calls a closure /// after it performs the next layout); the below code checks a condition on layout and if it passes it calls a closure
if let itemChangeInfo: ItemChangeInfo = itemChangeInfo, itemChangeInfo.isInsertAtTop { if let itemChangeInfo: ItemChangeInfo = itemChangeInfo, itemChangeInfo.isInsertAtTop {
let oldCellHeight: CGFloat = self.tableView.rectForRow(at: itemChangeInfo.oldVisibleIndexPath).height let oldCellRect: CGRect = self.tableView.rectForRow(at: itemChangeInfo.oldVisibleIndexPath)
let oldCellTopOffset: CGFloat = (self.tableView.frame.minY - self.tableView.convert(oldCellRect, to: self.tableView.superview).minY)
// The the user triggered the 'scrollToTop' animation (by tapping in the nav bar) then we // The the user triggered the 'scrollToTop' animation (by tapping in the nav bar) then we
// need to stop the animation before attempting to lock the offset (otherwise things break) // need to stop the animation before attempting to lock the offset (otherwise things break)
@ -828,27 +829,13 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
// loaded was smaller than 2 pages (this will prevent calculating the frames of // loaded was smaller than 2 pages (this will prevent calculating the frames of
// a large number of cells when getting search results which are very far away // a large number of cells when getting search results which are very far away
// only to instantly start scrolling making the calculation redundant) // only to instantly start scrolling making the calculation redundant)
if (abs(itemChangeInfo.visibleIndexPath.row - itemChangeInfo.oldVisibleIndexPath.row) <= (ConversationViewModel.pageSize * 2)) { UIView.performWithoutAnimation {
UIView.performWithoutAnimation { self?.tableView.scrollToRow(
let calculatedRowHeights: CGFloat = (0..<itemChangeInfo.visibleIndexPath.row) at: itemChangeInfo.visibleIndexPath,
.reduce(into: 0) { result, next in at: .top,
result += (self?.tableView animated: false
.rectForRow( )
at: IndexPath( self?.tableView.contentOffset.y += oldCellTopOffset
row: next,
section: itemChangeInfo.visibleIndexPath.section
)
)
.height)
.defaulting(to: 0)
}
let newTargetHeight: CGFloat? = self?.tableView
.rectForRow(at: itemChangeInfo.visibleIndexPath)
.height
let heightDiff: CGFloat = (oldCellHeight - (newTargetHeight ?? oldCellHeight))
self?.tableView.contentOffset.y += (calculatedRowHeights - heightDiff)
}
} }
if let focusedInteractionId: Int64 = self?.focusedInteractionId { if let focusedInteractionId: Int64 = self?.focusedInteractionId {
@ -1327,6 +1314,15 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
return return
} }
// Note: In this case we need to force a tableView layout to ensure updating the
// scroll position has the correct offset (otherwise there are some cases where
// the screen will jump up - eg. when sending a reply while the soft keyboard
// is visible)
UIView.performWithoutAnimation {
self.tableView.setNeedsLayout()
self.tableView.layoutIfNeeded()
}
let targetIndexPath: IndexPath = IndexPath( let targetIndexPath: IndexPath = IndexPath(
row: (self.viewModel.interactionData[messagesSectionIndex].elements.count - 1), row: (self.viewModel.interactionData[messagesSectionIndex].elements.count - 1),
section: messagesSectionIndex section: messagesSectionIndex
@ -1337,7 +1333,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
animated: isAnimated animated: isAnimated
) )
self.handleInitialOffsetBounceBug(targetIndexPath: targetIndexPath, at: .bottom)
self.viewModel.markAsRead(beforeInclusive: nil) self.viewModel.markAsRead(beforeInclusive: nil)
} }
@ -1577,7 +1572,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
at: position, at: position,
animated: (self.didFinishInitialLayout && isAnimated) animated: (self.didFinishInitialLayout && isAnimated)
) )
self.handleInitialOffsetBounceBug(targetIndexPath: targetIndexPath, at: position)
// If we haven't finished the initial layout then we want to delay the highlight slightly // If we haven't finished the initial layout then we want to delay the highlight slightly
// so it doesn't look buggy with the push transition // so it doesn't look buggy with the push transition
@ -1604,7 +1598,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
} }
self.tableView.scrollToRow(at: targetIndexPath, at: position, animated: true) self.tableView.scrollToRow(at: targetIndexPath, at: position, animated: true)
self.handleInitialOffsetBounceBug(targetIndexPath: targetIndexPath, at: position)
} }
func highlightCellIfNeeded(interactionId: Int64) { func highlightCellIfNeeded(interactionId: Int64) {
@ -1620,37 +1613,4 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
.highlight() .highlight()
} }
} }
private func handleInitialOffsetBounceBug(targetIndexPath: IndexPath, at position: UITableView.ScrollPosition) {
/// Note: This code is a hack to prevent a weird 'bounce' behaviour that occurs when triggering the initial scroll due
/// to the UITableView properly calculating it's cell sizes (it seems to layout ~3 times each with slightly different sizes)
if !self.hasPerformedInitialScroll {
let initialUpdateTime: CFTimeInterval = CACurrentMediaTime()
var lastSize: CGSize = .zero
self.tableView.afterNextLayoutSubviews(
when: { [weak self] numSections, numRowInSections, updatedContentSize in
// If too much time has passed or the section/row count doesn't match then
// just stop the callback
guard
(CACurrentMediaTime() - initialUpdateTime) < 2 &&
lastSize != updatedContentSize &&
numSections > targetIndexPath.section &&
numRowInSections[targetIndexPath.section] > targetIndexPath.row
else { return true }
lastSize = updatedContentSize
self?.tableView.scrollToRow(
at: targetIndexPath,
at: position,
animated: false
)
return false
},
then: {}
)
}
}
} }

View File

@ -245,8 +245,8 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
authorId: quoteDraftInfo.model.authorId, authorId: quoteDraftInfo.model.authorId,
quotedText: quoteDraftInfo.model.body, quotedText: quoteDraftInfo.model.body,
threadVariant: threadVariant, threadVariant: threadVariant,
currentUserPublicKey: nil, currentUserPublicKey: quoteDraftInfo.model.currentUserPublicKey,
currentUserBlindedPublicKey: nil, currentUserBlindedPublicKey: quoteDraftInfo.model.currentUserBlindedPublicKey,
direction: (quoteDraftInfo.isOutgoing ? .outgoing : .incoming), direction: (quoteDraftInfo.isOutgoing ? .outgoing : .incoming),
attachment: quoteDraftInfo.model.attachment, attachment: quoteDraftInfo.model.attachment,
hInset: hInset, hInset: hInset,

View File

@ -112,7 +112,7 @@ final class LinkPreviewView: UIView {
// Title label // Title label
let titleLabelContainer = UIView() let titleLabelContainer = UIView()
titleLabelContainer.addSubview(titleLabel) titleLabelContainer.addSubview(titleLabel)
titleLabel.pin(to: titleLabelContainer, withInset: Values.smallSpacing) titleLabel.pin(to: titleLabelContainer, withInset: Values.mediumSpacing)
// Horizontal stack view // Horizontal stack view
hStackView.addArrangedSubview(imageViewContainer) hStackView.addArrangedSubview(imageViewContainer)

View File

@ -187,14 +187,19 @@ final class QuoteView: UIView {
} }
// Body label // Body label
let bodyLabel = UILabel() let bodyLabel = TappableLabel()
bodyLabel.numberOfLines = 0 bodyLabel.numberOfLines = 0
bodyLabel.lineBreakMode = .byTruncatingTail bodyLabel.lineBreakMode = .byTruncatingTail
let targetThemeColor: ThemeValue = (direction == .outgoing ? let targetThemeColor: ThemeValue = {
.messageBubble_outgoingText : switch mode {
.messageBubble_incomingText case .regular: return (direction == .outgoing ?
) .messageBubble_outgoingText :
.messageBubble_incomingText
)
case .draft: return .textPrimary
}
}()
bodyLabel.font = .systemFont(ofSize: Values.smallFontSize) bodyLabel.font = .systemFont(ofSize: Values.smallFontSize)
ThemeManager.onThemeChange(observer: bodyLabel) { [weak bodyLabel] theme, primaryColor in ThemeManager.onThemeChange(observer: bodyLabel) { [weak bodyLabel] theme, primaryColor in

View File

@ -12,6 +12,8 @@ public struct QuotedReplyModel {
public let contentType: String? public let contentType: String?
public let sourceFileName: String? public let sourceFileName: String?
public let thumbnailDownloadFailed: Bool public let thumbnailDownloadFailed: Bool
public let currentUserPublicKey: String?
public let currentUserBlindedPublicKey: String?
// MARK: - Initialization // MARK: - Initialization
@ -23,7 +25,9 @@ public struct QuotedReplyModel {
attachment: Attachment?, attachment: Attachment?,
contentType: String?, contentType: String?,
sourceFileName: String?, sourceFileName: String?,
thumbnailDownloadFailed: Bool thumbnailDownloadFailed: Bool,
currentUserPublicKey: String?,
currentUserBlindedPublicKey: String?
) { ) {
self.attachment = attachment self.attachment = attachment
self.threadId = threadId self.threadId = threadId
@ -33,6 +37,8 @@ public struct QuotedReplyModel {
self.contentType = contentType self.contentType = contentType
self.sourceFileName = sourceFileName self.sourceFileName = sourceFileName
self.thumbnailDownloadFailed = thumbnailDownloadFailed self.thumbnailDownloadFailed = thumbnailDownloadFailed
self.currentUserPublicKey = currentUserPublicKey
self.currentUserBlindedPublicKey = currentUserBlindedPublicKey
} }
public static func quotedReplyForSending( public static func quotedReplyForSending(
@ -42,7 +48,9 @@ public struct QuotedReplyModel {
body: String?, body: String?,
timestampMs: Int64, timestampMs: Int64,
attachments: [Attachment]?, attachments: [Attachment]?,
linkPreviewAttachment: Attachment? linkPreviewAttachment: Attachment?,
currentUserPublicKey: String?,
currentUserBlindedPublicKey: String?
) -> QuotedReplyModel? { ) -> QuotedReplyModel? {
guard variant == .standardOutgoing || variant == .standardIncoming else { return nil } guard variant == .standardOutgoing || variant == .standardIncoming else { return nil }
guard (body != nil && body?.isEmpty == false) || attachments?.isEmpty == false else { return nil } guard (body != nil && body?.isEmpty == false) || attachments?.isEmpty == false else { return nil }
@ -57,7 +65,9 @@ public struct QuotedReplyModel {
attachment: targetAttachment, attachment: targetAttachment,
contentType: targetAttachment?.contentType, contentType: targetAttachment?.contentType,
sourceFileName: targetAttachment?.sourceFilename, sourceFileName: targetAttachment?.sourceFilename,
thumbnailDownloadFailed: false thumbnailDownloadFailed: false,
currentUserPublicKey: currentUserPublicKey,
currentUserBlindedPublicKey: currentUserBlindedPublicKey
) )
} }
} }