Compare commits

...

17 Commits

25 changed files with 1179 additions and 377 deletions

View File

@ -20,6 +20,8 @@
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 */; };
061E35862D1E5028009A2D6F /* MessageItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061E35852D1E5028009A2D6F /* MessageItemModel.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 */; };
@ -102,7 +104,7 @@
0672CB06256903F700570C9D /* MessageListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0672CB05256903F700570C9D /* MessageListViewModel.swift */; };
06787C392A710568008ABDD7 /* GesturePassTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06787C382A710568008ABDD7 /* GesturePassTextView.swift */; };
06787C3B2AB82BDB008ABDD7 /* CrashReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06787C3A2AB82BDB008ABDD7 /* CrashReportViewController.swift */; };
067B2EB525693E38008B6BE1 /* MessageTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B2EB425693E38008B6BE1 /* MessageTableViewCellViewModel.swift */; };
067B2EB525693E38008B6BE1 /* MessageSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B2EB425693E38008B6BE1 /* MessageSection.swift */; };
06802E5320ECC40C00767047 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0661A549204FDA4100965E4E /* Assets.xcassets */; };
06840DBB272298FB001B3193 /* BKColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06840DBA272298FB001B3193 /* BKColor.swift */; };
0687F2A82CCB791A00B2A52F /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0687F2A72CCB791A00B2A52F /* UIFont+Extension.swift */; };
@ -113,6 +115,7 @@
068EC15827ED99C900D5D11E /* ServerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068EC15727ED99C900D5D11E /* ServerListViewController.swift */; };
068EC15A27ED99E700D5D11E /* ServerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068EC15927ED99E700D5D11E /* ServerListViewModel.swift */; };
068F66B3247BD84C00DAD25A /* MessageListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068F66B2247BD84C00DAD25A /* MessageListViewController.swift */; };
0699473D2D223094008D5E40 /* CustomTapTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0699473C2D223094008D5E40 /* CustomTapTextView.swift */; };
06AE3118266F4E2E00B39FBB /* GroupFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AE3117266F4E2E00B39FBB /* GroupFilterViewController.swift */; };
06AE311A266F4E6600B39FBB /* GroupFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AE3119266F4E6600B39FBB /* GroupFilterViewModel.swift */; };
06AE311C266F54A500B39FBB /* GroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AE311B266F54A500B39FBB /* GroupTableViewCell.swift */; };
@ -261,6 +264,8 @@
06172FDB27F6DB06002333A4 /* ServerListTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableViewCellViewModel.swift; sourceTree = "<group>"; };
061894C429962EB900E001C2 /* GradientButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButton.swift; sourceTree = "<group>"; };
061894C629A75BEA00E001C2 /* Algorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Algorithm.swift; sourceTree = "<group>"; };
061C17072D1BDA4B00891D66 /* MessageGroupMoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageGroupMoreView.swift; sourceTree = "<group>"; };
061E35852D1E5028009A2D6F /* MessageItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageItemModel.swift; sourceTree = "<group>"; };
0627DABA298B6EA2002F3F69 /* DropBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropBoxView.swift; sourceTree = "<group>"; };
0627DABC2990D615002F3F69 /* BorderTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderTextField.swift; sourceTree = "<group>"; };
062B98C2251B2762004562E7 /* BKButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BKButton.swift; sourceTree = "<group>"; };
@ -342,7 +347,7 @@
0672CB05256903F700570C9D /* MessageListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListViewModel.swift; sourceTree = "<group>"; };
06787C382A710568008ABDD7 /* GesturePassTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GesturePassTextView.swift; sourceTree = "<group>"; };
06787C3A2AB82BDB008ABDD7 /* CrashReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportViewController.swift; sourceTree = "<group>"; };
067B2EB425693E38008B6BE1 /* MessageTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTableViewCellViewModel.swift; sourceTree = "<group>"; };
067B2EB425693E38008B6BE1 /* MessageSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSection.swift; sourceTree = "<group>"; };
0683486A2050F1310024B6DA /* Bark.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Bark.entitlements; sourceTree = "<group>"; };
0683487020510FB20024B6DA /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
0683487220510FB20024B6DA /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; };
@ -353,6 +358,7 @@
068EC15727ED99C900D5D11E /* ServerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListViewController.swift; sourceTree = "<group>"; };
068EC15927ED99E700D5D11E /* ServerListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListViewModel.swift; sourceTree = "<group>"; };
068F66B2247BD84C00DAD25A /* MessageListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListViewController.swift; sourceTree = "<group>"; };
0699473C2D223094008D5E40 /* CustomTapTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTapTextView.swift; sourceTree = "<group>"; };
06AE3117266F4E2E00B39FBB /* GroupFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupFilterViewController.swift; sourceTree = "<group>"; };
06AE3119266F4E6600B39FBB /* GroupFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupFilterViewModel.swift; sourceTree = "<group>"; };
06AE311B266F54A500B39FBB /* GroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupTableViewCell.swift; sourceTree = "<group>"; };
@ -533,6 +539,8 @@
isa = PBXGroup;
children = (
06B1158E247BB1FB006D91FB /* Message.swift */,
067B2EB425693E38008B6BE1 /* MessageSection.swift */,
061E35852D1E5028009A2D6F /* MessageItemModel.swift */,
064CABA5256BE9510018155C /* PreviewModel.swift */,
06BD4DA92901352E003364DB /* Object+Dictionary.swift */,
061894C629A75BEA00E001C2 /* Algorithm.swift */,
@ -675,11 +683,12 @@
066890092D19495400E106F2 /* MessageList */ = {
isa = PBXGroup;
children = (
0699473C2D223094008D5E40 /* CustomTapTextView.swift */,
066890072D1946D500E106F2 /* MessageItemView.swift */,
0667D191247D162C005DE2ED /* MessageTableViewCell.swift */,
067B2EB425693E38008B6BE1 /* MessageTableViewCellViewModel.swift */,
0668900A2D19525400E106F2 /* ShowLessAndClearView.swift */,
0668900C2D19582400E106F2 /* MessageGroupHeaderView.swift */,
061C17072D1BDA4B00891D66 /* MessageGroupMoreView.swift */,
);
path = MessageList;
sourceTree = "<group>";
@ -1232,6 +1241,7 @@
0642B55C27EB149900453D91 /* MutableTextCellViewModel.swift in Sources */,
062B98C8251B27AE004562E7 /* UINavigationItem+Extension.swift in Sources */,
060481EE250F404500BC9799 /* SoundsViewController.swift in Sources */,
0699473D2D223094008D5E40 /* CustomTapTextView.swift in Sources */,
06EEF333291CCFF400CA228A /* CryptoSettingController.swift in Sources */,
0603706D20E23EC000F4CA05 /* BarkSFSafariViewController.swift in Sources */,
06AE311A266F4E6600B39FBB /* GroupFilterViewModel.swift in Sources */,
@ -1263,7 +1273,7 @@
06AE311E266F54CC00B39FBB /* GroupCellViewModel.swift in Sources */,
0637FA8620E0AB6600E80174 /* UIColor+Extension.swift in Sources */,
0637FA8A20E0D58800E80174 /* NewServerViewController.swift in Sources */,
067B2EB525693E38008B6BE1 /* MessageTableViewCellViewModel.swift in Sources */,
067B2EB525693E38008B6BE1 /* MessageSection.swift in Sources */,
0637FA8220E09C4B00E80174 /* BarkNavigationController.swift in Sources */,
06AE3118266F4E2E00B39FBB /* GroupFilterViewController.swift in Sources */,
0637FA7A20E092B300E80174 /* Observable+Extension.swift in Sources */,
@ -1285,8 +1295,10 @@
06787C392A710568008ABDD7 /* GesturePassTextView.swift in Sources */,
068EC15827ED99C900D5D11E /* ServerListViewController.swift in Sources */,
0637FA7C20E0930E00E80174 /* BarkApi.swift in Sources */,
061E35862D1E5028009A2D6F /* MessageItemModel.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 */,

View File

@ -199,6 +199,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
let controller = Client.shared.window?.rootViewController
let activityController = UIActivityViewController(activityItems: items,
applicationActivities: nil)
if let popover = activityController.popoverPresentationController {
popover.sourceView = controller?.view
popover.sourceRect = CGRect(x: controller?.view.bounds.midX ?? 0, y: controller?.view.bounds.midY ?? 0, width: 0, height: 0)
}
controller?.present(activityController, animated: true, completion: nil)
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel"), style: .cancel, handler: nil))

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz242 (3).png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz242 (2).png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz24 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz24.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "keyboard_arrow_right_keyboard_arrow_right_symbol.svg",
"idiom" : "universal"
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -2254,6 +2254,29 @@
}
}
},
"removeNotice" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "This will be permanently removed and cannot be undone."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bu kalıcı olarak kaldırılacak ve geri alınamaz."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "将永久删除并且无法恢复。"
}
}
}
},
"resetFailed" : {
"extractionState" : "manual",
"localizations" : {
@ -3128,6 +3151,29 @@
}
}
},
"viewAllMessages" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "View all %d messages"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tüm %d mesajı görüntüle"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "查看全部%d条消息"
}
}
}
},
"viewAllSounds" : {
"extractionState" : "manual",
"localizations" : {

View File

@ -34,6 +34,8 @@ class BKColor: NSObject {
}
public static let white = UIColor(named: "white")!
public static let black = UIColor(named: "black")!
enum background {
public static let primary = UIColor(named: "background")!

View File

@ -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)
}
}

View File

@ -40,16 +40,21 @@ class MessageListViewController: BaseViewController<MessageListViewModel> {
let groupButton: UIBarButtonItem = {
let btn = BKButton()
btn.setImage(UIImage(named: "baseline_folder_open_black_24pt"), for: .normal)
btn.setImage(UIImage(named: "group_expand")?.withRenderingMode(.alwaysTemplate), for: .normal)
btn.setImage(UIImage(named: "group_collapse")?.withRenderingMode(.alwaysTemplate), for: .selected)
btn.imageView?.tintColor = BKColor.black
btn.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
return UIBarButtonItem(customView: btn)
}()
let tableView: UITableView = {
private var expandedGroup: Set<String> = []
lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.separatorStyle = .none
tableView.backgroundColor = BKColor.background.primary
tableView.register(MessageTableViewCell.self, forCellReuseIdentifier: "\(MessageTableViewCell.self)")
tableView.register(MessageGroupTableViewCell.self, forCellReuseIdentifier: "\(MessageGroupTableViewCell.self)")
// LargeTitle LargeTitle
//
// tableView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
@ -57,6 +62,10 @@ class MessageListViewController: BaseViewController<MessageListViewModel> {
// contentInset header
tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 20))
tableView.rx.setDelegate(self).disposed(by: rx.disposeBag)
tableView.mj_footer = MJRefreshAutoFooter()
tableView.refreshControl = UIRefreshControl()
return tableView
}()
@ -71,9 +80,6 @@ class MessageListViewController: BaseViewController<MessageListViewModel> {
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
tableView.rx.setDelegate(self).disposed(by: rx.disposeBag)
tableView.mj_footer = MJRefreshAutoFooter()
tableView.refreshControl = UIRefreshControl()
// tab
Client.shared.currentTabBarController?
@ -98,13 +104,137 @@ class MessageListViewController: BaseViewController<MessageListViewModel> {
.subscribe(onNext: { [weak self] _ in
self?.tableView.refreshControl?.sendActions(for: .valueChanged)
}).disposed(by: rx.disposeBag)
//
tableView.rx.itemSelected.subscribe(onNext: { [weak self] indexPath in
guard let self else { return }
if let cell = self.tableView.cellForRow(at: indexPath) as? MessageGroupTableViewCell {
if !cell.isExpanded {
UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2) {
self.tableView.performBatchUpdates {
cell.isExpanded = true
}
}
if let groupName = cell.cellData?.groupName {
self.expandedGroup.insert(groupName)
}
}
}
}).disposed(by: rx.disposeBag)
}
// tableView
private lazy var dataSource = RxTableViewSectionedAnimatedDataSource<MessageSection>(
animationConfiguration: AnimationConfiguration(
insertAnimation: .none,
reloadAnimation: .none,
deleteAnimation: .left
),
configureCell: { _, tableView, _, item -> UITableViewCell in
switch item {
case .message(let message):
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(MessageTableViewCell.self)") as? MessageTableViewCell else {
return UITableViewCell()
}
cell.tapAction = { [weak self, weak cell] message, sourceView in
guard let self else { return }
self.alertMessage(message: message.attributedText?.string ?? "", sourceView: sourceView)
}
cell.message = message
return cell
case .messageGroup(let title, let totalCount, let messages):
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(MessageGroupTableViewCell.self)") as? MessageGroupTableViewCell else {
return UITableViewCell()
}
cell.showLessAction = { [weak self, weak cell] in
guard let self else { return }
UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2) {
self.tableView.performBatchUpdates {
cell?.isExpanded = false
}
if let groupName = cell?.cellData?.groupName {
self.expandedGroup.remove(groupName)
}
}
}
cell.showGroupMessageAction = { [weak self] group in
let viewModel = MessageListViewModel(sourceType: .group(group))
let controller = MessageListViewController(viewModel: viewModel)
self?.navigationController?.pushViewController(controller, animated: true)
}
cell.clearAction = { [weak self, weak cell] in
guard let self, let cell, let indexPath = self.tableView.indexPath(for: cell) else { return }
self.tableView.dataSource?.tableView?(self.tableView, commit: .delete, forRowAt: indexPath)
}
cell.tapAction = { [weak self, weak cell] message, sourceView in
guard let self else { return }
self.alertMessage(message: message.attributedText?.string ?? "", sourceView: sourceView)
}
cell.cellData = (title, totalCount, messages)
cell.isExpanded = self.expandedGroup.contains(title)
return cell
}
}, canEditRowAtIndexPath: { _, _ in
true
}
)
override func bindViewModel() {
guard let deleteBtn = deleteButton.customView as? BKButton else {
guard let groupBtn = groupButton.customView as? BKButton else {
return
}
let batchDelete = deleteBtn.rx
let output = viewModel.transform(
input: MessageListViewModel.Input(
refresh: tableView.refreshControl!.rx.controlEvent(.valueChanged).asDriver(),
loadMore: tableView.mj_footer!.rx.refresh.asDriver(),
itemDelete: tableView.rx.modelDeleted(MessageListCellItem.self).asDriver(),
delete: getBatchDeleteDriver(),
groupToggleTap: groupBtn.rx.tap.asDriver(),
searchText: navigationItem.searchController!.searchBar.rx.text.asObservable()
))
// tableView
output.refreshAction
.drive(tableView.rx.refreshAction)
.disposed(by: rx.disposeBag)
output.messages
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: rx.disposeBag)
//
output.type
.drive(onNext: { [weak self] type in
(self?.groupButton.customView as? UIButton)?.isSelected = type == .group
}).disposed(by: rx.disposeBag)
//
output.title
.drive(self.navigationItem.rx.title).disposed(by: rx.disposeBag)
//
output.errorAlert
.drive(onNext: { [weak self] error in
let alertController = UIAlertController(title: "Error", message: error, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Copy2"), style: .default, handler: { _ in
UIPasteboard.general.string = error
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel"), style: .cancel, handler: nil))
self?.present(alertController, animated: true, completion: nil)
}).disposed(by: rx.disposeBag)
output.groupToggleButtonHidden
.drive((groupButton.customView as! UIButton).rx.isHidden).disposed(by: rx.disposeBag)
}
private func getBatchDeleteDriver() -> Driver<MessageDeleteType> {
guard let deleteBtn = deleteButton.customView as? BKButton else {
return Driver.never()
}
return deleteBtn.rx
.tap
.flatMapLatest { _ -> PublishRelay<MessageDeleteType> in
let relay = PublishRelay<MessageDeleteType>()
@ -144,66 +274,10 @@ class MessageListViewController: BaseViewController<MessageListViewModel> {
return relay
}
guard let groupBtn = groupButton.customView as? BKButton else {
return
}
let output = viewModel.transform(
input: MessageListViewModel.Input(
refresh: tableView.refreshControl!.rx.controlEvent(.valueChanged).asDriver(),
loadMore: tableView.mj_footer!.rx.refresh.asDriver(),
itemDelete: tableView.rx.itemDeleted.asDriver().map { $0.row },
itemSelected: tableView.rx.itemSelected.asDriver().map { $0.row },
delete: batchDelete.asDriver(onErrorDriveWith: .empty()),
groupTap: groupBtn.rx.tap.asDriver(),
searchText: navigationItem.searchController!.searchBar.rx.text.asObservable()
))
// tableView
output.refreshAction
.drive(tableView.rx.refreshAction)
.disposed(by: rx.disposeBag)
// tableView
let dataSource = RxTableViewSectionedAnimatedDataSource<MessageSection>(
animationConfiguration: AnimationConfiguration(
insertAnimation: .none,
reloadAnimation: .none,
deleteAnimation: .left
),
configureCell: { _, tableView, _, item -> UITableViewCell in
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(MessageTableViewCell.self)") as? MessageTableViewCell else {
return UITableViewCell()
}
cell.message = item.message
return cell
}, canEditRowAtIndexPath: { _, _ in
true
}
)
output.messages
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: rx.disposeBag)
// messagealert
output.alertMessage.drive(onNext: { [weak self] message in
self?.alertMessage(message: message.0, indexPath: IndexPath(row: message.1, section: 0))
}).disposed(by: rx.disposeBag)
//
output.groupFilter
.drive(onNext: { [weak self] groupModel in
self?.navigationController?.present(BarkNavigationController(rootViewController: GroupFilterViewController(viewModel: groupModel)), animated: true, completion: nil)
}).disposed(by: rx.disposeBag)
//
output.title
.drive(self.navigationItem.rx.title).disposed(by: rx.disposeBag)
.asDriver(onErrorDriveWith: .empty())
}
func alertMessage(message: String, indexPath: IndexPath) {
private func alertMessage(message: String, sourceView: UIView) {
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let copyAction = UIAlertAction(title: NSLocalizedString("CopyAll"), style: .default, handler: { [weak self]
(_: UIAlertAction) in
@ -216,11 +290,9 @@ class MessageListViewController: BaseViewController<MessageListViewModel> {
alertController.addAction(copyAction)
alertController.addAction(cancelAction)
if UIDevice.current.userInterfaceIdiom == .pad {
if let cell = self.tableView.cellForRow(at: indexPath) {
alertController.popoverPresentationController?.sourceView = self.tableView
alertController.popoverPresentationController?.sourceRect = cell.frame
alertController.modalPresentationStyle = .popover
}
alertController.popoverPresentationController?.sourceView = sourceView.superview
alertController.popoverPresentationController?.sourceRect = sourceView.frame
alertController.modalPresentationStyle = .popover
}
self.navigationController?.present(alertController, animated: true, completion: nil)
@ -236,8 +308,25 @@ class MessageListViewController: BaseViewController<MessageListViewModel> {
extension MessageListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let action = UIContextualAction(style: .destructive, title: NSLocalizedString("removeMessage")) { [weak self] _, _, actionPerformed in
self?.tableView.dataSource?.tableView?(self!.tableView, commit: .delete, forRowAt: indexPath)
actionPerformed(true)
guard let self else { return }
if self.tableView.cellForRow(at: indexPath) is MessageTableViewCell {
//
self.tableView.dataSource?.tableView?(self.tableView, commit: .delete, forRowAt: indexPath)
actionPerformed(true)
return
}
//
let alertView = UIAlertController(title: nil, message: NSLocalizedString("removeNotice"), preferredStyle: .alert)
alertView.addAction(UIAlertAction(title: NSLocalizedString("removeMessage"), style: .destructive, handler: { _ in
self.tableView.dataSource?.tableView?(self.tableView, commit: .delete, forRowAt: indexPath)
actionPerformed(true)
}))
alertView.addAction(UIAlertAction(title: NSLocalizedString("Cancel"), style: .cancel, handler: { _ in
actionPerformed(false)
}))
self.present(alertView, animated: true, completion: nil)
}
let configuration = UISwipeActionsConfiguration(actions: [action])

View File

@ -12,37 +12,88 @@ import RxCocoa
import RxDataSources
import RxSwift
enum MessageListCellItem {
///
case message(model: Message)
///
case messageGroup(group: [Message])
enum MessageListType: Int, Codable {
//
case list
//
case group
}
enum MessageSourceType {
///
case all
///
case group(String?)
}
class MessageListViewModel: ViewModel, ViewModelType {
struct Input {
///
var refresh: Driver<Void>
///
var loadMore: Driver<Void>
var itemDelete: Driver<Int>
var itemSelected: Driver<Int>
///
var itemDelete: Driver<MessageListCellItem>
///
var delete: Driver<MessageDeleteType>
var groupTap: Driver<Void>
///
var groupToggleTap: Driver<Void>
///
var searchText: Observable<String?>
}
struct Output {
///
var messages: Driver<[MessageSection]>
///
var refreshAction: Driver<MJRefreshAction>
var alertMessage: Driver<(String, Int)>
var groupFilter: Driver<GroupFilterViewModel>
///
var type: Driver<MessageListType>
///
var title: Driver<String>
///
var groupToggleButtonHidden: Driver<Bool>
///
var errorAlert: Driver<String>
}
private static let typeKey = "me.fin.messageListType"
///
private var type: MessageListType = {
if let t: MessageListType = Settings[MessageListViewModel.typeKey] {
return t
}
return .list
}() {
didSet {
Settings[MessageListViewModel.typeKey] = type
}
}
///
private var sourceType: MessageSourceType = .all
///
private var page = 0
///
private let pageCount = 20
///
private var groups: Results<Message>?
///
private var results: Results<Message>?
//
private var errorAlert: PublishRelay<String> = PublishRelay()
convenience init(sourceType: MessageSourceType) {
self.init()
self.sourceType = sourceType
}
///
private func getResults(filterGroups: [String?], searchText: String?) -> Results<Message>? {
if let realm = try? Realm() {
do {
let realm = try Realm()
var results = realm.objects(Message.self)
.sorted(byKeyPath: "createDate", ascending: false)
if filterGroups.count > 0 {
@ -52,51 +103,101 @@ class MessageListViewModel: ViewModel, ViewModelType {
results = results.filter("title CONTAINS[c] %@ OR subtitle CONTAINS[c] %@ OR body CONTAINS[c] %@", text, text, text)
}
return results
} catch {
// 1,
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.errorAlert.accept(error.localizedDescription)
}
}
return nil
}
private var page = 0
private let pageCount = 20
private func getNextPage() -> [Message] {
if let result = results {
let startIndex = page * pageCount
let endIndex = min(startIndex + pageCount, result.count)
guard endIndex > startIndex else {
return []
}
var messages: [Message] = []
for i in startIndex..<endIndex {
messages.append(result[i])
}
page += 1
return messages
///
private var searchText: String = ""
///
private func getGroups() -> Results<Message>? {
if let realm = try? Realm() {
return realm.objects(Message.self)
.sorted(byKeyPath: "createDate", ascending: false)
.distinct(by: ["group"])
}
return []
return nil
}
/// message
private func getListNextPage() -> [MessageListCellItem] {
guard let result = results else {
return []
}
let startIndex = page * pageCount
let endIndex = min(startIndex + pageCount, result.count)
guard endIndex > startIndex else {
return []
}
var messages: [MessageListCellItem] = []
for i in startIndex..<endIndex {
// messages.append(result[i].freeze())
// freeze freeze copy
// copy message 访退
messages.append(.message(model: MessageItemModel(message: result[i])))
}
page += 1
return messages
}
/// group
private func getGroupNextPage() -> [MessageListCellItem] {
guard let groups, let results else {
return []
}
let startIndex = page * pageCount
let endIndex = min(startIndex + pageCount, groups.count)
guard endIndex > startIndex else {
return []
}
var items: [MessageListCellItem] = []
for i in startIndex..<endIndex {
let group = groups[i].group
let messageResult: Results<Message>
if let group {
messageResult = results.filter("group == %@", group)
} else {
messageResult = results.filter("group == nil")
}
var messages: [MessageItemModel] = []
for i in 0..<min(messageResult.count, 5) {
messages.append(MessageItemModel(message: messageResult[i]))
}
if messages.count == 1 {
//
items.append(.message(model: messages[0]))
} else if messages.count > 0 {
//
items.append(.messageGroup(name: group ?? NSLocalizedString("default"), totalCount: messageResult.count, messages: messages))
}
}
page += 1
return items
}
private func getNextPage() -> [MessageListCellItem] {
if case .group = self.sourceType {
//
return getListNextPage()
}
if type == .list || !searchText.isEmpty {
//
return getListNextPage()
}
return getGroupNextPage()
}
func transform(input: Input) -> Output {
let alertMessage = input.itemSelected.map { [weak self] index in
guard let results = self?.results else {
return ("", 0)
}
let message = results[index]
// let message = model.message
var copyContent: String = ""
if let title = message.title {
copyContent += "\(title)\n"
}
if let body = message.body {
copyContent += "\(body)\n"
}
if let url = message.url {
copyContent += "\(url)\n"
}
copyContent = String(copyContent.prefix(copyContent.count - 1))
return (copyContent, index)
}
//
let titleRelay = BehaviorRelay<String>(value: NSLocalizedString("historyMessage"))
//
@ -104,20 +205,16 @@ class MessageListViewModel: ViewModel, ViewModelType {
//
let refreshAction = BehaviorRelay<MJRefreshAction>(value: .none)
//
let filterGroups: BehaviorRelay<[String?]> = {
if let groups: [String?] = Settings["me.fin.filterGroups"] {
return BehaviorRelay<[String?]>(value: groups)
let filterGroups: BehaviorRelay<[String?]> = { [weak self] in
guard let self = self else {
return BehaviorRelay<[String?]>(value: [])
}
if case .group(let name) = self.sourceType {
return BehaviorRelay<[String?]>(value: [name])
}
return BehaviorRelay<[String?]>(value: [])
}()
// Message MessageSection
func messagesToMessageSection(messages: [Message]) -> [MessageSection] {
let cellViewModels = messages.map { message -> MessageTableViewCellViewModel in
MessageTableViewCellViewModel(message: message)
}
return [MessageSection(header: "model", messages: cellViewModels)]
}
//
filterGroups
.subscribe(onNext: { filterGroups in
@ -132,23 +229,34 @@ class MessageListViewModel: ViewModel, ViewModelType {
Observable
.combineLatest(filterGroups, input.searchText)
.subscribe(onNext: { [weak self] groups, searchText in
self?.searchText = searchText ?? ""
self?.results = self?.getResults(filterGroups: groups, searchText: searchText)
if case .all = self?.sourceType {
//
self?.groups = self?.getGroups()
}
}).disposed(by: rx.disposeBag)
//
let messageTypeChanged = input.groupToggleTap.compactMap { () -> MessageListType? in
self.type = self.type == .group ? .list : .group
return self.type
}
//
Observable
.merge(
input.refresh.asObservable().map { () },
filterGroups.map { _ in () },
input.searchText.asObservable().map { _ in () }
input.searchText.asObservable().map { _ in () },
messageTypeChanged.asObservable().map { _ in () }
)
.subscribe(onNext: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.page = 0
let messages = strongSelf.getNextPage()
messagesRelay.accept(
messagesToMessageSection(
messages: strongSelf.getNextPage()
)
[MessageSection(header: "model", messages: messages)]
)
refreshAction.accept(.endRefresh)
}).disposed(by: rx.disposeBag)
@ -160,31 +268,70 @@ class MessageListViewModel: ViewModel, ViewModelType {
.delay(.milliseconds(10), scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] in
guard let strongSelf = self else { return }
let messages = strongSelf.getNextPage()
let cellViewModels = messages.map { message -> MessageTableViewCellViewModel in
MessageTableViewCellViewModel(message: message)
}
let items = strongSelf.getNextPage()
refreshAction.accept(.endLoadmore)
if var section = messagesRelay.value.first {
section.messages.append(contentsOf: cellViewModels)
section.messages.append(contentsOf: items)
messagesRelay.accept([section])
} else {
messagesRelay.accept([MessageSection(header: "model", messages: cellViewModels)])
messagesRelay.accept([MessageSection(header: "model", messages: items)])
}
}).disposed(by: rx.disposeBag)
// message
input.itemDelete.drive(onNext: { [weak self] index in
if var section = messagesRelay.value.first {
if let realm = try? Realm(), let message = self?.results?[index] {
input.itemDelete.drive(onNext: { [weak self] item in
guard let self else { return }
guard var section = messagesRelay.value.first else {
return
}
//
switch item {
case .message(let model):
//
if let realm = try? Realm(),
let message = realm.objects(Message.self).filter("id == %@", model.id).first
{
try? realm.write {
realm.delete(message)
}
}
section.messages.remove(at: index)
messagesRelay.accept([section])
// cell item
section.messages.removeAll { cellItem in
if case .message(let m) = cellItem {
return m.id == model.id
}
return false
}
case .messageGroup(let groupName, _, let messages):
//
if let realm = try? Realm(), let first = messages.first {
let messageResult: Results<Message>?
if let group = first.group {
messageResult = self.results?.filter("group == %@", group)
} else {
messageResult = self.results?.filter("group == nil")
}
if let messageResult {
try? realm.write {
realm.delete(messageResult)
}
}
}
// cell item
section.messages.removeAll { cellItem in
if case .messageGroup(let name, _, _) = cellItem {
return name == groupName
}
return false
}
}
//
messagesRelay.accept([section])
}).disposed(by: rx.disposeBag)
//
@ -214,49 +361,25 @@ class MessageListViewModel: ViewModel, ViewModelType {
}
strongSelf.page = 0
messagesRelay.accept(messagesToMessageSection(messages: strongSelf.getNextPage()))
messagesRelay.accept([MessageSection(header: "model", messages: strongSelf.getNextPage())])
}).disposed(by: rx.disposeBag)
//
let groupFilter = input.groupTap.compactMap { () -> GroupFilterViewModel? in
if let realm = try? Realm() {
let groups = realm.objects(Message.self)
.distinct(by: ["group"])
.value(forKeyPath: "group") as? [String?]
let groupModels = groups?.compactMap { groupName -> GroupFilterModel in
var check = true
if filterGroups.value.count > 0 {
check = filterGroups.value.contains(groupName)
}
return GroupFilterModel(name: groupName, checked: check)
}
if let models = groupModels {
let viewModel = GroupFilterViewModel(groups: models)
// group
viewModel.done.subscribe(onNext: { filterGroups in
Settings["me.fin.filterGroups"] = filterGroups
}).disposed(by: viewModel.rx.disposeBag)
//
viewModel.done
.bind(to: filterGroups)
.disposed(by: viewModel.rx.disposeBag)
return viewModel
}
//
let groupToggleButtonHidden = {
if case .group = self.sourceType {
return true
}
return nil
}
return false
}()
return Output(
messages: messagesRelay.asDriver(onErrorJustReturn: []),
refreshAction: refreshAction.asDriver(),
alertMessage: alertMessage,
groupFilter: groupFilter.asDriver(),
title: titleRelay.asDriver()
type: Driver.merge(messageTypeChanged.asDriver(), Driver.just(self.type)),
title: titleRelay.asDriver(),
groupToggleButtonHidden: Driver.just(groupToggleButtonHidden),
errorAlert: errorAlert.asDriver(onErrorDriveWith: .empty())
)
}
}

View File

@ -6,9 +6,9 @@
// Copyright © 2020 Fin. All rights reserved.
//
//import IceCream
import RealmSwift
import UIKit
class Message: Object {
@objc dynamic var id = NSUUID().uuidString
@objc dynamic var title: String?
@ -26,6 +26,3 @@ class Message: Object {
return ["group", "createDate"]
}
}
//extension Message: CKRecordConvertible {}
//extension Message: CKRecordRecoverable {}

View File

@ -0,0 +1,97 @@
//
// MessageItemModel.swift
// Bark
//
// Created by huangfeng on 12/27/24.
// Copyright © 2024 Fin. All rights reserved.
//
import UIKit
enum MessageListCellDateStyle {
/// 11
case relative
/// 2024-01-01 12:00
case exact
}
class MessageItemModel {
var id: String = ""
var group: String?
var attributedText: NSAttributedString?
var dateText: String?
var createDate: Date?
var dateStyle: MessageListCellDateStyle = .relative {
didSet {
switch dateStyle {
case .relative:
dateText = createDate?.agoFormatString()
case .exact:
dateText = createDate?.formatString(format: "yyyy-MM-dd HH:mm")
}
}
}
init(message: Message) {
self.id = message.id
self.group = message.group
let title = message.title ?? ""
let subtitle = message.subtitle ?? ""
let body = message.body ?? ""
let url = message.url ?? ""
let text = NSMutableAttributedString(
string: body,
attributes: [.font: UIFont.preferredFont(ofSize: 14), .foregroundColor: BKColor.grey.darken4]
)
if subtitle.count > 0 {
// spacer
text.insert(NSAttributedString(
string: "\n",
attributes: [.font: UIFont.systemFont(ofSize: 6, weight: .medium)]
), at: 0)
text.insert(NSAttributedString(
string: subtitle + "\n",
attributes: [.font: UIFont.preferredFont(ofSize: 16, weight: .medium), .foregroundColor: BKColor.grey.darken4]
), at: 0)
}
if title.count > 0 {
// spacer
text.insert(NSAttributedString(
string: "\n",
attributes: [.font: UIFont.systemFont(ofSize: 6, weight: .medium)]
), at: 0)
text.insert(NSAttributedString(
string: title + "\n",
attributes: [.font: UIFont.preferredFont(ofSize: 16, weight: .medium), .foregroundColor: BKColor.grey.darken4]
), at: 0)
}
if url.count > 0 {
// spacer
text.append(NSAttributedString(
string: "\n ",
attributes: [.font: UIFont.systemFont(ofSize: 8, weight: .medium)]
))
text.append(NSAttributedString(string: "\n\(url)", attributes: [
.font: UIFont.preferredFont(ofSize: 14),
.foregroundColor: BKColor.grey.darken4,
.link: url
]))
}
self.attributedText = text
self.createDate = message.createDate
defer {
self.dateStyle = .relative
}
}
}

View File

@ -0,0 +1,80 @@
//
// MessageTableViewCellViewModel.swift
// Bark
//
// Created by huangfeng on 2020/11/21.
// Copyright © 2020 Fin. All rights reserved.
//
import Differentiator
import Foundation
import RxCocoa
import RxDataSources
enum MessageListCellItem: Equatable {
///
case message(model: MessageItemModel)
///
case messageGroup(name: String, totalCount: Int, messages: [MessageItemModel])
static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.message(let l), .message(let r)):
return l.id == r.id && l.dateText == r.dateText
case (.messageGroup(let l, _, let lMessages), .messageGroup(let r, _, let rMessages)):
if l != r {
return false
}
if lMessages.first?.dateText != rMessages.first?.dateText {
return false
}
if lMessages.count != rMessages.count {
return false
}
for (lMessage, rMessage) in zip(lMessages, rMessages) {
if lMessage.id != rMessage.id {
return false
}
}
return true
default:
return false
}
}
}
extension MessageListCellItem: IdentifiableType {
typealias Identity = String
var identity: String {
switch self {
case .message(let model):
return "list_\(model.id)"
case .messageGroup(_, _, let messages):
return "group_\(messages.first?.id ?? "")"
}
}
}
struct MessageSection {
var header: String
var messages: [MessageListCellItem]
}
extension MessageSection: AnimatableSectionModelType {
typealias Item = MessageListCellItem
typealias Identity = String
var items: [MessageListCellItem] {
return self.messages
}
init(original: MessageSection, items: [MessageListCellItem]) {
self = original
self.messages = items
}
var identity: String {
return header
}
}

View File

@ -0,0 +1,80 @@
//
// CustomTapTextView.swift
// Bark
//
// Created by huangfeng on 12/30/24.
// Copyright © 2024 Fin. All rights reserved.
//
import UIKit
/// UITextView UITextView
/// TextView
class CustomTapTextView: UITextView, UIGestureRecognizerDelegate {
///
private lazy var tapGesture = UITapGestureRecognizer(target: self, action: #selector(tap))
/// tapGesture
private let doubleTapGesture = UITapGestureRecognizer()
/// UITextView
private var linkTapGesture: UIGestureRecognizer? = nil
///
var customTapAction: (() -> Void)?
init() {
super.init(frame: .zero, textContainer: nil)
self.backgroundColor = UIColor.clear
self.isEditable = false
self.dataDetectorTypes = [.phoneNumber, .link]
self.isScrollEnabled = false
self.textContainerInset = .zero
self.textContainer.lineFragmentPadding = 0
tapGesture.delegate = self
self.addGestureRecognizer(tapGesture)
self.linkTapGesture = self.gestureRecognizers?.first { $0 is UITapGestureRecognizer && $0.name == "UITextInteractionNameLinkTap" }
doubleTapGesture.numberOfTapsRequired = 2
doubleTapGesture.delegate = self
self.addGestureRecognizer(doubleTapGesture)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func tap() {
self.customTapAction?()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == doubleTapGesture {
return true
}
return false
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == tapGesture {
if self.selectedRange.length > 0 {
return false
}
}
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == tapGesture {
if otherGestureRecognizer == doubleTapGesture {
return true
}
if otherGestureRecognizer == linkTapGesture {
return true
}
}
return false
}
}

View File

@ -50,13 +50,19 @@ class MessageGroupHeaderView: UIView {
make.bottom.equalTo(-10)
}
showLessAndClearView.snp.makeConstraints { make in
make.right.equalTo(-8)
make.right.equalToSuperview()
make.centerY.equalTo(groupNameLabel)
}
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func tap() {
showLessAction?()
}
}

View File

@ -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("viewAllMessages").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")
}
}

View File

@ -8,13 +8,6 @@
import UIKit
enum MessageListCellDateStyle {
/// 11
case relative
/// 2024-01-01 12:00
case exact
}
class MessageItemView: UIView {
let panel: UIView = {
let view = UIView()
@ -26,20 +19,14 @@ class MessageItemView: UIView {
let blackMaskView: UIView = {
let view = UIView()
view.layer.cornerRadius = 10
view.backgroundColor = UIColor.black
view.backgroundColor = BKColor.black
view.isUserInteractionEnabled = false
view.alpha = 0
return view
}()
let bodyLabel: UITextView = {
let label = UITextView()
label.backgroundColor = UIColor.clear
label.isEditable = false
label.dataDetectorTypes = [.phoneNumber, .link]
label.isScrollEnabled = false
label.textContainerInset = .zero
label.textContainer.lineFragmentPadding = 0
let bodyLabel: CustomTapTextView = {
let label = CustomTapTextView()
label.font = UIFont.preferredFont(ofSize: 14)
label.adjustsFontForContentSizeCategory = true
label.textColor = BKColor.grey.darken4
@ -57,7 +44,7 @@ class MessageItemView: UIView {
return label
}()
var message: Message? = nil {
var message: MessageItemModel? = nil {
didSet {
guard let message else {
return
@ -66,35 +53,22 @@ class MessageItemView: UIView {
}
}
var dateStyle: MessageListCellDateStyle = .relative {
didSet {
guard let message else {
return
}
switch dateStyle {
case .relative:
dateLabel.text = message.createDate?.agoFormatString()
case .exact:
dateLabel.text = message.createDate?.formatString(format: "yyyy-MM-dd HH:mm")
}
}
}
var maskAlpha: CGFloat = 0 {
didSet {
blackMaskView.alpha = maskAlpha
}
}
var isShowShadow: Bool = false {
var isShowSubviews: Bool = true {
didSet {
panel.layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor
panel.layer.shadowOffset = CGSize(width: 0, height: 5)
panel.layer.shadowRadius = 5
panel.layer.shadowOpacity = isShowShadow ? 0.05 : 0
for view in [bodyLabel, dateLabel] {
view.alpha = isShowSubviews ? 1 : 0
}
}
}
var tapAction: ((_ message: MessageItemModel, _ sourceView: UIView) -> Void)?
init() {
super.init(frame: .zero)
self.backgroundColor = BKColor.background.primary
@ -108,21 +82,22 @@ class MessageItemView: UIView {
//
dateLabel.gestureRecognizers?.first?.rx.event.subscribe(onNext: { [weak self] _ in
guard let self else { return }
if self.dateStyle != .exact {
self.dateStyle = .exact
if self.message?.dateStyle != .exact {
self.message?.dateStyle = .exact
} else {
self.dateStyle = .relative
self.message?.dateStyle = .relative
}
self.dateLabel.text = self.message?.dateText
}).disposed(by: rx.disposeBag)
self.bodyLabel.customTapAction = { [weak self] in
guard let self, let message = self.message else { return }
self.tapAction?(message, self)
}
panel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))
}
convenience init(isShowShadow: Bool) {
self.init()
defer {
self.isShowShadow = isShowShadow
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
@ -137,7 +112,7 @@ 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
@ -150,62 +125,16 @@ class MessageItemView: UIView {
make.edges.equalTo(panel)
}
}
@objc func tap() {
guard let message else { return }
self.tapAction?(message, self)
}
}
extension MessageItemView {
func setMessage(message: Message) {
let title = message.title ?? ""
let subtitle = message.subtitle ?? ""
let body = message.body ?? ""
let url = message.url ?? ""
let text = NSMutableAttributedString(
string: body,
attributes: [.font: UIFont.preferredFont(ofSize: 14), .foregroundColor: BKColor.grey.darken4]
)
if subtitle.count > 0 {
// spacer
text.insert(NSAttributedString(
string: "\n",
attributes: [.font: UIFont.systemFont(ofSize: 6, weight: .medium)]
), at: 0)
text.insert(NSAttributedString(
string: subtitle + "\n",
attributes: [.font: UIFont.preferredFont(ofSize: 16, weight: .medium), .foregroundColor: BKColor.grey.darken4]
), at: 0)
}
if title.count > 0 {
// spacer
text.insert(NSAttributedString(
string: "\n",
attributes: [.font: UIFont.systemFont(ofSize: 6, weight: .medium)]
), at: 0)
text.insert(NSAttributedString(
string: title + "\n",
attributes: [.font: UIFont.preferredFont(ofSize: 16, weight: .medium), .foregroundColor: BKColor.grey.darken4]
), at: 0)
}
if url.count > 0 {
// spacer
text.append(NSAttributedString(
string: "\n ",
attributes: [.font: UIFont.systemFont(ofSize: 8, weight: .medium)]
))
text.append(NSAttributedString(string: "\n\(url)", attributes: [
.font: UIFont.preferredFont(ofSize: 14),
.foregroundColor: BKColor.grey.darken4,
.link: url
]))
}
self.bodyLabel.attributedText = text
self.dateStyle = .relative
func setMessage(message: MessageItemModel) {
self.bodyLabel.attributedText = message.attributedText
self.dateLabel.text = message.dateText
}
}

View File

@ -8,12 +8,13 @@
import Material
import RxSwift
import SnapKit
import UIKit
/// cell
class MessageTableViewCell: UITableViewCell {
let messageView = MessageItemView()
var message: Message? {
private let messageView = MessageItemView()
var message: MessageItemModel? {
get {
return messageView.message
}
@ -22,6 +23,12 @@ class MessageTableViewCell: UITableViewCell {
}
}
var tapAction: ((_ message: MessageItemModel, _ sourceView: UIView) -> Void)? {
didSet {
messageView.tapAction = tapAction
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
@ -38,3 +45,222 @@ 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(),
MessageItemView(),
MessageItemView(),
MessageItemView(),
MessageItemView()
]
/// 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()
}
}
///
private var messages: [MessageItemModel] = [] {
didSet {
for (index, item) in messageViews.enumerated() {
if index < messages.count {
item.message = messages[index]
item.isHidden = false
} else {
item.isHidden = true
}
}
}
}
///
private var totalCount: Int = 0 {
didSet {
moreView.count = totalCount
}
}
///
private var groupName: String? {
set {
if let newValue, !newValue.isEmpty {
groupHeader.groupName = newValue
} else {
groupHeader.groupName = NSLocalizedString("default")
}
}
get {
return groupHeader.groupName
}
}
var cellData: (groupName: String?, totalCount: Int, messages: [MessageItemModel])? {
didSet {
groupName = cellData?.groupName ?? ""
totalCount = cellData?.totalCount ?? 0
messages = cellData?.messages ?? []
}
}
///
var showLessAction: (() -> Void)? {
get {
return groupHeader.showLessAction
}
set {
groupHeader.showLessAction = newValue
}
}
///
var clearAction: (() -> Void)? {
get {
return groupHeader.clearAction
}
set {
groupHeader.clearAction = newValue
}
}
var tapAction: ((_ message: MessageItemModel, _ sourceView: UIView) -> Void)? = nil
///
var showGroupMessageAction: ((_ group: String?) -> Void)? = nil
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()
}
moreView.addGestureRecognizer(UITapGestureRecognizer())
moreView.gestureRecognizers?.first?.rx.event.subscribe(onNext: { [weak self] _ in
self?.showGroupMessageAction?(self?.messages.first?.group)
}).disposed(by: self.rx.disposeBag)
for view in messageViews {
view.tapAction = { [weak self] message, sourceView in
self?.tapAction?(message, sourceView)
}
}
refreshViewState()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// UI
private func refreshViewState() {
self.messageViews.first?.panel.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 {
item.isShowSubviews = true
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 {
make.bottom.equalTo(moreView.snp.top).offset(-18)
}
item.transform = .identity
} else {
if index == 0 {
make.top.equalToSuperview()
item.isShowSubviews = true
item.transform = .identity
} else {
// 1
make.bottom.equalTo(messageViews[0].snp.bottom).offset(min(index * 8, 1 * 8))
make.height.equalTo(40)
item.isShowSubviews = false
// index
let scale = 1 - CGFloat(index) * 0.04
item.transform = CGAffineTransform(scaleX: scale, y: 1)
}
if index == maxCount - 1 {
make.bottom.equalToSuperview().offset(-8)
}
}
}
}
}
}

View File

@ -1,83 +0,0 @@
//
// MessageTableViewCellViewModel.swift
// Bark
//
// Created by huangfeng on 2020/11/21.
// Copyright © 2020 Fin. All rights reserved.
//
import Differentiator
import Foundation
import RxCocoa
import RxDataSources
class MessageTableViewCellViewModel: ViewModel {
// 使crash
let message: Message
var identity: String
let title: BehaviorRelay<String>
let subtitle: BehaviorRelay<String>
let body: BehaviorRelay<String>
let url: BehaviorRelay<String>
let date = BehaviorRelay<String>(value: "")
var dateStyle = BehaviorRelay<MessageListCellDateStyle>(value: .relative)
init(message: Message) {
self.message = message
self.identity = message.id
self.title = BehaviorRelay<String>(value: message.title ?? "")
self.subtitle = BehaviorRelay<String>(value: message.subtitle ?? "")
self.body = BehaviorRelay<String>(value: message.body ?? "")
self.url = BehaviorRelay<String>(value: message.url ?? "")
super.init()
dateStyle.map { style in
switch style {
case .relative:
return self.message.createDate?.agoFormatString() ?? ""
case .exact:
return self.message.createDate?.formatString(format: "yyyy-MM-dd HH:mm") ?? ""
}
}
.bind(to: date)
.disposed(by: rx.disposeBag)
}
}
struct MessageSection {
var header: String
var messages: [MessageTableViewCellViewModel]
}
extension MessageSection: AnimatableSectionModelType {
typealias Item = MessageTableViewCellViewModel
typealias Identity = String
var items: [MessageTableViewCellViewModel] {
return self.messages
}
init(original: MessageSection, items: [MessageTableViewCellViewModel]) {
self = original
self.messages = items
}
var identity: String {
return header
}
}
extension MessageTableViewCellViewModel: IdentifiableType {
typealias Identity = String
override func isEqual(_ object: Any?) -> Bool {
if let obj = object as? MessageTableViewCellViewModel {
// cell12 ...
return self.identity == obj.identity && self.date.value == obj.date.value
}
return super.isEqual(object)
}
}