mirror of
https://github.com/Finb/Bark.git
synced 2025-12-08 21:36:01 +00:00
Compare commits
17 Commits
80732720ce
...
5766706e6d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5766706e6d | ||
|
|
ca6dcd2be3 | ||
|
|
64a66b16b8 | ||
|
|
83e116f005 | ||
|
|
c1c7ba3b31 | ||
|
|
eb77151fdc | ||
|
|
2f554e9e92 | ||
|
|
f38cf5b7d2 | ||
|
|
ca057fbf3a | ||
|
|
6915fe532b | ||
|
|
80f2bda954 | ||
|
|
a82f65833f | ||
|
|
437f4f92ab | ||
|
|
8c88a0e897 | ||
|
|
9dbd9aec06 | ||
|
|
3f8e7ef150 | ||
|
|
612cadafec |
@ -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 */,
|
||||
|
||||
@ -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))
|
||||
|
||||
38
Bark/Assets.xcassets/Colors/black.colorset/Contents.json
Normal file
38
Bark/Assets.xcassets/Colors/black.colorset/Contents.json
Normal 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
|
||||
}
|
||||
}
|
||||
22
Bark/Assets.xcassets/group_collapse.imageset/Contents.json
vendored
Normal file
22
Bark/Assets.xcassets/group_collapse.imageset/Contents.json
vendored
Normal 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 |
22
Bark/Assets.xcassets/group_expand.imageset/Contents.json
vendored
Normal file
22
Bark/Assets.xcassets/group_expand.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
Bark/Assets.xcassets/group_expand.imageset/folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz24 1.png
vendored
Normal file
BIN
Bark/Assets.xcassets/group_expand.imageset/folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz24 1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
Bark/Assets.xcassets/group_expand.imageset/folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz24.png
vendored
Normal file
BIN
Bark/Assets.xcassets/group_expand.imageset/folder_code_24dp_5F6368_FILL0_wght400_GRAD0_opsz24.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@ -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 |
@ -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" : {
|
||||
|
||||
@ -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")!
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
// message操作alert
|
||||
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])
|
||||
|
||||
@ -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())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
97
Model/MessageItemModel.swift
Normal file
97
Model/MessageItemModel.swift
Normal 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 {
|
||||
/// 相对时间,例如 1分钟前、1小时前
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Model/MessageSection.swift
Normal file
80
Model/MessageSection.swift
Normal 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
|
||||
}
|
||||
}
|
||||
80
View/MessageList/CustomTapTextView.swift
Normal file
80
View/MessageList/CustomTapTextView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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?()
|
||||
}
|
||||
}
|
||||
|
||||
56
View/MessageList/MessageGroupMoreView.swift
Normal file
56
View/MessageList/MessageGroupMoreView.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
@ -8,13 +8,6 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
enum MessageListCellDateStyle {
|
||||
/// 相对时间,例如 1分钟前、1小时前
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
// 消息列表cell上显示的时间需要随着时间的变化而变化(1分钟前、2分钟前 ...),如果时间不一样的就需要刷新界面
|
||||
return self.identity == obj.identity && self.date.value == obj.date.value
|
||||
}
|
||||
return super.isEqual(object)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user