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:
parent
0af00feed9
commit
2ad8fc381d
|
@ -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 }
|
||||||
|
|
|
@ -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: {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue