diff --git a/Bark.xcodeproj/project.pbxproj b/Bark.xcodeproj/project.pbxproj index bf2e264..3f2f3de 100644 --- a/Bark.xcodeproj/project.pbxproj +++ b/Bark.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 06172FDC27F6DB06002333A4 /* ServerListTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06172FDB27F6DB06002333A4 /* ServerListTableViewCellViewModel.swift */; }; 061894C529962EB900E001C2 /* GradientButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061894C429962EB900E001C2 /* GradientButton.swift */; }; 061894C729A75BEA00E001C2 /* Algorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061894C629A75BEA00E001C2 /* Algorithm.swift */; }; + 061C17082D1BDA4B00891D66 /* MessageGroupMoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C17072D1BDA4B00891D66 /* MessageGroupMoreView.swift */; }; 0627DABB298B6EA2002F3F69 /* DropBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0627DABA298B6EA2002F3F69 /* DropBoxView.swift */; }; 0627DABD2990D615002F3F69 /* BorderTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0627DABC2990D615002F3F69 /* BorderTextField.swift */; }; 062B98C3251B2762004562E7 /* BKButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062B98C2251B2762004562E7 /* BKButton.swift */; }; @@ -261,6 +262,7 @@ 06172FDB27F6DB06002333A4 /* ServerListTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableViewCellViewModel.swift; sourceTree = ""; }; 061894C429962EB900E001C2 /* GradientButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButton.swift; sourceTree = ""; }; 061894C629A75BEA00E001C2 /* Algorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Algorithm.swift; sourceTree = ""; }; + 061C17072D1BDA4B00891D66 /* MessageGroupMoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageGroupMoreView.swift; sourceTree = ""; }; 0627DABA298B6EA2002F3F69 /* DropBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropBoxView.swift; sourceTree = ""; }; 0627DABC2990D615002F3F69 /* BorderTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderTextField.swift; sourceTree = ""; }; 062B98C2251B2762004562E7 /* BKButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BKButton.swift; sourceTree = ""; }; @@ -680,6 +682,7 @@ 067B2EB425693E38008B6BE1 /* MessageTableViewCellViewModel.swift */, 0668900A2D19525400E106F2 /* ShowLessAndClearView.swift */, 0668900C2D19582400E106F2 /* MessageGroupHeaderView.swift */, + 061C17072D1BDA4B00891D66 /* MessageGroupMoreView.swift */, ); path = MessageList; sourceTree = ""; @@ -1287,6 +1290,7 @@ 0637FA7C20E0930E00E80174 /* BarkApi.swift in Sources */, 06C2CF252685BDB80034B127 /* SpacerCell.swift in Sources */, 06BBB8BC2567B3AD0076F63E /* ArchiveSettingCellViewModel.swift in Sources */, + 061C17082D1BDA4B00891D66 /* MessageGroupMoreView.swift in Sources */, 0642B55A27EB13F100453D91 /* MutableTextCell.swift in Sources */, 1EFB545F2C32514000B8E51B /* SectionViewModel-iPad.swift in Sources */, 06EEF335291CD00000CA228A /* CryptoSettingViewModel.swift in Sources */, diff --git a/Bark/Assets.xcassets/keyboard_arrow_right_symbol.symbolset/Contents.json b/Bark/Assets.xcassets/keyboard_arrow_right_symbol.symbolset/Contents.json new file mode 100644 index 0000000..ffaba90 --- /dev/null +++ b/Bark/Assets.xcassets/keyboard_arrow_right_symbol.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "keyboard_arrow_right_keyboard_arrow_right_symbol.svg", + "idiom" : "universal" + } + ] +} diff --git a/Bark/Assets.xcassets/keyboard_arrow_right_symbol.symbolset/keyboard_arrow_right_keyboard_arrow_right_symbol.svg b/Bark/Assets.xcassets/keyboard_arrow_right_symbol.symbolset/keyboard_arrow_right_keyboard_arrow_right_symbol.svg new file mode 100644 index 0000000..07b26b6 --- /dev/null +++ b/Bark/Assets.xcassets/keyboard_arrow_right_symbol.symbolset/keyboard_arrow_right_keyboard_arrow_right_symbol.svg @@ -0,0 +1,12 @@ +Weight/Scale VariationsUltralightThinLightRegularMediumSemiboldBoldHeavyBlackTemplate v.1.0SmallMediumLarge \ No newline at end of file diff --git a/Bark/Localizable.xcstrings b/Bark/Localizable.xcstrings index d9cf2c9..e78838a 100644 --- a/Bark/Localizable.xcstrings +++ b/Bark/Localizable.xcstrings @@ -3150,6 +3150,29 @@ } } } + }, + "viewMoreMessages" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View %d More Messages" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%d Daha Mesaj Görüntüle" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查看更多%d条消息" + } + } + } } }, "version" : "1.0" diff --git a/Common/String+Extension.swift b/Common/String+Extension.swift index bf96566..f7687c8 100644 --- a/Common/String+Extension.swift +++ b/Common/String+Extension.swift @@ -21,3 +21,35 @@ extension String { return self.removingPercentEncoding ?? "" } } + +// MARK: - NSAttributedString + +extension String { + var bold: NSAttributedString { + return NSMutableAttributedString(string: self, attributes: [.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)]) + } + + var underline: NSAttributedString { + return NSAttributedString(string: self, attributes: [.underlineStyle: NSUnderlineStyle.single.rawValue]) + } + + var strikethrough: NSAttributedString { + return NSAttributedString(string: self, attributes: [.strikethroughStyle: NSNumber(value: NSUnderlineStyle.single.rawValue as Int)]) + } + + var italic: NSAttributedString { + return NSMutableAttributedString(string: self, attributes: [.font: UIFont.italicSystemFont(ofSize: UIFont.systemFontSize)]) + } + + func colored(with color: UIColor) -> NSAttributedString { + return NSMutableAttributedString(string: self, attributes: [.foregroundColor: color]) + } +} + +// MARK: - Format + +extension String { + func format(_ arguments: any CVarArg...) -> String { + return String(format: self, arguments) + } +} diff --git a/View/MessageList/MessageGroupHeaderView.swift b/View/MessageList/MessageGroupHeaderView.swift index f905398..9c53efa 100644 --- a/View/MessageList/MessageGroupHeaderView.swift +++ b/View/MessageList/MessageGroupHeaderView.swift @@ -50,7 +50,7 @@ class MessageGroupHeaderView: UIView { make.bottom.equalTo(-10) } showLessAndClearView.snp.makeConstraints { make in - make.right.equalTo(-8) + make.right.equalToSuperview() make.centerY.equalTo(groupNameLabel) } } diff --git a/View/MessageList/MessageGroupMoreView.swift b/View/MessageList/MessageGroupMoreView.swift new file mode 100644 index 0000000..65169b8 --- /dev/null +++ b/View/MessageList/MessageGroupMoreView.swift @@ -0,0 +1,56 @@ +// +// MessageGroupMoreView.swift +// Bark +// +// Created by huangfeng on 12/25/24. +// Copyright © 2024 Fin. All rights reserved. +// + +import UIKit + +class MessageGroupMoreView: UIView { + private let moreLabel: UILabel = { + let label = UILabel() + label.textColor = BKColor.grey.darken3 + label.font = UIFont.preferredFont(ofSize: 12) + return label + }() + + let arrowImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "keyboard_arrow_right_symbol")?.withRenderingMode(.alwaysTemplate) + imageView.tintColor = BKColor.grey.darken2 + return imageView + }() + + var count: Int = 0 { + didSet { + moreLabel.text = NSLocalizedString("viewMoreMessages").format(count) + } + } + + init() { + super.init(frame: CGRect.zero) + self.backgroundColor = BKColor.grey.lighten5 + self.layer.cornerRadius = 28 / 2 + self.clipsToBounds = true + + self.addSubview(moreLabel) + self.addSubview(arrowImageView) + moreLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(8) + make.height.equalTo(28).priority(.medium) + make.top.bottom.equalToSuperview() + } + arrowImageView.snp.makeConstraints { make in + make.right.equalToSuperview().offset(-6) + make.centerY.equalToSuperview() + make.left.equalTo(moreLabel.snp.right).offset(4) + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/View/MessageList/MessageItemView.swift b/View/MessageList/MessageItemView.swift index 585576e..c4f0c35 100644 --- a/View/MessageList/MessageItemView.swift +++ b/View/MessageList/MessageItemView.swift @@ -137,12 +137,12 @@ class MessageItemView: UIView { dateLabel.snp.makeConstraints { make in make.left.equalTo(bodyLabel) make.top.equalTo(bodyLabel.snp.bottom).offset(12) - make.bottom.equalTo(panel).offset(-12) + make.bottom.equalTo(panel).offset(-12).priority(.medium) } panel.snp.makeConstraints { make in make.left.equalToSuperview().offset(16) - make.right.equalToSuperview().offset(-16) + make.right.equalToSuperview().offset(-16).priority(.medium) make.top.equalToSuperview() make.bottom.equalToSuperview() } diff --git a/View/MessageList/MessageTableViewCell.swift b/View/MessageList/MessageTableViewCell.swift index 90f2545..49c8bbd 100644 --- a/View/MessageList/MessageTableViewCell.swift +++ b/View/MessageList/MessageTableViewCell.swift @@ -8,11 +8,12 @@ import Material import RxSwift +import SnapKit import UIKit /// 单个消息 cell class MessageTableViewCell: UITableViewCell { - let messageView = MessageItemView() + private let messageView = MessageItemView() var message: Message? { get { return messageView.message @@ -38,3 +39,196 @@ class MessageTableViewCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } } + +/// 群组 cell +class MessageGroupTableViewCell: UITableViewCell { + /// 方便做动画,实际没什么用 + private let panel: UIView = { + let panel = UIView() + panel.backgroundColor = BKColor.background.primary + return panel + }() + + /// 消息列表,最多显示 5 条,如果不足5条,多余的会隐藏 + private let messageViews = [ + MessageItemView(isShowShadow: true), + MessageItemView(isShowShadow: true), + MessageItemView(isShowShadow: true), + MessageItemView(isShowShadow: true), + MessageItemView(isShowShadow: true) + ] + + /// 群组 header ,包含标题、折叠按钮、清除按钮 + private let groupHeader = MessageGroupHeaderView() + /// 查看更多按钮,消息数小于等于 5 时会隐藏 + private let moreView = MessageGroupMoreView() + /// 群组 header top offset 约束 + private var groupHeaderTopConstraint: Constraint? = nil + + /// 是否展开 + var isExpanded: Bool = false { + didSet { + refreshViewState() + refreshLayout() + self.contentView.layoutIfNeeded() + } + } + + /// 消息列表 + var messages: [Message] = [] { + didSet { + for (index, item) in messageViews.enumerated() { + if index < messages.count { + item.message = messages[index] + item.isHidden = false + } else { + item.isHidden = true + } + } + refreshLayout() + } + } + + /// 剩余消息数量 + var moreCount: Int = 0 { + didSet { + moreView.count = moreCount + } + } + + /// 群组名 + var groupName: String? { + set { + if let newValue, !newValue.isEmpty { + groupHeader.groupName = newValue + } else { + groupHeader.groupName = NSLocalizedString("default") + } + } + get { + return groupHeader.groupName + } + } + + /// 折叠事件 + var showLessAction: (() -> Void)? { + get { + return groupHeader.showLessAction + } + set { + groupHeader.showLessAction = newValue + } + } + + /// 清除群组事件 + var clearAction: (() -> Void)? { + get { + return groupHeader.clearAction + } + set { + groupHeader.clearAction = newValue + } + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + self.selectionStyle = .none + + self.contentView.addSubview(panel) + panel.addSubview(groupHeader) + panel.addSubview(moreView) + for view in messageViews.reversed() { + panel.addSubview(view) + } + + panel.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + groupHeader.snp.remakeConstraints { make in + groupHeaderTopConstraint = make.top.equalToSuperview().offset(0).constraint + make.left.equalTo(16) + make.right.equalTo(-16) + } + + moreView.snp.remakeConstraints { make in + make.bottom.equalToSuperview().offset(-18) + make.centerX.equalToSuperview() + } + + refreshViewState() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// 更新UI + private func refreshViewState() { + self.messageViews.first?.bodyLabel.isUserInteractionEnabled = isExpanded + self.contentView.gestureRecognizers?.first?.isEnabled = !isExpanded + + for (index, view) in messageViews.enumerated() { + if isExpanded { + view.maskAlpha = 0 + } else { + view.maskAlpha = index == 0 ? 0 : CGFloat(index + 1) * 0.01 + } + } + } + + /// 更新布局 + private func refreshLayout() { + // 调整 header 位置 + groupHeaderTopConstraint?.update(offset: isExpanded ? 0 : 15) + + // 最大显示 5 条消息, 也就是 messageViews.count + let maxCount = min(self.messages.count, self.messageViews.count) + + // 清除旧的约束 + for view in messageViews { + view.snp.removeConstraints() + } + + for (index, item) in messageViews.enumerated() { + if index >= maxCount { + break + } + + item.snp.remakeConstraints { make in + make.left.right.equalToSuperview() + if isExpanded { + if index == 0 { + make.top.equalTo(groupHeader.snp.bottom).offset(8) + } else { + make.top.equalTo(messageViews[index - 1].snp.bottom).offset(8) + } + if index == maxCount - 1 { + if moreCount > 0 { + make.bottom.equalTo(moreView.snp.top).offset(-18) + } else { + make.bottom.equalToSuperview().offset(-18) + } + } + item.transform = .identity + } else { + if index == 0 { + make.top.equalToSuperview() + item.transform = .identity + } else { + // 底部边缘最多额外再显示1条消息 + make.top.equalToSuperview().offset(min(index * 8, 1 * 8)) + make.height.equalTo(messageViews[0]) + // 根据 index 逐渐缩小 + let scale = 1 - CGFloat(index) * 0.04 + item.transform = CGAffineTransform(scaleX: scale, y: 1) + } + if index == maxCount - 1 { + make.bottom.equalToSuperview().offset(-8) + } + } + } + } + } +}