From 443f5a3921daed7e1e3a895e8061b5f67ee9c1bb Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 12 Nov 2021 10:51:38 -0300 Subject: [PATCH 1/3] Chat list changes for badges. --- .../HomeView/HomeViewCell.swift | 191 ++++++++++++------ 1 file changed, 129 insertions(+), 62 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.swift b/Signal/src/ViewControllers/HomeView/HomeViewCell.swift index d1ed7a4b08..b5da2543f8 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.swift +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.swift @@ -4,6 +4,7 @@ import UIKit import SignalMessaging +import SignalUI @objc public class HomeViewCell: UITableViewCell { @@ -58,12 +59,28 @@ 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 + + var hasUnreadStyle: Bool { + switch self { + case .none: + return false + case .unreadWithoutCount, .unreadWithCount: + return true + } + } + } + // Compare with HVCellContentToken: // // * Configuration captures _how_ the view wants to render the cell. @@ -84,7 +101,23 @@ public class HomeViewCell: UITableViewCell { overrideSnippet != nil } fileprivate var hasUnreadStyle: Bool { - thread.hasUnreadMessages && overrideSnippet == nil + unreadMode.hasUnreadStyle + } + 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 +140,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 { @@ -244,15 +277,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 +320,8 @@ public class HomeViewCell: UITableViewCell { bottomRowStackMeasurement: bottomRowStackMeasurement, vStackMeasurement: vStackMeasurement, outerHStackMeasurement: outerHStackMeasurement, - snippetLineHeight: snippetLineHeight) + snippetLineHeight: snippetLineHeight, + unreadBadgeMeasurements: unreadBadgeMeasurements) } func configure(cellContentToken: HVCellContentToken) { @@ -329,49 +375,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,6 +433,7 @@ public class HomeViewCell: UITableViewCell { width: typingIndicatorSize.width, height: typingIndicatorSize.height) } + updateTypingIndicatorState() var bottomRowStackSubviews: [UIView] = [ bottomRowWrapper ] if let messageStatusToken = cellContentToken.configs.messageStatusToken { @@ -437,14 +441,21 @@ public class HomeViewCell: UITableViewCell { bottomRowStackSubviews.append(statusIndicator) } - updateTypingIndicatorState() + // If there are unread messages, show the "unread badge." + if let unreadBadgeMeasurements = measurements.unreadBadgeMeasurements { + let unreadBadge = configureUnreadBadge(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 +474,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 +626,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 +648,49 @@ 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(unreadIndicatorLabelConfig: unreadIndicatorLabelConfig, + badgeSize: badgeSize, + unreadLabelSize: unreadLabelSize) + } + + private func configureUnreadBadge(unreadBadgeMeasurements measurements: HVUnreadBadgeMeasurements) -> UIView { + let unreadIndicatorLabelConfig = measurements.unreadIndicatorLabelConfig + + 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,7 +807,8 @@ public class HomeViewCell: UITableViewCell { let thread = configuration.thread var text: String = "" if let labelDate = configuration.overrideDate ?? thread.homeViewInfo?.lastMessageDate { - text = DateUtil.formatDateShort(labelDate) + text = DateUtil.formatMessageTimestampForCVC(labelDate.ows_millisecondsSince1970, + shouldUseLongFormat: false) } if configuration.hasUnreadStyle { return CVLabelConfig(text: text, @@ -932,6 +990,14 @@ private struct HVCellConfigs { // MARK: - +private struct HVUnreadBadgeMeasurements { + let unreadIndicatorLabelConfig: CVLabelConfig + let badgeSize: CGSize + let unreadLabelSize: CGSize +} + +// MARK: - + private struct HVCellMeasurements { let avatarStackMeasurement: ManualStackView.Measurement let topRowStackMeasurement: ManualStackView.Measurement @@ -939,6 +1005,7 @@ private struct HVCellMeasurements { let vStackMeasurement: ManualStackView.Measurement let outerHStackMeasurement: ManualStackView.Measurement let snippetLineHeight: CGFloat + let unreadBadgeMeasurements: HVUnreadBadgeMeasurements? } // MARK: - From 9384bc82002d243b300be7c917363ae9d50c7b5a Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 12 Nov 2021 10:56:21 -0300 Subject: [PATCH 2/3] Chat list changes for badges. --- .../ViewControllers/HomeView/HomeViewCell.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.swift b/Signal/src/ViewControllers/HomeView/HomeViewCell.swift index b5da2543f8..191fcbfadd 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.swift +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.swift @@ -436,14 +436,16 @@ public class HomeViewCell: UITableViewCell { updateTypingIndicatorState() var bottomRowStackSubviews: [UIView] = [ bottomRowWrapper ] - if let messageStatusToken = cellContentToken.configs.messageStatusToken { + if let messageStatusToken = configs.messageStatusToken { let statusIndicator = configureStatusIndicatorView(token: messageStatusToken) bottomRowStackSubviews.append(statusIndicator) } // If there are unread messages, show the "unread badge." - if let unreadBadgeMeasurements = measurements.unreadBadgeMeasurements { - let unreadBadge = configureUnreadBadge(unreadBadgeMeasurements: unreadBadgeMeasurements) + if let unreadIndicatorLabelConfig = configs.unreadIndicatorLabelConfig, + let unreadBadgeMeasurements = measurements.unreadBadgeMeasurements { + let unreadBadge = configureUnreadBadge(unreadIndicatorLabelConfig: unreadIndicatorLabelConfig, + unreadBadgeMeasurements: unreadBadgeMeasurements) bottomRowStackSubviews.append(unreadBadge) } @@ -665,13 +667,12 @@ public class HomeViewCell: UITableViewCell { unreadLabelSize.width + minMargin), height: unreadBadgeHeight) - return HVUnreadBadgeMeasurements(unreadIndicatorLabelConfig: unreadIndicatorLabelConfig, - badgeSize: badgeSize, + return HVUnreadBadgeMeasurements(badgeSize: badgeSize, unreadLabelSize: unreadLabelSize) } - private func configureUnreadBadge(unreadBadgeMeasurements measurements: HVUnreadBadgeMeasurements) -> UIView { - let unreadIndicatorLabelConfig = measurements.unreadIndicatorLabelConfig + private func configureUnreadBadge(unreadIndicatorLabelConfig: CVLabelConfig, + unreadBadgeMeasurements measurements: HVUnreadBadgeMeasurements) -> UIView { let unreadLabel = self.unreadLabel unreadIndicatorLabelConfig.applyForRendering(label: unreadLabel) @@ -991,7 +992,6 @@ private struct HVCellConfigs { // MARK: - private struct HVUnreadBadgeMeasurements { - let unreadIndicatorLabelConfig: CVLabelConfig let badgeSize: CGSize let unreadLabelSize: CGSize } From 6d692e264aa35171a1d5b96df98b0b764f70024c Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 12 Nov 2021 13:45:08 -0300 Subject: [PATCH 3/3] Do not bold timestamp for unread state. --- .../HomeView/HomeViewCell.swift | 30 +++---------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.swift b/Signal/src/ViewControllers/HomeView/HomeViewCell.swift index 191fcbfadd..a558dcea11 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.swift +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.swift @@ -70,15 +70,6 @@ public class HomeViewCell: UITableViewCell { case none case unreadWithCount(count: UInt) case unreadWithoutCount - - var hasUnreadStyle: Bool { - switch self { - case .none: - return false - case .unreadWithoutCount, .unreadWithCount: - return true - } - } } // Compare with HVCellContentToken: @@ -100,9 +91,6 @@ public class HomeViewCell: UITableViewCell { fileprivate var hasOverrideSnippet: Bool { overrideSnippet != nil } - fileprivate var hasUnreadStyle: Bool { - unreadMode.hasUnreadStyle - } fileprivate var unreadMode: UnreadMode { guard !hasOverrideSnippet else { // If we're using the conversation list cell to render search results, @@ -217,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, @@ -811,17 +798,10 @@ public class HomeViewCell: UITableViewCell { text = DateUtil.formatMessageTimestampForCVC(labelDate.ows_millisecondsSince1970, shouldUseLongFormat: false) } - 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) - } + return CVLabelConfig(text: text, + font: dateTimeFont, + textColor: snippetColor, + textAlignment: .trailing) } private static func nameLabelConfig(configuration: Configuration) -> CVLabelConfig { @@ -973,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? @@ -1045,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 {