Merge branch 'charlesmchen/chatListVsBadges'

This commit is contained in:
Nora Trapp 2021-11-16 09:48:33 -08:00
commit 20224ab668

View File

@ -4,6 +4,7 @@
import UIKit
import SignalMessaging
import SignalUI
@objc
public class HomeViewCell: UITableViewCell {
@ -58,12 +59,19 @@ public class HomeViewCell: UITableViewCell {
private struct ReuseToken {
let hasMuteIndicator: Bool
let hasMessageStatusToken: Bool
let hasUnreadBadge: Bool
}
private var reuseToken: ReuseToken?
// MARK: - Configuration
fileprivate enum UnreadMode {
case none
case unreadWithCount(count: UInt)
case unreadWithoutCount
}
// Compare with HVCellContentToken:
//
// * Configuration captures _how_ the view wants to render the cell.
@ -83,8 +91,21 @@ public class HomeViewCell: UITableViewCell {
fileprivate var hasOverrideSnippet: Bool {
overrideSnippet != nil
}
fileprivate var hasUnreadStyle: Bool {
thread.hasUnreadMessages && overrideSnippet == nil
fileprivate var unreadMode: UnreadMode {
guard !hasOverrideSnippet else {
// If we're using the conversation list cell to render search results,
// don't show "unread badge" or "message status" indicator.
return .none
}
guard thread.hasUnreadMessages else {
return .none
}
let unreadCount = thread.unreadCount
if unreadCount > 0 {
return .unreadWithCount(count: unreadCount)
} else {
return .unreadWithoutCount
}
}
init(thread: ThreadViewModel,
@ -107,7 +128,7 @@ public class HomeViewCell: UITableViewCell {
// MARK: - View Constants
private static var unreadFont: UIFont {
UIFont.ows_dynamicTypeCaption1Clamped.ows_semibold
UIFont.ows_dynamicTypeCaption1Clamped
}
private static var dateTimeFont: UIFont {
@ -184,7 +205,6 @@ public class HomeViewCell: UITableViewCell {
lastReloadDate: configuration.lastReloadDate,
isBlocked: configuration.isBlocked,
shouldShowMuteIndicator: shouldShowMuteIndicator,
hasUnreadStyle: configuration.hasUnreadStyle,
hasOverrideSnippet: configuration.hasOverrideSnippet,
messageStatusToken: messageStatusToken,
unreadIndicatorLabelConfig: unreadIndicatorLabelConfig,
@ -244,15 +264,27 @@ public class HomeViewCell: UITableViewCell {
var bottomRowStackSubviewInfos: [ManualStackSubviewInfo] = [
bottomRowWrapperSize.asManualSubviewInfo()
]
if let messageStatusToken = configs.messageStatusToken {
let statusIndicatorSize = messageStatusToken.image.size
// The status indicator should vertically align with the
// first line of the snippet.
let locationOffset = CGPoint(x: 0,
y: snippetLineHeight * -0.5)
let locationOffset = CGPoint(x: 0, y: snippetLineHeight * -0.5)
bottomRowStackSubviewInfos.append(statusIndicatorSize.asManualSubviewInfo(hasFixedSize: true,
locationOffset: locationOffset))
}
var unreadBadgeMeasurements: HVUnreadBadgeMeasurements?
if let unreadMeasurements = measureUnreadBadge(unreadIndicatorLabelConfig: configs.unreadIndicatorLabelConfig) {
unreadBadgeMeasurements = unreadMeasurements
let unreadBadgeSize = unreadMeasurements.badgeSize
// The unread indicator should vertically align with the
// first line of the snippet.
let locationOffset = CGPoint(x: 0, y: snippetLineHeight * -0.5)
bottomRowStackSubviewInfos.append(unreadBadgeSize.asManualSubviewInfo(hasFixedSize: true,
locationOffset: locationOffset))
}
let bottomRowStackMeasurement = ManualStackView.measure(config: bottomRowStackConfig,
subviewInfos: bottomRowStackSubviewInfos)
let bottomRowStackSize = bottomRowStackMeasurement.measuredSize
@ -275,7 +307,8 @@ public class HomeViewCell: UITableViewCell {
bottomRowStackMeasurement: bottomRowStackMeasurement,
vStackMeasurement: vStackMeasurement,
outerHStackMeasurement: outerHStackMeasurement,
snippetLineHeight: snippetLineHeight)
snippetLineHeight: snippetLineHeight,
unreadBadgeMeasurements: unreadBadgeMeasurements)
}
func configure(cellContentToken: HVCellContentToken) {
@ -329,49 +362,6 @@ public class HomeViewCell: UITableViewCell {
name: TypingIndicatorsImpl.typingIndicatorStateDidChange,
object: nil)
// Avatar
let avatarSize: CGSize = .square(CGFloat(HomeViewCell.avatarSize))
// Unread Indicator
// If there are unread messages, show the "unread badge."
if let unreadIndicatorLabelConfig = configs.unreadIndicatorLabelConfig {
let unreadLabel = self.unreadLabel
unreadIndicatorLabelConfig.applyForRendering(label: unreadLabel)
unreadLabel.removeFromSuperview()
let unreadLabelSize = unreadLabel.sizeThatFits(.square(.greatestFiniteMagnitude))
let unreadBadge = self.unreadBadge
unreadBadge.backgroundColor = .ows_accentBlue
unreadBadge.addSubview(unreadLabel) { view in
// Center within badge.
unreadLabel.frame = CGRect(origin: (view.frame.size - unreadLabelSize).asPoint * 0.5,
size: unreadLabelSize)
}
let unreadBadgeHeight = ceil(unreadLabel.font.lineHeight * 1.5)
unreadBadge.layer.cornerRadius = unreadBadgeHeight / 2
unreadBadge.layer.borderColor = Theme.backgroundColor.cgColor
unreadBadge.layer.borderWidth = 2
// This is a bit arbitrary, but it should scale with the size of dynamic text
let minMargin = CeilEven(unreadBadgeHeight * 0.5)
// Pill should be at least circular; can be wider.
let unreadBadgeSize = CGSize(width: max(unreadBadgeHeight,
unreadLabelSize.width + minMargin),
height: unreadBadgeHeight)
avatarStack.addSubview(unreadBadge) { view in
unreadBadge.frame = CGRect(origin: CGPoint(x: view.width - unreadBadgeSize.width + 6,
y: (view.height - avatarSize.height) * 0.5),
size: unreadBadgeSize)
}
// The unread badge should appear on top of the avatar, but it is added
// to the view hierarchy first (in the "configure" below where we leverage
// existing measurement/arrangements). We solve that by nudging the zPosition
// of the unread badge.
unreadBadge.layer.zPosition = +1
}
// The top row contains:
//
// * Name label
@ -430,21 +420,31 @@ public class HomeViewCell: UITableViewCell {
width: typingIndicatorSize.width,
height: typingIndicatorSize.height)
}
updateTypingIndicatorState()
var bottomRowStackSubviews: [UIView] = [ bottomRowWrapper ]
if let messageStatusToken = cellContentToken.configs.messageStatusToken {
if let messageStatusToken = configs.messageStatusToken {
let statusIndicator = configureStatusIndicatorView(token: messageStatusToken)
bottomRowStackSubviews.append(statusIndicator)
}
updateTypingIndicatorState()
// If there are unread messages, show the "unread badge."
if let unreadIndicatorLabelConfig = configs.unreadIndicatorLabelConfig,
let unreadBadgeMeasurements = measurements.unreadBadgeMeasurements {
let unreadBadge = configureUnreadBadge(unreadIndicatorLabelConfig: unreadIndicatorLabelConfig,
unreadBadgeMeasurements: unreadBadgeMeasurements)
bottomRowStackSubviews.append(unreadBadge)
}
let avatarStackSubviews = [ avatarView ]
let vStackSubviews = [ topRowStack, bottomRowStack ]
let outerHStackSubviews = [ avatarStack, vStack ]
// It is only safe to reuse the bottom row wrapper if its subview list
// hasn't changed.
let newReuseToken = ReuseToken(hasMuteIndicator: shouldShowMuteIndicator,
hasMessageStatusToken: cellContentToken.configs.messageStatusToken != nil)
hasMessageStatusToken: configs.messageStatusToken != nil,
hasUnreadBadge: measurements.unreadBadgeMeasurements != nil)
avatarStack.configure(config: avatarStackConfig,
measurement: avatarStackMeasurement,
@ -463,10 +463,11 @@ public class HomeViewCell: UITableViewCell {
subviews: topRowStackSubviews)
}
// bottomRowStack can only be configured for reuse if
// its subview list hasn't changed.
// It is only safe to reuse bottomRowStack if the same subset of subviews
// are in use.
if let oldReuseToken = self.reuseToken,
oldReuseToken.hasMessageStatusToken == newReuseToken.hasMessageStatusToken {
oldReuseToken.hasMessageStatusToken == newReuseToken.hasMessageStatusToken,
oldReuseToken.hasUnreadBadge == newReuseToken.hasUnreadBadge {
bottomRowStack.configureForReuse(config: bottomRowStackConfig,
measurement: bottomRowStackMeasurement)
} else {
@ -614,18 +615,20 @@ public class HomeViewCell: UITableViewCell {
// MARK: - Unread Indicator
private static func buildUnreadIndicatorLabelConfig(configuration: Configuration) -> CVLabelConfig? {
guard !configuration.hasOverrideSnippet else {
let text: String
switch configuration.unreadMode {
case .none:
// If we're using the conversation list cell to render search results,
// don't show "unread badge" or "message status" indicator.
//
// Or there might simply be no unread messages / the thread is not
// marked as unread.
return nil
case .unreadWithoutCount:
text = ""
case .unreadWithCount(let unreadCount):
text = unreadCount > 0 ? OWSFormat.formatUInt(unreadCount) : ""
}
guard configuration.hasUnreadStyle else {
return nil
}
let thread = configuration.thread
let unreadCount = thread.unreadCount
let text = unreadCount > 0 ? OWSFormat.formatUInt(unreadCount) : ""
return CVLabelConfig(text: text,
font: unreadFont,
textColor: .ows_white,
@ -634,6 +637,48 @@ public class HomeViewCell: UITableViewCell {
textAlignment: .center)
}
private static func measureUnreadBadge(unreadIndicatorLabelConfig: CVLabelConfig?) -> HVUnreadBadgeMeasurements? {
guard let unreadIndicatorLabelConfig = unreadIndicatorLabelConfig else {
return nil
}
let unreadLabelSize = CVText.measureLabel(config: unreadIndicatorLabelConfig,
maxWidth: .greatestFiniteMagnitude)
let unreadBadgeHeight = ceil(unreadIndicatorLabelConfig.font.lineHeight * 1.35)
// This is a bit arbitrary, but it should scale with the size of dynamic text
let minMargin = CeilEven(unreadBadgeHeight * 0.5)
// Pill should be at least circular; can be wider.
let badgeSize = CGSize(width: max(unreadBadgeHeight,
unreadLabelSize.width + minMargin),
height: unreadBadgeHeight)
return HVUnreadBadgeMeasurements(badgeSize: badgeSize,
unreadLabelSize: unreadLabelSize)
}
private func configureUnreadBadge(unreadIndicatorLabelConfig: CVLabelConfig,
unreadBadgeMeasurements measurements: HVUnreadBadgeMeasurements) -> UIView {
let unreadLabel = self.unreadLabel
unreadIndicatorLabelConfig.applyForRendering(label: unreadLabel)
unreadLabel.removeFromSuperview()
let unreadLabelSize = measurements.unreadLabelSize
let unreadBadge = self.unreadBadge
unreadBadge.backgroundColor = .ows_accentBlue
unreadBadge.addSubview(unreadLabel) { view in
// Center within badge.
unreadLabel.frame = CGRect(origin: (view.frame.size - unreadLabelSize).asPoint * 0.5,
size: unreadLabelSize)
}
let unreadBadgeHeight = measurements.badgeSize.height
unreadBadge.layer.cornerRadius = unreadBadgeHeight / 2
return unreadBadge
}
// MARK: - Label Configs
private static func attributedSnippet(configuration: Configuration) -> NSAttributedString {
@ -750,19 +795,13 @@ public class HomeViewCell: UITableViewCell {
let thread = configuration.thread
var text: String = ""
if let labelDate = configuration.overrideDate ?? thread.homeViewInfo?.lastMessageDate {
text = DateUtil.formatDateShort(labelDate)
}
if configuration.hasUnreadStyle {
return CVLabelConfig(text: text,
font: dateTimeFont.ows_semibold,
textColor: Theme.primaryTextColor,
textAlignment: .trailing)
} else {
return CVLabelConfig(text: text,
font: dateTimeFont,
textColor: snippetColor,
textAlignment: .trailing)
text = DateUtil.formatMessageTimestampForCVC(labelDate.ows_millisecondsSince1970,
shouldUseLongFormat: false)
}
return CVLabelConfig(text: text,
font: dateTimeFont,
textColor: snippetColor,
textAlignment: .trailing)
}
private static func nameLabelConfig(configuration: Configuration) -> CVLabelConfig {
@ -914,7 +953,6 @@ private struct HVCellConfigs {
let lastReloadDate: Date?
let isBlocked: Bool
let shouldShowMuteIndicator: Bool
let hasUnreadStyle: Bool
let hasOverrideSnippet: Bool
let messageStatusToken: HVMessageStatusToken?
let unreadIndicatorLabelConfig: CVLabelConfig?
@ -932,6 +970,13 @@ private struct HVCellConfigs {
// MARK: -
private struct HVUnreadBadgeMeasurements {
let badgeSize: CGSize
let unreadLabelSize: CGSize
}
// MARK: -
private struct HVCellMeasurements {
let avatarStackMeasurement: ManualStackView.Measurement
let topRowStackMeasurement: ManualStackView.Measurement
@ -939,6 +984,7 @@ private struct HVCellMeasurements {
let vStackMeasurement: ManualStackView.Measurement
let outerHStackMeasurement: ManualStackView.Measurement
let snippetLineHeight: CGFloat
let unreadBadgeMeasurements: HVUnreadBadgeMeasurements?
}
// MARK: -
@ -978,7 +1024,6 @@ class HVCellContentToken {
fileprivate var thread: TSThread { configs.thread }
fileprivate var isBlocked: Bool { configs.isBlocked }
fileprivate var shouldShowMuteIndicator: Bool { configs.shouldShowMuteIndicator }
fileprivate var hasUnreadStyle: Bool { configs.hasUnreadStyle }
fileprivate var hasOverrideSnippet: Bool { configs.hasOverrideSnippet }
fileprivate var shouldLoadAvatarAsync: Bool {